# Laravel AI SDK 用の Amazon Bedrock ドライバー Source: https://kawax.biz/jp/packages/laravel-amazon-bedrock Laravel AI SDK 用の Amazon Bedrock ドライバー。テキスト生成、ストリーミング、ツール使用、構造化出力、ファイル添付、会話履歴、埋め込み、画像生成、音声(TTS)、リランキングをサポートします。 ## 概要 [revolution/laravel-amazon-bedrock](https://github.com/invokable/laravel-amazon-bedrock) は、[Laravel AI SDK](https://github.com/laravel/ai) で Amazon Bedrock を使うためのドライバーです。Bedrock の複数モデルを Laravel AI SDK の統一 API で扱えます。 Laravel AI SDK v1.0がリリースされ、Bedrockは公式SDKを使えば十分になったのでこのパッケージは終了しました | 機能 | Bedrock API キーのみで利用可 | 対応モデル / サービス | | - | - | - | | テキスト生成 / ストリーミング | ✅ | Anthropic Claude、Amazon Nova、その他ほとんどの Bedrock モデル(すべて Converse API 経由) | | ツール使用(Function Calling) | ✅ | | | 構造化出力 | ✅ | | | ファイル添付 | ✅ | 画像、ドキュメント、音声、動画を Converse API 経由で添付(モデルごとに対応形式は異なる) | | 画像生成 | ✅ | Stability AI(既定)、Amazon Nova Canvas(非推奨) | | 音声(TTS) | ⚠️ | Amazon Polly(generative / neural / long-form / standard エンジン) | | 文字起こし(STT) | ❌ | 未サポート | | 埋め込み | ✅ | Amazon Titan Embeddings V2(既定)、Cohere Embed English/Multilingual V3、Cohere Embed V4(バッチ対応) | | リランキング | ⚠️ | Cohere Rerank 3.5、Amazon Rerank 1.0 | | ファイル | ✅ | テキスト生成でローカルファイル添付に対応。サーバー側へのアップロードや `fromId()` は未対応 | 表の `⚠️` は「機能自体が未対応」ではなく「Bedrock API キーのみでは利用できない」ことを示します。 主な特徴は次のとおりです。 * **認証**: Bedrock API キー、AWS IAM クレデンシャル(SigV4)、デフォルト AWS クレデンシャルチェーン(IAM ロール、インスタンスプロファイル等)から選べます。 * **フェイルオーバー**: AI SDK のマルチプロバイダーフェイルオーバーに対応。レート制限(429)、過負荷(503、529)、クレジット系エラーはフェイルオーバー可能な例外にマッピングされます。 * **キャッシュ制御**: [Bedrock Converse API](https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html) のシステムプロンプトに ephemeral キャッシュを常時有効化。 * **統一 API**: Anthropic Claude、Amazon Nova、Meta Llama、Mistral など全モデルを Bedrock Converse API 経由で統一インターフェースで扱えます。 ## 必要要件 * PHP >= 8.3 * Laravel >= 12.x ## インストール ```bash theme={null} composer require revolution/laravel-amazon-bedrock ``` ```bash theme={null} php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider" ``` ## 設定 `config/ai.php` に `amazon-bedrock` プロバイダーを追加します。必要に応じて既定プロバイダーも Bedrock に切り替えます。 ```php theme={null} 'default' => 'amazon-bedrock', 'default_for_images' => 'amazon-bedrock', 'default_for_audio' => 'amazon-bedrock', 'default_for_embeddings' => 'amazon-bedrock', 'default_for_reranking' => 'amazon-bedrock', ``` ### Option 1: Bedrock API キー ```php theme={null} 'providers' => [ 'amazon-bedrock' => [ 'driver' => 'amazon-bedrock', 'key' => env('AWS_BEDROCK_API_KEY', ''), 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), ], ], ``` ```dotenv theme={null} AWS_BEDROCK_API_KEY=your_api_key AWS_DEFAULT_REGION=us-east-1 ``` Bedrock API キーは AWS マネジメントコンソールから取得します。 Bedrock API キーは Bedrock Runtime API 専用です。`bedrock-agent-runtime` を使うリランキングや Amazon Polly(TTS)では使えません。これらは SigV4 またはデフォルト AWS クレデンシャルチェーンを使ってください。 ### Option 2: AWS IAM クレデンシャル(SigV4) [Signature Version 4](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html) で署名する AWS アクセスキーとシークレットキーを使います。 ```php theme={null} 'providers' => [ 'amazon-bedrock' => [ 'driver' => 'amazon-bedrock', 'key' => env('AWS_ACCESS_KEY_ID'), 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'token' => env('AWS_SESSION_TOKEN'), 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), ], ], ``` ```dotenv theme={null} AWS_ACCESS_KEY_ID=AKIA... AWS_SECRET_ACCESS_KEY=wJalr... AWS_SESSION_TOKEN= # 任意(STS 一時クレデンシャル用) AWS_DEFAULT_REGION=us-east-1 ``` `AWS_SESSION_TOKEN` は一時クレデンシャル(STS)を使う場合のみ設定します。 ### Option 3: デフォルト AWS クレデンシャルチェーン(IAM ロール) EC2 / ECS / Lambda など IAM ロールがある環境では、`key` と `secret` を省略して [デフォルトの AWS クレデンシャルプロバイダーチェーン](https://docs.aws.amazon.com/sdkref/latest/guide/standardized-credentials.html) を使えます。 ```php theme={null} 'providers' => [ 'amazon-bedrock' => [ 'driver' => 'amazon-bedrock', 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), ], ], ``` ```dotenv theme={null} AWS_DEFAULT_REGION=us-east-1 ``` デフォルトのクレデンシャルチェーンは、環境変数、共有クレデンシャルファイル(`~/.aws/credentials`)、ECS タスクロール、EC2 インスタンスプロファイルなどから自動で解決します。 ### 任意の設定キー | キー | 説明 | 既定値 | | - | - | - | | `secret` | AWS シークレットアクセスキー(SigV4) | — | | `token` | AWS セッショントークン(SigV4) | — | | `timeout` | HTTP リクエストタイムアウト(秒) | 30 | | `max_tokens` | 1 リクエスト当たりの既定 max tokens | 8096 | | `models.text.default` | 既定のテキストモデル | Claude Sonnet | | `models.text.cheapest` | 最安テキストモデル | Claude Haiku | | `models.text.smartest` | 最高性能テキストモデル | Claude Opus / Fable | | `models.embeddings.default` | 既定の埋め込みモデル | `amazon.titan-embed-text-v2:0` | | `models.embeddings.dimensions` | 既定の埋め込み次元数 | `1024` | | `models.image.default` | 既定の画像モデル | `stability.stable-image-core-v1:1` | | `models.audio.default` | 既定の音声(TTS)エンジン | `generative` | | `models.reranking.default` | 既定のリランキングモデル | `cohere.rerank-v3-5:0` | ## テキスト生成 ### Agent クラス Artisan コマンドで Agent クラスを作成します。 ```bash theme={null} php artisan make:agent BedrockAgent ``` ```php theme={null} prompt('Laravel について教えてください。'); echo $response->text; ``` ### Anonymous Agent クラスを作らずに手早く使う場合は `agent()` ヘルパーを利用します。 ```php theme={null} use function Laravel\Ai\agent; $response = agent( instructions: 'あなたはソフトウェア開発の専門家です。', )->prompt('Laravel について教えてください。'); echo $response->text; ``` ### ストリーミング ```php theme={null} use App\Ai\Agents\BedrockAgent; use Illuminate\Support\Facades\Route; Route::get('/stream', function () { return (new BedrockAgent)->stream('Laravel について教えてください。'); }); ``` イベントを手動で扱うこともできます。 ```php theme={null} use Laravel\Ai\Streaming\Events\TextDelta; use function Laravel\Ai\agent; $stream = agent( instructions: 'あなたはソフトウェア開発の専門家です。', )->stream('Laravel について教えてください。'); foreach ($stream as $event) { if ($event instanceof TextDelta) { echo $event->delta; } } ``` ### ツール使用(Function Calling) 生成中に呼び出されるツールを定義します。 ```php theme={null} use Illuminate\Contracts\JsonSchema\JsonSchema; use Laravel\Ai\Contracts\Tool; use Laravel\Ai\Tools\Request; class GetWeather implements Tool { public function description(): string { return '都市の現在の天気を返します。'; } public function schema(JsonSchema $schema): array { return [ 'city' => $schema->string()->required()->description('都市名'), ]; } public function handle(Request $request): string { // ここで天気 API を呼び出す return json_encode(['temperature' => 22, 'condition' => 'sunny']); } } ``` Agent から利用します。 ```php theme={null} use App\Ai\Tools\GetWeather; use Laravel\Ai\Attributes\MaxSteps; use Laravel\Ai\Contracts\Agent; use Laravel\Ai\Contracts\HasTools; use Laravel\Ai\Promptable; #[MaxSteps(5)] class WeatherAgent implements Agent, HasTools { use Promptable; public function tools(): array { return [new GetWeather]; } public function instructions(): string { return 'あなたは天気アシスタントです。'; } } $response = (new WeatherAgent)->prompt('東京の天気は?'); echo $response->text; ``` Anonymous Agent でも使えます。 ```php theme={null} use function Laravel\Ai\agent; $response = agent( instructions: 'あなたは天気アシスタントです。', tools: [new GetWeather], maxSteps: 5, )->prompt('東京の天気は?'); ``` ストリーミングでもツール呼び出しは動作します。SDK が自動でツールを実行し、最終的なテキスト応答が出るまで会話を継続します。 ### ファイル添付 `attachments` パラメーターで画像、ドキュメント、音声、動画ファイルをプロンプトに添付できます。Bedrock Converse API が添付ブロックを処理しますが、実際にどの形式が使えるかはモデルに依存します(例: Anthropic Claude は画像とドキュメントのみ対応)。 ```php theme={null} use Laravel\Ai\Files\Document; use Laravel\Ai\Files\Image; use function Laravel\Ai\agent; // ローカルパスの画像を添付 $response = agent( instructions: 'あなたは便利なアシスタントです。', )->prompt('この画像を説明してください。', attachments: [ Image::fromPath('/path/to/photo.jpg'), ]); // URL のドキュメントを添付 $response = agent( instructions: 'あなたは便利なアシスタントです。', )->prompt('このドキュメントを要約してください。', attachments: [ Document::fromUrl('https://example.com/report.pdf'), ]); // 文字列から添付(明示的にフォーマット指定) $response = agent( instructions: 'あなたは便利なアシスタントです。', )->prompt('このテキストを分析してください。', attachments: [ Document::fromString($csvContent, 'text/csv')->as('data.csv'), ]); ``` サポートされている添付タイプは `Laravel\Ai\Files\*` の `Image`、`Document`、`Audio` です。動画は `Illuminate\Http\UploadedFile` 経由で添付できます。 サーバー側のファイルアップロード(`Document::fromPath()->put()`)や ID 経由の再利用(`Document::fromId()`)は Bedrock では未対応です。 ### 会話履歴 複数ターンの会話を維持するには Agent クラスで `Conversational` インターフェースを実装します。`messages()` で過去の会話メッセージを返すと、各プロンプトに自動で含まれます。 ```php theme={null} userId) ->latest() ->limit(20) ->get() ->reverse() ->map(fn ($m) => new Message($m->role, $m->content)) ->all(); } } ``` ```php theme={null} $response = (new ChatAgent(auth()->id()))->prompt('前回何を話していましたか?'); ``` #### `RemembersConversations` で自動保存 `messages()` を自分で実装したくない場合は `RemembersConversations` トレイトで完全自動の会話保存を利用できます。AI SDK のデータベーステーブルが必要なので、先に `php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider" && php artisan migrate` を実行してください。 ```php theme={null} forUser($user)->prompt('こんにちは!'); $conversationId = $response->conversationId; ``` 既存の会話を継続します。 ```php theme={null} $response = (new ChatAgent) ->continue($conversationId, as: $user) ->prompt('もっと詳しく教えてください。'); ``` Bedrock ドライバーは Bedrock Converse API リクエストに会話履歴を自動で含めるため、対応する全モデルでマルチターン会話のコンテキストを利用できます。 ### 構造化出力 `HasStructuredOutput` インターフェースを実装すると、型付きの応答を取得できます。 ```php theme={null} use Illuminate\Contracts\JsonSchema\JsonSchema; use Laravel\Ai\Contracts\Agent; use Laravel\Ai\Contracts\HasStructuredOutput; use Laravel\Ai\Promptable; class ExtractPerson implements Agent, HasStructuredOutput { use Promptable; public function instructions(): string { return '与えられた文章から人物情報を抽出してください。'; } public function schema(JsonSchema $schema): array { return [ 'name' => $schema->string()->description('人物のフルネーム'), 'age' => $schema->integer()->description('年齢'), 'occupation' => $schema->string()->description('職業'), ]; } } $response = (new ExtractPerson)->prompt('John は 30 歳のソフトウェアエンジニアです。'); echo $response['name']; // "John" echo $response['age']; // 30 echo $response['occupation']; // "software engineer" ``` Anonymous Agent でも構造化出力が使えます。 ```php theme={null} use function Laravel\Ai\agent; $response = agent( instructions: '与えられた文章から人物情報を抽出してください。', schema: fn (JsonSchema $schema) => [ 'name' => $schema->string()->description('人物のフルネーム'), 'age' => $schema->integer()->description('年齢'), ], )->prompt('Alice is 25 years old.'); echo $response['name']; // "Alice" echo $response['age']; // 25 ``` 内部的にはスキーマに沿った値を返させる合成ツール(`output_structured_data`)を作成しています。この方式は Converse API 経由で Bedrock 上の全モデルと互換性があります。 ### Converse API(全モデル) すべてのテキスト生成とストリーミングは [Bedrock Converse API](https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html) 経由で実行されます。Anthropic Claude も含めて統一されており、Amazon Nova、Meta Llama、Mistral、Cohere、DeepSeek など、Bedrock 上の各種モデルを同じインターフェースで利用できます。 ```php theme={null} use function Laravel\Ai\agent; // Anthropic Claude(Converse API 経由) $response = agent( instructions: 'あなたはソフトウェア開発の専門家です。', model: 'global.anthropic.claude-sonnet-4-6', )->prompt('Laravel について教えてください。'); // Amazon Nova $response = agent( instructions: 'あなたは便利なアシスタントです。', model: 'amazon.nova-pro-v1:0', )->prompt('AWS について教えてください。'); // Meta Llama $response = agent( instructions: 'あなたは便利なアシスタントです。', model: 'meta.llama3-1-70b-instruct-v1:0', )->prompt('量子コンピューティングを説明してください。'); // Mistral $response = agent( instructions: 'あなたは便利なアシスタントです。', model: 'mistral.mistral-large-2402-v1:0', )->prompt('コーディングについての俳句を書いてください。'); // Cohere Command R+ $response = agent( instructions: 'あなたは便利なアシスタントです。', model: 'cohere.command-r-plus-v1:0', )->prompt('このテキストを要約してください。'); // DeepSeek R1 $response = agent( instructions: 'あなたは便利なアシスタントです。', model: 'deepseek.r1-v1:0', )->prompt('この数学の問題を解いてください。'); ``` ストリーミング、ツール使用、構造化出力、ファイル添付は、それらをサポートしているモデルで動作します。詳細は [Bedrock 対応モデル一覧](https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-supported-models-features.html) を参照してください。 ### Provider Options `anthropic_version` などの Bedrock 固有オプションを渡すには `HasProviderOptions` を実装します。 ```php theme={null} 0.9, ]; } } ``` サポートされる Provider Options: | オプション | 説明 | 既定値 | | - | - | - | | `top_k` | Top-K サンプリングパラメーター | — | | `top_p` | Top-P(nucleus)サンプリングパラメーター | — | | `additionalModelRequestFields` | Converse API のモデル固有追加パラメーター | — | ### Agent の設定属性 PHP 属性でテキスト生成のオプションを設定できます。 ```php theme={null} use Laravel\Ai\Attributes\MaxTokens; use Laravel\Ai\Attributes\Temperature; use Laravel\Ai\Attributes\Timeout; use Laravel\Ai\Contracts\Agent; use Laravel\Ai\Promptable; #[MaxTokens(4096)] #[Temperature(0.7)] #[Timeout(120)] class BedrockAgent implements Agent { use Promptable; // ... } ``` ## 画像生成 Stability AI モデル(既定)または Amazon Nova Canvas を使って画像を生成します。 ```php theme={null} use Laravel\Ai\Image; use Revolution\Amazon\Bedrock\Bedrock; // 既定で Stability AI Stable Image Core を使用 $response = Image::of('かわいいスチームパンクのロボット')->generate(provider: Bedrock::KEY); // 最初の画像を取得 $image = $response->firstImage(); // ディスクに保存 $response->store('images', 's3'); // HTML の タグとして出力 echo $response->toHtml('スチームパンクのロボット'); ``` 利用できる Stability AI モデル(すべて `us-west-2` リージョンが必要): ```php theme={null} // Stable Image Core — 高速・低コスト(既定) $response = Image::of('風景') ->generate(provider: Bedrock::KEY, model: 'stability.stable-image-core-v1:1'); // Stable Diffusion 3.5 Large — 高品質・大量生成 $response = Image::of('ポートレート') ->generate(provider: Bedrock::KEY, model: 'stability.sd3-5-large-v1:0'); // Stable Image Ultra — 超リアル・最高品質 $response = Image::of('高級感のある製品') ->generate(provider: Bedrock::KEY, model: 'stability.stable-image-ultra-v1:1'); ``` Stability AI の画像モデルは `us-west-2` でのみ利用可能です。これらのモデルを使うときは `AWS_DEFAULT_REGION=us-west-2` を設定してください。 ### Stability AI による画像編集 Stability AI Image Services の編集系モデルも `attachments()` メソッドで利用できます。入力画像を渡し、編集モデルで変換します。 ```php theme={null} use Laravel\Ai\Files\Image as ImageFile; use Laravel\Ai\Image; use Revolution\Amazon\Bedrock\Bedrock; $inputImage = ImageFile::fromPath('/path/to/photo.jpg'); // Inpaint — マスクやアルファチャンネルで領域を塗り替え $response = Image::of('背景を森に置き換えてください') ->attachments([$inputImage]) ->generate(provider: Bedrock::KEY, model: 'stability.stable-image-inpaint-v1:0'); // Erase — 不要な要素を削除 $response = Image::of('') ->attachments([$inputImage]) ->generate(provider: Bedrock::KEY, model: 'stability.stable-image-erase-object-v1:0'); // 背景を削除して被写体だけを切り抜き $response = Image::of('') ->attachments([$inputImage]) ->generate(provider: Bedrock::KEY, model: 'stability.stable-image-remove-background-v1:0'); // Search and Replace — プロンプトで指定したオブジェクトを置き換え $response = Image::of('猫') ->attachments([$inputImage]) ->generate(provider: Bedrock::KEY, model: 'stability.stable-image-search-replace-v1:0'); // Style Transfer — プロンプトのスタイルを適用 $response = Image::of('油絵風') ->attachments([$inputImage]) ->generate(provider: Bedrock::KEY, model: 'stability.stable-style-transfer-v1:0'); ``` 利用できる Stability AI 編集モデル(すべて `us-east-1`、`us-east-2`、`us-west-2` で利用可能): | Model ID | 説明 | | - | - | | `stability.stable-image-inpaint-v1:0` | Inpaint — 選択領域の塗り替え | | `stability.stable-outpaint-v1:0` | Outpaint — 画像を境界外に拡張 | | `stability.stable-image-erase-object-v1:0` | Erase — オブジェクトを削除 | | `stability.stable-image-remove-background-v1:0` | 背景の削除 | | `stability.stable-image-search-replace-v1:0` | Search and Replace — オブジェクトの置換 | | `stability.stable-image-search-recolor-v1:0` | Search and Recolor — オブジェクトの色変更 | | `stability.stable-image-style-guide-v1:0` | Style Guide — リファレンス画像でスタイルを適用 | | `stability.stable-style-transfer-v1:0` | Style Transfer — 画風を転送 | | `stability.stable-image-control-sketch-v1:0` | Control Sketch — スケッチから生成 | | `stability.stable-image-control-structure-v1:0` | Control Structure — 構造に従って生成 | | `stability.stable-creative-upscale-v1:0` | Creative Upscale — 補完しながらアップスケール | | `stability.stable-conservative-upscale-v1:0` | Conservative Upscale — 細部を保持 | | `stability.stable-fast-upscale-v1:0` | Fast Upscale — 軽量な 4 倍アップスケール | Amazon Nova Canvas もサポートしていますが、AWS により非推奨化が進んでいます。 ```php theme={null} // Nova Canvas(非推奨。us-east-1、ap-northeast-1、eu-west-1 で利用可能) $response = Image::of('夕焼け') ->size('3:2') // '1:1', '3:2', '2:3' ->quality('high') // 'low', 'medium', 'high'(Nova Canvas のみ) ->generate(provider: Bedrock::KEY, model: 'amazon.nova-canvas-v1:0'); ``` ## 音声(TTS) [Amazon Polly](https://docs.aws.amazon.com/polly/latest/dg/what-is.html) を使ってテキストから音声を生成します。 ```php theme={null} use Laravel\Ai\Audio; use Revolution\Amazon\Bedrock\Bedrock; $response = Audio::of('Laravel でコーディングするのが大好きです。')->generate(provider: Bedrock::KEY); $rawContent = (string) $response; ``` 男声 / 女声を指定できます。 ```php theme={null} $response = Audio::of('Laravel でコーディングするのが大好きです。') ->female() ->generate(provider: 'bedrock'); $response = Audio::of('Laravel でコーディングするのが大好きです。') ->male() ->generate(provider: Bedrock::KEY); ``` 特定の [Polly ボイス](https://docs.aws.amazon.com/polly/latest/dg/voicelist.html) を指定します。 ```php theme={null} $response = Audio::of('Laravel でコーディングするのが大好きです。') ->voice('Joanna') ->generate(provider: Bedrock::KEY); ``` 生成された音声を保存します。 ```php theme={null} $response = Audio::of('Laravel でコーディングするのが大好きです。')->generate(provider: Bedrock::KEY); $path = $response->store(); $path = $response->storeAs('audio.mp3'); ``` エンジン(モデル)を指定することもできます。 ```php theme={null} // generative(既定)、neural、long-form、standard $response = Audio::of('Laravel でコーディングするのが大好きです。') ->generate(provider: Bedrock::KEY, model: 'neural'); ``` **既定ボイス:** `default-female` → Ruth、`default-male` → Matthew(どちらも generative エンジンに対応)。 Amazon Polly は Bedrock とは別の AWS サービスです。Bedrock API キー(bearer token)は Polly では使えません。AWS IAM クレデンシャル(SigV4)またはデフォルト AWS クレデンシャルチェーンを使ってください。 ## 埋め込み Amazon Titan Embeddings V2 を使ってベクトル埋め込みを生成します。 ```php theme={null} use Laravel\Ai\Embeddings; use Revolution\Amazon\Bedrock\Bedrock; $response = Embeddings::for(['Hello world', 'Foo bar'])->generate(provider: Bedrock::KEY); // 最初の埋め込みベクトル $vector = $response->first(); // 全ての埋め込みを反復処理 foreach ($response as $embedding) { // $embedding は float の配列 } echo $response->tokens; // 合計トークン数 ``` 次元数を指定できます(Titan Embeddings V2 は 256、512、1024)。 ```php theme={null} $response = Embeddings::for(['Hello world'])->dimensions(512)->generate(provider: Bedrock::KEY); ``` カスタムモデルを指定する例。 ```php theme={null} $response = Embeddings::for(['Hello world']) ->dimensions(1024) ->generate(provider: Bedrock::KEY, model: 'amazon.titan-embed-text-v2:0'); ``` ### Cohere Embed モデル Cohere Embed モデルは自動検出され、バッチ API を使用します。Titan が入力ごとに HTTP リクエストを送るのに対し、Cohere は全入力を 1 リクエストにまとめるため、複数テキストを処理する場合に効率的です。 ```php theme={null} // Cohere Embed English V3 $response = Embeddings::for(['Hello world', 'Foo bar']) ->dimensions(1024) ->generate(provider: Bedrock::KEY, model: 'cohere.embed-english-v3'); // Cohere Embed Multilingual V3 $response = Embeddings::for(['Hello', 'こんにちは']) ->dimensions(1024) ->generate(provider: Bedrock::KEY, model: 'cohere.embed-multilingual-v3'); // Cohere Embed V4(出力次元 256〜1536 を設定可能) $response = Embeddings::for(['Hello world']) ->dimensions(512) ->generate(provider: Bedrock::KEY, model: 'cohere.embed-v4'); ``` Cohere Embed モデルはトークン数を返さないため、`$response->tokens` は常に `0` です。 ## リランキング Cohere Rerank 3.5 や Amazon Rerank 1.0 を使い、クエリとの関連度でドキュメントを並べ替えます。 ```php theme={null} use Laravel\Ai\Reranking; use Revolution\Amazon\Bedrock\Bedrock; $response = Reranking::of([ 'Laravel は PHP の Web フレームワークです。', 'Python はプログラミング言語です。', 'Laravel は Web 開発のためのエレガントな構文を提供します。', ])->rerank(query: 'Laravel とは何ですか?', provider: Bedrock::KEY); // もっとも関連度の高いドキュメント echo $response->first()->document; echo $response->first()->score; // すべてを並べ替え順に取得 foreach ($response as $result) { echo "{$result->index}: {$result->document} ({$result->score})\n"; } // 件数を制限 $response = Reranking::of([...]) ->limit(2) ->rerank(query: 'Laravel とは?', provider: Bedrock::KEY); ``` カスタムモデルを指定する例。 ```php theme={null} $response = Reranking::of([...]) ->rerank(query: '検索クエリ', provider: Bedrock::KEY, model: 'amazon.rerank-v1:0'); ``` リランキング API は `bedrock-agent-runtime` エンドポイントを使います(`bedrock-runtime` ではありません)。Amazon Rerank 1.0 は `us-east-1` で利用できないため、そのリージョンでは Cohere Rerank 3.5 を使ってください。 ## テスト AI SDK の標準的なテスト機能をそのまま利用できます。 公式ドキュメントには記載がありませんが、`agent()` ヘルパーで作った Anonymous Agent は `AnonymousAgent::fake()`、構造化出力版は `StructuredAnonymousAgent::fake()` でモックできます。 ```php theme={null} use Laravel\Ai\AnonymousAgent; use Laravel\Ai\Prompts\AgentPrompt; use function Laravel\Ai\agent; it('can generate text', function () { AnonymousAgent::fake(); $response = agent( instructions: 'あなたはソフトウェア開発の専門家です。', )->prompt('Laravel について教えてください。'); AnonymousAgent::assertPrompted(function (AgentPrompt $prompt) { return $prompt->contains('Laravel'); }); }); ``` 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-amazon-bedrock) を参照してください。 # 認証方法の比較 - Laravel Bluesky Source: https://kawax.biz/jp/packages/laravel-bluesky/authentication Laravel Bluesky の App Password (LegacyAgent) と OAuth (OAuthAgent) の違い、内部アーキテクチャ、コード例を解説します。 ## 概要 Laravel Bluesky は2種類の認証方法をサポートします。認証後の API 呼び出しは両方法で共通です。 | 項目 | App Password | OAuth | | - | - | - | | 内部クラス | `LegacyAgent` / `LegacySession` | `OAuthAgent` / `OAuthSession` | | エントリーポイント | `Bluesky::login()` | `Bluesky::withToken(OAuthSession)` | | 秘密鍵 | 不要 | `BLUESKY_OAUTH_PRIVATE_KEY` が必要 | | ユーザーの認可操作 | 不要 | 必要(ブラウザでの承認) | | バックグラウンド実行 | ✅ 得意 | ✅ 可能(refresh\_token を保存) | | セッションキー | `accessJwt` / `refreshJwt` | `access_token` / `refresh_token` | | 廃止予定 | **なし** | — | | 主な用途 | 自動投稿・通知・バッチ処理 | ユーザー代理操作・Socialite 連携 | `LegacyAgent` という命名は「OAuth 以前の認証方式」を意味しますが、App Password 自体は廃止予定ではありません。通知や自動投稿など、ユーザー操作を伴わない場面では App Password の方がシンプルで適しています。 ## アーキテクチャ ```mermaid theme={null} classDiagram class BlueskyManager { +login(identifier, password) Factory +withToken(session) Factory +agent() Agent +check() bool +refreshSession() Factory } class LegacyAgent { +session LegacySession +http() PendingRequest +refreshSession() self } class OAuthAgent { +session OAuthSession +http() PendingRequest +refreshSession() self } class LegacySession { +accessJwt +refreshJwt +did +handle +token() string +refresh() string } class OAuthSession { +access_token +refresh_token +did +iss +token() string +refresh() string +issuer() string } BlueskyManager --> LegacyAgent : login() BlueskyManager --> OAuthAgent : withToken(OAuthSession) BlueskyManager --> LegacyAgent : withToken(LegacySession) LegacyAgent --> LegacySession : holds OAuthAgent --> OAuthSession : holds ``` `BlueskyManager` は Facade `Bluesky` の実体です。`login()` または `withToken()` でエージェントを設定し、その後の API 呼び出しはどちらの認証方法でも同じメソッドを使います。 ## App Password (LegacyAgent) ### 認証フロー ```mermaid theme={null} sequenceDiagram participant App as Laravelアプリ participant Bluesky as Bluesky サーバー App->>Bluesky: createSession(identifier, password) Bluesky-->>App: LegacySession (accessJwt + refreshJwt) App->>App: LegacyAgent::create(session) App->>Bluesky: API呼び出し (Bearer accessJwt) Bluesky-->>App: レスポンス Note over App: accessJwt が期限切れの場合 App->>Bluesky: refreshSession (refreshJwt) Bluesky-->>App: 新しい accessJwt + refreshJwt ``` ### Bluesky::login() `.env` に App Password を設定して `login()` を呼ぶだけです。 ```dotenv theme={null} BLUESKY_IDENTIFIER=your-handle.bsky.social BLUESKY_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx ``` ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; $response = Bluesky::login( identifier: config('bluesky.identifier'), password: config('bluesky.password'), )->post('Hello Bluesky'); ``` ### LegacySession の再利用 毎回 `login()` を呼ぶと毎回 API リクエストが発生します。セッションをキャッシュして再利用するとより効率的です。 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Session\LegacySession; // 初回ログインしてセッションを保存 Bluesky::login( identifier: config('bluesky.identifier'), password: config('bluesky.password'), ); cache()->put('bluesky_session', Bluesky::agent()->session()->toArray(), now()->addDay()); // 次回以降はキャッシュから復元 $session = LegacySession::create(cache('bluesky_session', [])); Bluesky::withToken($session); // アクセストークンが期限切れなら更新 if (! Bluesky::check()) { Bluesky::refreshSession(); } $response = Bluesky::post('Hello from cached session'); ``` ### LegacySession の主なキー | キー | 内容 | メソッド | | - | - | - | | `accessJwt` | アクセストークン | `token()` | | `refreshJwt` | リフレッシュトークン | `refresh()` | | `did` | Bluesky DID | `did()` | | `handle` | ハンドル | `handle()` | | `email` | メールアドレス | `email()` | | `active` | アカウントがアクティブか | `active()` | ### 適したユースケース * バックグラウンドジョブ・キュー処理での自動投稿 * Laravel Notification チャンネルを使った通知 * バッチ処理・スケジューリング * アプリケーション自身のアカウントで投稿する場合 ## OAuth (OAuthAgent) ### 認証フロー ```mermaid theme={null} sequenceDiagram participant User as ユーザー participant App as Laravelアプリ participant Bluesky as Bluesky サーバー User->>App: ログインフォーム送信(ハンドル入力) App->>Bluesky: PAR リクエスト(login_hint 含む) Bluesky-->>App: request_uri App->>Bluesky: 認可エンドポイントへリダイレクト User->>Bluesky: Bluesky でログイン承認 Bluesky-->>App: コールバック(code + iss) App->>Bluesky: DPoP を使ったトークン交換 Bluesky-->>App: OAuthSession (access_token + refresh_token) App->>User: ログイン完了 App->>Bluesky: API呼び出し (DPoP access_token) Bluesky-->>App: レスポンス ``` ### Bluesky::withToken() Socialite で取得した `OAuthSession` を `withToken()` に渡します。 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Session\OAuthSession; // Laravel セッションから復元(Web リクエスト) $session = OAuthSession::create(session('bluesky_session')); $timeline = Bluesky::withToken($session)->getTimeline(); ``` バックグラウンドジョブや Console でも、DB に保存した値から `OAuthSession` を組み立てられます。 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Session\OAuthSession; $session = OAuthSession::create([ 'did' => $user->did, 'refresh_token' => $user->refresh_token, // bsky.social 以外のアカウントは iss も指定 // 'iss' => $user->iss, ]); $response = Bluesky::withToken($session) ->refreshSession() ->post('Hello from OAuth'); ``` ### OAuthSession の主なキー | キー | 内容 | メソッド | | - | - | - | | `access_token` | アクセストークン | `token()` | | `refresh_token` | リフレッシュトークン(1回のみ使用可) | `refresh()` | | `did` / `sub` | Bluesky DID | `did()` | | `iss` | 認可サーバー URL | `issuer()` | | `profile.handle` | ハンドル | `handle()` | | `profile.displayName` | 表示名 | `displayName()` | OAuth の refresh\_token は1回しか使えません。`OAuthSessionUpdated` イベントを使って、トークン更新後は必ず DB を更新してください。詳細は [Socialite](/jp/packages/laravel-bluesky/socialite) を参照してください。 ### 適したユースケース * Socialite を使ったユーザーログイン * ユーザーの代わりに API を呼び出す操作 * ユーザーごとに異なるアカウントで操作が必要な場合 ## 認証後の API 呼び出しは共通 どちらの認証方法でも、`withToken()` の後は同じ API メソッドを使います。 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Session\LegacySession; use Revolution\Bluesky\Session\OAuthSession; // App Password Bluesky::login(config('bluesky.identifier'), config('bluesky.password')); // または OAuth $session = OAuthSession::create(session('bluesky_session')); Bluesky::withToken($session); // ↓ 以降のAPI呼び出しは完全に共通 ↓ Bluesky::post('Hello Bluesky'); Bluesky::getTimeline(); Bluesky::getProfile(); Bluesky::searchPosts(q: '#laravel'); ``` `BlueskyManager` は内部で `LegacyAgent` または `OAuthAgent` を使い分けますが、呼び出し側のコードには影響しません。 ## どちらを選ぶべきか ```mermaid theme={null} flowchart TD A["どちらの認証方法を使う?"] --> B{"ユーザーが Bluesky アカウントで
ログインする必要があるか?"} B -->|"はい"| C["OAuth (OAuthAgent)
Socialite 連携"] B -->|"いいえ"| D{"アプリ自身のアカウントで
操作するか?"} D -->|"はい"| E["App Password (LegacyAgent)
シンプルで運用しやすい"] D -->|"複数ユーザーの代理操作"| C ``` | 状況 | 推奨 | | - | - | | 自動投稿・通知・バッチ | App Password | | ユーザーログイン機能 | OAuth | | バックグラウンドジョブのみ | App Password | | ユーザー代理操作が必要 | OAuth | | 運用シンプルさ優先 | App Password | | セキュリティ・権限制御重視 | OAuth | 迷ったときは目的で分けるのが一番わかりやすいです。「アプリが自分で動く」なら App Password、「ユーザーが操作する」なら OAuth です。両方を組み合わせることも可能で、通知には App Password を使い、ユーザーログインには OAuth を使うという構成もよく見られます。 ## 参考リンク * [Basic client](/jp/packages/laravel-bluesky/basic-client) — 認証後の API 操作 * [Socialite](/jp/packages/laravel-bluesky/socialite) — OAuth フローの詳細 * [通知チャンネル](/jp/packages/laravel-bluesky/notification) — App Password / OAuth での通知 * Source: [invokable/laravel-bluesky](https://github.com/invokable/laravel-bluesky) # Basic client - Laravel Bluesky Source: https://kawax.biz/jp/packages/laravel-bluesky/basic-client Laravel Blueskyの基本クライアント操作。認証、投稿、メディア、リプライ、引用、フィード取得、検索を確認します。 ## 認証 Blueskyの認証は App Password と OAuth の2つです。新規実装ではOAuthの採用も検討してください。 ### App Password ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; $profile = Bluesky::login( identifier: config('bluesky.identifier'), password: config('bluesky.password'), )->getProfile(); ``` セッションを再利用する場合は `LegacySession` を使います。 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Session\LegacySession; Bluesky::login(identifier: config('bluesky.identifier'), password: config('bluesky.password')); cache()->put('bluesky_legacy_session', Bluesky::agent()->session()->toArray(), now()->addDay()); Bluesky::withToken(LegacySession::create(cache('bluesky_legacy_session', []))); if (! Bluesky::check()) { Bluesky::refreshSession(); } ``` ### OAuth Socialiteで取得したトークンを `OAuthSession` に変換して使います。 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Session\OAuthSession; $session = OAuthSession::create(session('bluesky_session')); $timeline = Bluesky::withToken($session)->getTimeline(); ``` OAuthフロー全体は [Socialite](/jp/packages/laravel-bluesky/socialite) を参照してください。 ## 投稿を作成する ### 単純なテキスト投稿 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; $response = Bluesky::withToken()->post('test'); ``` ### TextBuilderで投稿を組み立てる リンク・タグ・メンションを正しく扱うには `TextBuilder` を使います。 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\RichText\TextBuilder; $post = TextBuilder::make(text: 'test') ->newLine() ->mention('@example.bsky.social') ->newLine() ->link('https://bsky.app/') ->newLine() ->tag('#Laravel') ->toPost(); $response = Bluesky::withToken()->post($post); ``` ## メディアを添付する ### 画像添付 ```php theme={null} use Illuminate\Support\Facades\Storage; use Revolution\Bluesky\Embed\Images; use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Record\Post; Bluesky::withToken(); $images = Images::create()->add( alt: 'App screenshot', blob: fn (): array => Bluesky::uploadBlob( Storage::get('test.png'), Storage::mimeType('test.png') )->json('blob') ); $post = Post::create(text: 'image post')->embed($images); $response = Bluesky::post($post); ``` ### 外部リンクカード ```php theme={null} use Revolution\Bluesky\Embed\External; use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Record\Post; $external = External::create( title: 'Title', description: 'Description', uri: 'https://example.com' ); $post = Post::create(text: 'external card')->embed($external); $response = Bluesky::withToken()->post($post); ``` ## リプライ・引用 ### リプライ ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Record\Post; use Revolution\Bluesky\Types\ReplyRef; use Revolution\Bluesky\Types\StrongRef; $reply = ReplyRef::to( root: StrongRef::to(uri: 'at://', cid: 'cid'), parent: StrongRef::to(uri: 'at://', cid: 'cid') ); $post = Post::create(text: 'reply')->reply($reply); $response = Bluesky::withToken()->post($post); ``` ### 引用投稿 ```php theme={null} use Revolution\Bluesky\Embed\QuoteRecord; use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Record\Post; use Revolution\Bluesky\Types\StrongRef; $quote = QuoteRecord::create(StrongRef::to(uri: 'at://', cid: 'cid')); $post = Post::create(text: 'quote')->embed($quote); $response = Bluesky::withToken()->post($post); ``` ## フィード取得 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; $response = Bluesky::withToken()->getTimeline(); $items = $response->collect('feed'); ``` 公開投稿を取得するだけなら、認証なしのメソッドも使えます。 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; $feed = Bluesky::getAuthorFeed(actor: 'alice.bsky.social')->json('feed'); ``` ## 検索 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; $response = Bluesky::withToken()->searchPosts(q: '#bluesky', limit: 10); ``` `searchPosts()` はAPI側の一時的な制限により、未認証アクセスでは現在利用できません。 Source: [docs/basic-client.md](https://github.com/invokable/laravel-bluesky/blob/main/docs/basic-client.md) # BlueskyManager と HasShortHand Source: https://kawax.biz/jp/packages/laravel-bluesky/bluesky-manager Bluesky Facade の実体 BlueskyManager のアーキテクチャと、HasShortHand トレイトが提供するショートカットメソッドを解説します。 ## 概要 `Bluesky` Facade は `BlueskyManager` の薄いラッパーです。`BlueskyManager` は複数の Agent を管理し、`HasShortHand` トレイトが頻繁に使うメソッドのショートカットをまとめて提供します。 ## アーキテクチャ ```mermaid theme={null} graph TD A["Bluesky Facade
(Revolution\\Bluesky\\Facades\\Bluesky)"] --> B["BlueskyManager
(Factory インターフェース)"] B --> C["HasShortHand トレイト
ショートカットメソッド群"] B --> D["Conditionable トレイト
when() / unless()"] B --> E["Macroable トレイト
macro() / mixin()"] B --> F{"Agent"} F --> G["LegacyAgent
(App Password)"] F --> H["OAuthAgent
(OAuth)"] G --> I["LegacySession"] H --> J["OAuthSession"] ``` `BlueskyServiceProvider` は `Factory::class` を `BlueskyManager` としてスコープ付きシングルトン(1リクエストにつき1インスタンス)で登録します。 ```php theme={null} // BlueskyServiceProvider::register() $this->app->scoped(Factory::class, BlueskyManager::class); ``` Facade の `getFacadeAccessor()` がこのバインディングを解決します。 ```php theme={null} // Facades/Bluesky.php protected static function getFacadeAccessor(): string { return Factory::class; } ``` ## BlueskyManager のコアメソッド `BlueskyManager` 自身が定義するコアメソッドは以下のとおりです。`HasShortHand` トレイトのメソッドとは区別して把握しておくと、内部構造の理解に役立ちます。 ### 認証 | メソッド | 説明 | | - | - | | `login(string $identifier, string $password, ?string $service = null)` | App Password で認証し `LegacyAgent` をセット | | `withToken(?AbstractSession $token)` | `OAuthSession` または `LegacySession` を渡して Agent をセット | | `check(): bool` | 認証済みかどうかを確認(トークン有効期限も検証) | | `refreshSession()` | トークンをリフレッシュ | | `logout()` | Agent をクリア | ### Agent 操作 | メソッド | 説明 | | - | - | | `agent(): ?Agent` | 現在の Agent を取得 | | `withAgent(?Agent $agent)` | Agent を直接セット | | `assertDid(): string` | 認証済み DID を返す。未認証なら例外をスロー | ### HTTP クライアント | メソッド | 説明 | | - | - | | `client(bool $auth = true): AtpClient` | 認証済み(または匿名の)XRPC クライアントを返す | | `public(): BskyClient` | 認証不要のパブリックエンドポイントクライアントを返す | | `send(BackedEnum\|string $api, string $method, bool $auth, ?array $params, ?callable $callback)` | 任意の AT Protocol API を直接呼び出す | ### ユーティリティ | メソッド | 説明 | | - | - | | `identity(): Identity` | DID 解決などのアイデンティティサービスを取得 | | `pds(): PDS` | PDS 情報サービスを取得 | | `entryway(?string $path): string` | サービス URL(`bsky.social` など)を返す | | `publicEndpoint(): string` | パブリックエンドポイント URL を返す | ## HasShortHand トレイト `HasShortHand` トレイトは、AT Protocol の低レベル API を PHP 向けのわかりやすいメソッドにラップしています。`BlueskyManager` に `use HasShortHand;` と記述するだけで、これらのメソッドが `Bluesky::` から直接呼べるようになります。 ```php theme={null} // BlueskyManager.php class BlueskyManager implements Factory { use Conditionable; use HasShortHand; use Macroable; // ... } ``` `HasShortHand` を独立したトレイトにしている理由は、テスト・カスタマイズの容易さです。このトレイトを使えば `BlueskyManager` のコードをシンプルに保ちながら、豊富な API ショートカットを提供できます。 ### 投稿・フィード | メソッド | 説明 | | - | - | | `post(Post\|string\|array $text)` | 新規投稿を作成 | | `getPost(string $uri)` | AT-URI で投稿を取得 | | `getPosts(array $uris)` | 複数の投稿を一括取得 | | `deletePost(string $uri)` | 投稿を削除 | | `getTimeline(?string $algorithm, ?int $limit, ?string $cursor)` | ホームタイムラインを取得 | | `getAuthorFeed(?string $actor, ...)` | 指定アカウントのフィードを取得 | | `searchPosts(string $q, ...)` | 投稿を検索 | ### エンゲージメント | メソッド | 説明 | | - | - | | `like(Like\|StrongRef $subject)` | いいねを作成 | | `deleteLike(string $uri)` | いいねを取り消し | | `repost(Repost\|StrongRef $subject)` | リポストを作成 | | `deleteRepost(string $uri)` | リポストを取り消し | | `getActorLikes(?string $actor, ...)` | アカウントのいいね一覧を取得 | ### フォロー | メソッド | 説明 | | - | - | | `follow(Follow\|string $did)` | フォロー | | `deleteFollow(string $uri)` | フォロー解除 | | `getFollowers(?string $actor, ...)` | フォロワー一覧を取得 | | `getFollows(?string $actor, ...)` | フォロー中一覧を取得 | ### プロフィール・アカウント | メソッド | 説明 | | - | - | | `getProfile(?string $actor)` | プロフィールを取得 | | `upsertProfile(callable $callback)` | プロフィールを更新 | | `resolveHandle(string $handle)` | ハンドルを DID に解決 | ### メディア | メソッド | 説明 | | - | - | | `uploadBlob(StreamInterface\|string $data, string $type)` | 画像などのブロブをアップロード | | `uploadVideo(StreamInterface\|string $data, string $type)` | 動画をアップロード | | `getJobStatus(string $jobId)` | 動画アップロードのジョブ状態を確認 | | `getUploadLimits()` | アップロード制限を取得 | ### 通知 | メソッド | 説明 | | - | - | | `listNotifications(...)` | 通知一覧を取得 | | `countUnreadNotifications(...)` | 未読通知件数を取得 | | `updateSeenNotifications(string $seenAt)` | 既読を更新 | ### AT Protocol レコード操作 | メソッド | 説明 | | - | - | | `createRecord(string $repo, string $collection, ...)` | レコードを作成 | | `getRecord(string $repo, string $collection, string $rkey, ...)` | レコードを取得 | | `listRecords(string $repo, string $collection, ...)` | レコード一覧を取得 | | `putRecord(string $repo, string $collection, string $rkey, ...)` | レコードを更新・作成 | | `deleteRecord(string $repo, string $collection, string $rkey, ...)` | レコードを削除 | ### フィードジェネレーター・ラベラー | メソッド | 説明 | | - | - | | `publishFeedGenerator(BackedEnum\|string $name, Generator $generator)` | フィードジェネレーターを公開 | | `createThreadGate(string $post, ?array $allow)` | スレッドゲートを作成 | | `upsertLabelDefinitions(callable $callback)` | ラベル定義を更新 | | `deleteLabelDefinitions()` | ラベル定義を削除 | | `createLabels(RepoRef\|StrongRef\|array $subject, array $labels)` | ラベルを付与 | | `deleteLabels(RepoRef\|StrongRef\|array $subject, array $labels)` | ラベルを削除 | ## よく使うショートカットの例 ### 投稿する ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; $response = Bluesky::login( identifier: config('bluesky.identifier'), password: config('bluesky.password'), )->post('Hello Bluesky'); ``` ### リプライする リプライは `Post::build()` で親投稿の `StrongRef` を設定します。`HasShortHand` に専用の `reply()` メソッドはなく、`post()` に `Post` オブジェクトを渡します。 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Record\Post; use Revolution\Bluesky\Types\StrongRef; $parent = StrongRef::to(uri: 'at://did:plc:.../app.bsky.feed.post/...', cid: 'bafyrei...'); $reply = Post::create('リプライのテキスト') ->reply(root: $parent, parent: $parent); Bluesky::login(config('bluesky.identifier'), config('bluesky.password')) ->post($reply); ``` ### いいね・リポスト ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Types\StrongRef; $ref = StrongRef::to(uri: 'at://did:plc:.../app.bsky.feed.post/...', cid: 'bafyrei...'); // いいね Bluesky::withToken($session)->like($ref); // リポスト Bluesky::withToken($session)->repost($ref); ``` ### プロフィールを更新する `upsertProfile()` は現在のプロフィールを取得し、クロージャ内で変更した内容を保存します。`Profile` オブジェクトのメソッドを使って displayName・description などを設定できます。 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Record\Profile; Bluesky::login(config('bluesky.identifier'), config('bluesky.password')) ->upsertProfile(function (Profile $profile) { $profile->displayName('新しい表示名') ->description('プロフィール説明文'); }); ``` #### セルフラベルを設定する `SelfLabels` を使って、アカウント全体に対してセルフラベルを設定できます。 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Record\Profile; use Revolution\Bluesky\Types\SelfLabels; Bluesky::login(config('bluesky.identifier'), config('bluesky.password')) ->upsertProfile(function (Profile $profile) { $profile->labels(SelfLabels::make(['!no-unauthenticated'])); }); ``` ### 任意の API を直接呼び出す `HasShortHand` にないメソッドは `send()` または `client()` を使います。 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; // send() で任意の XRPC メソッドを呼び出す $response = Bluesky::withToken($session) ->send( api: 'app.bsky.actor.getProfiles', method: 'get', params: ['actors' => ['did:plc:...', 'did:plc:...']], ); // client() でより細かい操作 $response = Bluesky::withToken($session) ->client() ->bsky() ->getProfiles(actors: ['did:plc:...']); ``` ## Facade 経由と直接利用の違い `Bluesky::post()` と `app(Factory::class)->post()` は同じ `BlueskyManager` インスタンスに対する操作です。 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Contracts\Factory; // Facade 経由(通常の使い方) Bluesky::login(config('bluesky.identifier'), config('bluesky.password')) ->post('Hello'); // コンテナから直接取得 $manager = app(Factory::class); $manager->login(config('bluesky.identifier'), config('bluesky.password')) ->post('Hello'); // 型ヒントで DI class MyService { public function __construct(private Factory $bluesky) {} public function doPost(): void { $this->bluesky->login( config('bluesky.identifier'), config('bluesky.password'), )->post('Hello from DI'); } } ``` `scoped` 登録のため、1リクエスト内では `login()` 後のセッション状態が保持されます。 ## Conditionable と Macroable `BlueskyManager` は `Conditionable` と `Macroable` トレイトも使用しています。 ### Conditionable: when() / unless() ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; Bluesky::login(config('bluesky.identifier'), config('bluesky.password')) ->when(config('app.env') === 'production', function ($bluesky) { $bluesky->post('本番環境からの投稿'); }); ``` ### Macroable: カスタムメソッドの追加 `macro()` を使うとアプリケーション側から `BlueskyManager` にメソッドを追加できます。`AppServiceProvider` の `boot()` 内で定義するのが一般的です。 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; Bluesky::macro('postWithHashtag', function (string $text, string $tag) { /** @var \Revolution\Bluesky\BlueskyManager $this */ return $this->post("{$text} #{$tag}"); }); // 使用例 Bluesky::login(config('bluesky.identifier'), config('bluesky.password')) ->postWithHashtag('Hello', 'laravel'); ``` ## カスタム Agent の差し込み `withAgent()` を使うと、任意の `Agent` 実装を直接セットできます。 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Contracts\Agent; // カスタム Agent を実装する場合は Agent インターフェースを実装する class MyCustomAgent implements Agent { // ... } Bluesky::withAgent(new MyCustomAgent()); ``` 通常の認証(`login()` / `withToken()`)を使えば自動でエージェントがセットされるため、`withAgent()` を直接使う機会はテスト時が主となります。 ## 参考リンク * [認証方法の比較](/jp/packages/laravel-bluesky/authentication) — App Password と OAuth の詳細 * [Basic client](/jp/packages/laravel-bluesky/basic-client) — 認証後の API 操作例 * [テスト](/jp/packages/laravel-bluesky/testing) — Fake を使ったテスト方法 * Source: [src/BlueskyManager.php](https://github.com/invokable/laravel-bluesky/blob/main/src/BlueskyManager.php) * Source: [src/HasShortHand.php](https://github.com/invokable/laravel-bluesky/blob/main/src/HasShortHand.php) # ボットチュートリアル - Laravel Bluesky Source: https://kawax.biz/jp/packages/laravel-bluesky/bot-tutorial AT Protocol 公式ボットチュートリアルの Laravel 版。laravel-bluesky パッケージを使って投稿ボット・リプライボット・ラベルボットを実装します。 ## 概要 このページは [AT Protocol 公式ボットチュートリアル](https://atproto.com/ja/guides/bot-tutorial) の Laravel 版です。TypeScript 向けの内容を `laravel-bluesky` パッケージを使って PHP/Laravel で実装する方法を解説します。 公式チュートリアルでは `lex` コマンドで Lexicon ファイルを都度ダウンロードしますが、`laravel-bluesky` では [atproto-lexicon-contracts](https://github.com/invokable/atproto-lexicon-contracts) から事前に全 Lexicon を取り込み済みのため、この手順は不要です。Artisan コマンドと `HasShortHand` トレイトのメソッドを使って、ほぼすべての操作が実現できます。 ```mermaid theme={null} graph LR A["公式チュートリアル
(TypeScript)"] -->|"Laravel版"| B["laravel-bluesky"] B --> C["Artisanコマンド"] B --> D["HasShortHandトレイト"] B --> E["タスクスケジュール
/ GitHub Actions"] ``` ## 前提条件 * `laravel-bluesky` パッケージがインストール済みであること * Bluesky のアカウントとアプリパスワードが用意されていること インストール方法は [Laravel Bluesky](/jp/packages/laravel-bluesky/index) を参照してください。 ```dotenv theme={null} BLUESKY_IDENTIFIER=yourbot.bsky.social BLUESKY_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx ``` *** ## パート 1: 基本ボット(投稿) ### Artisan コマンドを作成する `php artisan make:command` でボット用のコマンドを作成します。 ```bash theme={null} php artisan make:command BotPostCommand ``` 生成された `app/Console/Commands/BotPostCommand.php` を編集します。 ```php theme={null} post('🙂'); $this->info('Posted successfully.'); } } ``` ### 手動実行 ```bash theme={null} php artisan bot:post ``` ### タスクスケジュールで自動実行 `routes/console.php` に定期実行の設定を追加します。 ```php theme={null} use Illuminate\Support\Facades\Schedule; // 3時間ごとに投稿 Schedule::command('bot:post')->everyThreeHours(); ``` スケジューラを有効にするには、cron に以下を設定してください。 ```bash theme={null} * * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1 ``` ### GitHub Actions で自動実行 サーバーを持たない場合は GitHub Actions でも自動実行できます。 ```yaml theme={null} # .github/workflows/bot.yml name: Bot Post on: schedule: - cron: '0 */3 * * *' # 3時間ごと workflow_dispatch: jobs: post: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: '8.4' - run: composer install --no-dev --optimize-autoloader - run: php artisan bot:post env: BLUESKY_IDENTIFIER: ${{ secrets.BLUESKY_IDENTIFIER }} BLUESKY_APP_PASSWORD: ${{ secrets.BLUESKY_APP_PASSWORD }} ``` GitHub Actions のシークレットに `BLUESKY_IDENTIFIER` と `BLUESKY_APP_PASSWORD` を登録してください。 *** ## パート 2: リプライボット(メンション監視) メンションに自動返信するボットを作成します。AI を使ってリプライを生成する例も紹介します。 ### 通知を取得してメンションに返信する ```bash theme={null} php artisan make:command BotReplyCommand ``` ```php theme={null} json('notifications', []); foreach ($notifications as $notification) { // メンション通知のみ処理 if (data_get($notification, 'reason') !== 'mention') { continue; } // 既読の通知はスキップ(isRead が true のものは処理済み) if (data_get($notification, 'isRead')) { continue; } $uri = data_get($notification, 'uri'); $cid = data_get($notification, 'cid'); if (! $uri || ! $cid) { continue; } $ref = StrongRef::to(uri: $uri, cid: $cid); $reply = ReplyRef::to(root: $ref, parent: $ref); $post = Post::create('こんにちは!メンションありがとうございます。🙂') ->reply($reply); Bluesky::post($post); $this->info("Replied to: {$uri}"); } // 通知を既読にする Bluesky::updateSeenNotifications(now()->toISOString()); } } ``` スレッドの root が異なる場合は `app.bsky.feed.getPostThread` でスレッド情報を取得して `root` に設定してください。簡易実装では親投稿を root として扱っています。 ### ポーリングをスケジュールする ```php theme={null} // routes/console.php use Illuminate\Support\Facades\Schedule; Schedule::command('bot:reply')->everyFiveMinutes(); ``` ### AI を使ってリプライを生成する `laravel/ai` パッケージと `laravel-amazon-bedrock` ドライバーを組み合わせると、メンション内容に応じた AI リプライを生成できます。 ```bash theme={null} composer require laravel/ai revolution/laravel-amazon-bedrock ``` AI エージェントを作成します。 ```php theme={null} json('notifications', []); foreach ($notifications as $notification) { if (data_get($notification, 'reason') !== 'mention') { continue; } if (data_get($notification, 'isRead')) { continue; } $uri = data_get($notification, 'uri'); $cid = data_get($notification, 'cid'); $mentionText = data_get($notification, 'record.text', ''); if (! $uri || ! $cid) { continue; } // AI でリプライを生成 $replyText = (new BotReplyAgent)->prompt($mentionText)->text; $ref = StrongRef::to(uri: $uri, cid: $cid); $reply = ReplyRef::to(root: $ref, parent: $ref); $post = Post::create($replyText)->reply($reply); Bluesky::post($post); $this->info("AI replied to: {$uri}"); } Bluesky::updateSeenNotifications(now()->toISOString()); } } ``` `laravel-amazon-bedrock` の設定方法は [Amazon Bedrock ドライバー](/jp/packages/laravel-amazon-bedrock) を参照してください。 *** ## パート 3: ラベルボット 公式チュートリアルの `labelAsBot` に相当する機能です。ボットアカウントのプロフィールにセルフラベルを付与して、自動化アカウントであることを明示します。 ラベル付けは初回セットアップ時に一度だけ実行します。パート 1・2 のコマンドとは独立した別のコマンドとして実装します。 ### ラベルボットコマンドを作成する ```bash theme={null} php artisan make:command BotLabelCommand ``` ```php theme={null} upsertProfile(function (Profile $profile) { // !bot ラベルを設定する $profile->labels(SelfLabels::make(['!bot'])); }); $this->info('Bot label applied successfully.'); } } ``` ### 実行する ```bash theme={null} php artisan bot:label ``` このコマンドはボットの初回セットアップ時に一度だけ実行してください。ラベルはプロフィールに永続的に保存されます。 `!bot` は Bluesky の標準セルフラベルです。ボットアカウントには必ず設定することが推奨されています。 *** ## 他の公式チュートリアルについて AT Protocol の公式チュートリアルには他にもいくつかのトピックがあります。`laravel-bluesky` での対応状況をまとめます。 ### カスタムフィード [Feed Generator](/jp/packages/laravel-bluesky/feed-generator) で Laravel を使ったカスタムフィードジェネレーターの実装方法を詳しく解説しています。 ### OAuth 認証 [Socialite](/jp/packages/laravel-bluesky/socialite) で Laravel Socialite を使った OAuth 認証の実装方法を解説しています。公式チュートリアルより簡単に実装できます。 ### ソーシャルアプリ(statusphere) Laravel 版の statusphere が [invokable/statusphere](https://github.com/invokable/statusphere) として公開されています。`lex` や `tap` コマンドを使っていない以前の statusphere をベースにしているため、最新の公式チュートリアルとは若干異なる点があります。 ## 参考リンク * [AT Protocol 公式ボットチュートリアル(英語)](https://atproto.com/guides/bot-tutorial) * [AT Protocol 公式ボットチュートリアル(日本語)](https://atproto.com/ja/guides/bot-tutorial) * [laravel-bluesky](https://github.com/invokable/laravel-bluesky) * [BlueskyManager と HasShortHand](/jp/packages/laravel-bluesky/bluesky-manager) * [通知チャンネル](/jp/packages/laravel-bluesky/notification) * [Amazon Bedrock ドライバー](/jp/packages/laravel-amazon-bedrock) # Core — AT Protocol コア操作 Source: https://kawax.biz/jp/packages/laravel-bluesky/core Laravel Bluesky の Core モジュール。CBOR エンコーディング、CID による内容検証、CAR ファイル操作、TID による時系列レコードキーの生成を解説します。 Core は高度な内部実装です。通常の投稿・フィード取得・通知などには必要ありません。AT Protocol のデータ構造を直接操作する場合に参照してください。 ## AT Protocol のデータモデル概要 AT Protocol のリポジトリはコンテンツアドレス型のデータ構造を採用しています。データの保存・転送・検証に以下の要素が使われます。 ```mermaid theme={null} graph TD A["PHP データ (配列)"] --> B["CBOR エンコード
(DAG-CBOR)"] B --> C["SHA-256 ハッシュ"] C --> D["CID
(Content Identifier)"] D --> E["CAR ファイル
(ブロックの集合)"] E --> F["リポジトリ
(Merkle Search Tree)"] ``` | クラス | 役割 | | - | - | | `CBOR` | バイナリシリアライゼーション | | `CID` | コンテンツアドレス (ハッシュ識別子) | | `CAR` | リポジトリアーカイブの読み書き | | `TID` | 時系列レコードキー | | `Varint` | 可変長整数エンコーディング (内部) | *** ## CBOR `CBOR` クラスは AT Protocol が採用する [DAG-CBOR](https://ipld.io/specs/codecs/dag-cbor/spec/) 形式のエンコード・デコードを提供します。標準 CBOR の拡張として CID リンク(タグ 42)をサポートしています。 ### エンコード ```php theme={null} use Revolution\Bluesky\Core\CBOR; $record = [ '$type' => 'app.bsky.feed.post', 'text' => 'Hello, Bluesky!', 'createdAt' => '2025-01-01T00:00:00.000Z', ]; $bytes = CBOR::encode($record); // バイナリ文字列 ``` ### デコード ```php theme={null} use Revolution\Bluesky\Core\CBOR; // 単一アイテムをデコード $data = CBOR::decode($bytes); // ストリームの先頭アイテムをデコードして残りを返す [$item, $remainder] = CBOR::decodeFirst($bytes); // ストリームの全アイテムをデコード $items = CBOR::decodeAll($bytes); ``` ### 用途 投稿の CID を手動で計算・検証したい場合は CBOR エンコードが必要です([verify ページ](/jp/packages/laravel-bluesky/verify)も参照)。 ```php theme={null} use Revolution\Bluesky\Core\CBOR; use Revolution\Bluesky\Core\CID; $record = data_get($block, 'value'); $cbor = CBOR::encode($record); $bool = CID::verify($cbor, data_get($block, 'cid')); ``` *** ## CID (Content Identifier) CID はデータのハッシュを元にした自己記述型の識別子です。AT Protocol では SHA-256 ハッシュを multihash でラップし、さらに multicodec と multibase でエンコードします。 ```mermaid theme={null} graph LR A["データ (CBOR/Raw)"] --> B["SHA-256"] B --> C["multihash"] C --> D["multicodec
(dag-cbor / raw)"] D --> E["CIDv1
(base32 / base58btc)"] ``` ### CIDv0 と CIDv1 | バージョン | エンコーディング | 先頭文字 | 用途 | | - | - | - | - | | v0 | base58btc | `Qm...` | 古い仕様 (ブロブ) | | v1 | base32 | `bafy...` | 新しい仕様 (レコード) | ### 主要 API ```php theme={null} use Revolution\Bluesky\Core\CID; // データから CID を生成 $cid = CID::encode($data, CID::DAG_CBOR); // レコード用 $cid = CID::encode($data, CID::RAW); // バイナリ (画像など) // CID を検証 $bool = CID::verify($data, $cid, CID::DAG_CBOR); $bool = CID::verify($file, $cid, CID::RAW); // CID を解析 $decoded = CID::decode($cid); // ['version', 'codec', 'hash'] // CID のバージョンを確認 $version = CID::version($cid); // 0 or 1 // バイト表現への変換 $bytes = CID::decodeBytes($cid); $cid = CID::encodeBytes($bytes); ``` *** ## CAR (Content Addressable aRchive) CAR ファイルは AT Protocol リポジトリをブロックの列として格納するバイナリフォーマットです。`com.atproto.sync.getRepo` で取得したデータがこの形式です。 ```mermaid theme={null} graph TD A["CAR ファイル"] --> B["ヘッダー
(ルート CID)"] A --> C["ブロック列"] C --> D["Signed Commit
(署名 + prev CID)"] C --> E["MST ノード"] C --> F["レコード (CBOR)"] ``` ### デコード ```php theme={null} use Revolution\Bluesky\Core\CAR; // CAR ファイル全体をデコード (ルートと全ブロック) ['roots' => $roots, 'blocks' => $blocks] = CAR::decode($carData); // ルート CID のみ取得 $roots = CAR::decodeRoots($carData); // ブロックをイテレート foreach (CAR::blockIterator($carData) as [$cid, $block]) { // $cid: CID 文字列, $block: バイナリデータ } // レコードマップとして取得 foreach (CAR::blockMap($carData) as $key => $record) { // $key: レコードキー, $record: デコード済み配列 } ``` ### 署名付き Commit の検証 CAR ファイルがそのユーザーのものか確認するには、Signed Commit の署名を DID Document の公開鍵で検証します。 ```php theme={null} use Revolution\Bluesky\Core\CAR; use Revolution\Bluesky\Crypto\DidKey; use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Support\DidDocument; $did = 'did:plc:***'; $didDoc = DidDocument::make(Bluesky::identity()->resolveDID($did)->json()); $publicKey = DidKey::parse($didDoc->publicKey()); $signed = CAR::signedCommit($carData); $bool = CAR::verifySignedCommit($signed, $publicKey); ``` サンプル実装: [DownloadRepoCommand](https://github.com/invokable/laravel-bluesky/blob/main/src/Console/DownloadRepoCommand.php) *** ## TID (Timestamp Identifier) TID は AT Protocol のレコードキーに使われる時系列 ID です。マイクロ秒単位のタイムスタンプとクロック ID を組み合わせ、base32 でエンコードした 13 文字の文字列です。 ``` 例: 3jujm55ngfc24 ``` ### 生成・変換 ```php theme={null} use Revolution\Bluesky\Core\TID; // 次の TID を生成 $tid = TID::next(); echo $tid->toString(); // "3jujm55ngfc24" のような文字列 // 文字列から TID を作成 $tid = TID::fromStr('3jujm55ngfc24'); // タイムスタンプ (マイクロ秒) と クロック ID から作成 $tid = TID::fromTime(microtime(true) * 1000000, 0); ``` ### TID の構造 ```mermaid theme={null} graph LR A["63 ビット整数"] --> B["53 ビット: タイムスタンプ
(マイクロ秒)"] A --> C["10 ビット: クロック ID"] B --> D["base32 エンコード
(13 文字)"] C --> D ``` `TID::s32encode()` と `TID::s32decode()` で整数と文字列の相互変換ができます。 *** ## Varint (可変長整数) `Varint` は CAR / CBOR のバイナリ解析で使用される内部ユーティリティです。通常は直接使用しません。 ```php theme={null} use Revolution\Bluesky\Core\Varint; // 整数をエンコード $bytes = Varint::encode(1234); // ストリームからデコード (値と消費バイト数を返す) [$value, $bytesRead] = Varint::decodeStream($stream); ``` *** ## Core 機能のモックについて Core 機能 (CBOR/CID/CAR/TID) は外部アクセスを行わないため、テスト時にモックする必要はありません。 ```php theme={null} // テストでも直接使用できる $bytes = CBOR::encode(['text' => 'Hello']); $cid = CID::encode($bytes, CID::DAG_CBOR); $bool = CID::verify($bytes, $cid); ``` *** ## 参考リンク * [AT Protocol: Repository spec](https://atproto.com/specs/repository) * [AT Protocol: CID spec](https://atproto.com/specs/data-model#cid-formats) * [IPLD: DAG-CBOR spec](https://ipld.io/specs/codecs/dag-cbor/spec/) Source: [src/Core/](https://github.com/invokable/laravel-bluesky/tree/main/src/Core) # Crypto — AT Protocol 暗号化 Source: https://kawax.biz/jp/packages/laravel-bluesky/crypto Laravel Bluesky パッケージの Crypto モジュール。P-256 / secp256k1 の鍵ペア、DID キー、JWT、DPoP、署名変換を解説します。 Crypto は高度な内部実装です。通常の投稿・フィード取得・通知などには必要ありません。AT Protocol の低レベルな署名検証や OAuth 実装を構築する場合に参照してください。 ## AT Protocol における暗号化の概要 AT Protocol は分散型ソーシャルネットワークを実現するために、楕円曲線暗号(ECC)を全面的に採用しています。主な用途は次の通りです。 ```mermaid theme={null} graph TD A["鍵ペア
(P-256 / secp256k1)"] --> B["秘密鍵で署名"] A --> C["公開鍵を did:key にエンコード"] B --> D["DPoP トークン (OAuth)"] B --> E["Commit 署名 (リポジトリ)"] B --> F["ラベル署名 (Labeler)"] C --> G["DID Document に登録"] G --> H["公開鍵で署名を検証"] ``` | 曲線 | アルゴリズム | 主な用途 | | - | - | - | | secp256r1 (P-256) | ES256 | OAuth (DPoP、Client Assertion) | | secp256k1 (K256) | ES256K | Feed Generator、Labeler の署名検証 | *** ## 鍵ペアクラス ### AbstractKeypair `AbstractKeypair` は P256 / K256 の共通基底クラスです。内部では [phpseclib3](https://phpseclib.com/) を使用しています。 ```php theme={null} // 新しい鍵ペアを生成 $keypair = P256::create(); $keypair = K256::create(); // URL-safe Base64 エンコードの秘密鍵から読み込む $keypair = P256::load($base64PrivateKey); // PEM 形式で取得 $privatePem = $keypair->privatePEM(); $publicPem = $keypair->publicPEM(); // JWK (JSON Web Key) に変換 $jwk = $keypair->toJWK(); ``` ### P256 (secp256r1) ```php theme={null} use Revolution\Bluesky\Crypto\P256; $keypair = P256::create(); ``` OAuth (DPoP / Client Assertion) で使用されます。`OAuthKey` は P256 を継承しており、`config('bluesky.oauth.private_key')` から秘密鍵を読み込みます。 新しい OAuth 秘密鍵を生成する Artisan コマンドも用意されています。 ```bash theme={null} php artisan bluesky:new-private-key ``` ### K256 (secp256k1) ```php theme={null} use Revolution\Bluesky\Crypto\K256; $keypair = K256::create(); ``` Feed Generator や Labeler の認証で使用されます。Bluesky の PDS / Relay がこの曲線で署名を検証します。 *** ## DidKey `DidKey` クラスは公開鍵を `did:key` 形式にエンコード・デコードします。 ### did:key 形式とは AT Protocol では公開鍵を `did:key:z...` という文字列で表現します。これは公開鍵に曲線識別子を付加して Base58btc でマルチベースエンコードしたものです。 ```mermaid theme={null} graph LR A["公開鍵 (PEM)"] --> B["曲線識別子 + 圧縮公開鍵"] B --> C["Base58btc エンコード"] C --> D["did:key:z..."] ``` ### 公開鍵を did:key にエンコード ```php theme={null} use Revolution\Bluesky\Crypto\DidKey; use Revolution\Bluesky\Crypto\K256; $keypair = K256::create(); $publicPem = $keypair->publicPEM(); // did:key 形式に変換 $didKey = DidKey::format($publicPem); // => "did:key:zQ3s..." ``` ### DID Document から公開鍵をパース ```php theme={null} use Revolution\Bluesky\Crypto\DidKey; use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Support\DidDocument; $didDoc = DidDocument::make( Bluesky::identity()->resolveDID('did:plc:***')->json() ); // DID Document の publicKey (multibase) から phpseclib3 の公開鍵オブジェクトを生成 $publicKey = DidKey::parse($didDoc->publicKey()); ``` *** ## JsonWebToken (JWT) `JsonWebToken` クラスは JWT のエンコード・デコードを提供します。 ```php theme={null} use Revolution\Bluesky\Crypto\JsonWebToken; // JWT を生成 (秘密鍵 PEM で署名) $token = JsonWebToken::encode( payload: ['iss' => 'did:plc:***', 'aud' => 'https://bsky.social', 'exp' => time() + 60], key: $privatePem, alg: 'ES256', ); // JWT をデコード (署名検証なし) $payload = JsonWebToken::decode($token); ``` *** ## DPoP (Demonstrated Proof of Possession) DPoP は OAuth アクセストークンを特定のクライアント鍵ペアに紐付けるセキュリティ機構です。トークンのリプレイ攻撃を防ぎます。 ```mermaid theme={null} sequenceDiagram participant App as Laravel アプリ participant AS as Authorization Server participant RS as Resource Server App->>AS: PAR リクエスト (+ DPoP ヘッダー) AS-->>App: request_uri App->>AS: トークンリクエスト (+ DPoP ヘッダー) AS-->>App: アクセストークン (DPoP bound) App->>RS: API リクエスト (+ DPoP ヘッダー + ath) RS-->>App: レスポンス ``` `DPoP` クラスは内部で使用されており、通常は直接呼び出す必要はありません。`OAuthAgent` ミドルウェアが自動的に DPoP ヘッダーを付与します。 ```php theme={null} // DPoP プルーフの生成 (OAuth トークンリクエスト用) $proof = DPoP::authProof( jwk: $jsonWebKey, url: 'https://bsky.social/oauth/token', method: 'POST', nonce: $nonce, ); // DPoP プルーフの生成 (API リクエスト用、ath を含む) $proof = DPoP::apiProof( jwk: $jsonWebKey, url: 'https://api.bsky.app/xrpc/...', method: 'GET', token: $accessToken, nonce: $nonce, ); ``` *** ## Signature (署名フォーマット変換) AT Protocol は 64 バイトのコンパクト署名形式を使用しますが、phpseclib3 は ASN.1 DER 形式を返します。`Signature` クラスはこの変換を担います。 ```php theme={null} use Revolution\Bluesky\Crypto\Signature; // ASN.1 DER → コンパクト (64バイト) $compact = Signature::toCompact($derSignature); // コンパクト → ASN.1 DER $der = Signature::fromCompact($compactSignature); ``` *** ## JsonWebKey `JsonWebKey` は JWK (JSON Web Key) を表すクラスです。DPoP プルーフの生成に使用されます。 ```php theme={null} use Revolution\Bluesky\Crypto\JsonWebKey; // 鍵ペアから JWK を生成 $jwk = $keypair->toJWK(); // または直接インスタンス化 $jwk = JsonWebKey::load(['kty' => 'EC', 'crv' => 'P-256', ...]); ``` *** ## OAuthKey `OAuthKey` は P256 を継承した OAuth 専用の鍵クラスで、`config('bluesky.oauth.private_key')` の値を秘密鍵として使用します。 ```php theme={null} use Revolution\Bluesky\Crypto\OAuthKey; // 設定から秘密鍵を読み込む $oauthKey = OAuthKey::load(); // Client Assertion JWT の署名に使用される $jwk = $oauthKey->toJWK(); ``` 通常は Bluesky Socialite 内部で自動的に処理されます。 *** ## 参考リンク * [AT Protocol: Identity](https://atproto.com/guides/identity) * [AT Protocol: Cryptography spec](https://atproto.com/specs/cryptography) * [phpseclib3](https://phpseclib.com/) Source: [src/Crypto/](https://github.com/invokable/laravel-bluesky/tree/main/src/Crypto) # Feed Generator Source: https://kawax.biz/jp/packages/laravel-bluesky/feed-generator Laravel Bluesky で独自のアルゴリズムフィードを作成・公開する方法。登録、コマンド作成、複数フィード、クラス分離、認証、高度な使い方を解説。 ## 概要 Feed Generator は Bluesky 上で動作する「アルゴリズム的フィード」の仕組みです。特定のキーワードやユーザー条件に合わせた独自フィードを公開できます。`laravel-bluesky` を使うと、Laravel アプリケーション上に Feed Generator を簡単に実装できます。 公式チュートリアル: [カスタムフィード作成](https://atproto.com/ja/guides/custom-feed-tutorial) 公式スターターキット: [bluesky-social/feed-generator](https://github.com/bluesky-social/feed-generator) ```mermaid theme={null} sequenceDiagram participant Bluesky as Bluesky
サーバー participant App as Laravel
アプリ participant DB as データベース Bluesky->>App: GET /xrpc/app.bsky.feed.getFeedSkeleton App->>DB: フィードデータ取得 DB-->>App: posts App-->>Bluesky: { cursor, feed } ``` ## FeedGenerator アルゴリズムの登録 最もシンプルな使い方は `AppServiceProvider::boot()` でアルゴリズムをクロージャとして登録することです。 ```php theme={null} // AppServiceProvider::boot() で登録 use Illuminate\Http\Request; use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\FeedGenerator\FeedGenerator; FeedGenerator::register(name: 'artisan', algo: function(int $limit, ?string $cursor, ?string $user, Request $request): array { // 実装は自由に決めてください。 // API の一時的な制限により認証が必要 $response = Bluesky::login(identifier: config('bluesky.identifier'), password: config('bluesky.password')) ->searchPosts(q: '#laravel', until: $cursor, limit: $limit); $cursor = data_get($response->collect('posts')->last(), 'indexedAt'); $feed = $response->collect('posts')->map(function(array $post) { return ['post' => data_get($post, 'uri')]; })->toArray(); // Request オブジェクトを使ってユーザーの状態に応じた結果を返すこともできます。 info('user: '.$user); // リクエスト元ユーザーの DID。'did:plc:***' info('header', $request->header()); return compact('cursor', 'feed'); }); ``` `name` は URL セーフな文字列を使用してください。 アルゴリズムの戻り値は `cursor` と `feed` を含む配列です。 ```php theme={null} [ 'cursor' => '', 'feed' => [ ['post' => 'at://'], ['post' => 'at://'], ], ] ``` パッケージが必要なルートはすべて自動で登録されます。 * `http://localhost/xrpc/app.bsky.feed.getFeedSkeleton?feed=at://did:web:example.com/app.bsky.feed.generator/artisan` * `http://localhost/xrpc/app.bsky.feed.describeFeedGenerator` * `http://localhost/.well-known/did.json` * Service DID は現在の URL から自動生成されます(例: `did:web:example.com`)。 自分で決めるのは FeedGenerator の `name` と実装内容だけです。 ## フィードの公開(コマンド作成) Laravel アプリ上に FeedGenerator を実装しただけでは Bluesky には公開されません。`publishFeedGenerator` を呼び出すコマンドを作成して実行します。 ```bash theme={null} php artisan make:command PublishGeneratorCommand ``` ```php theme={null} namespace App\Console\Commands; use Illuminate\Console\Command; use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Record\Generator; class PublishGeneratorCommand extends Command { protected $signature = 'bluesky:publish-generator'; protected $description = 'Bluesky に FeedGenerator を公開する'; public function handle() { $generator = Generator::create(did: 'did:web:example.com', displayName: 'Feed name') ->description('Feed description'); $res = Bluesky::login(identifier: config('bluesky.identifier'), password: config('bluesky.password')) ->publishFeedGenerator(name: 'artisan', generator: $generator); dump($res->json()); return 0; } } ``` ```bash theme={null} php artisan bluesky:publish-generator ``` 成功すると Bluesky プロフィールのフィード一覧にリンクが追加されます。`publishFeedGenerator` は情報を更新するだけなので何度でも実行できます。 ## 複数 FeedGenerator の作成 `name` を変えて `register` を複数回呼び出すだけで複数のフィードを作成できます。 ```php theme={null} // AppServiceProvider::boot() use Revolution\Bluesky\FeedGenerator\FeedGenerator; FeedGenerator::register(name: 'feed1', algo: function() { // feed1 の実装 }); FeedGenerator::register(name: 'feed2', algo: function() { // feed2 の実装 }); ``` 公開コマンドでも同様に複数回 `publishFeedGenerator` を呼び出します。 ```php theme={null} // PublishGeneratorCommand Bluesky::login(identifier: config('bluesky.identifier'), password: config('bluesky.password')); $generator1 = Generator::create(did: 'did:web:example.com', displayName: 'Feed 1') ->description('Feed 1'); Bluesky::publishFeedGenerator(name: 'feed1', generator: $generator1); $generator2 = Generator::create(did: 'did:web:example.com', displayName: 'Feed 2') ->description('Feed 2'); Bluesky::publishFeedGenerator(name: 'feed2', generator: $generator2); ``` ## アルゴリズムクラスの分離 クロージャの代わりに独立したクラスを使うことで、コードを整理しやすくなります。`FeedGeneratorAlgorithm` コントラクトを実装した callable クラスを作成し、`AppServiceProvider` に登録します。 ```php theme={null} // 任意の場所に作成 namespace App\FeedGenerator; use Illuminate\Http\Request; use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Contracts\FeedGeneratorAlgorithm; class ArtisanFeed implements FeedGeneratorAlgorithm { public function __invoke(int $limit, ?string $cursor, ?string $user, Request $request): array { // API の一時的な制限により認証が必要 $response = Bluesky::login(identifier: config('bluesky.identifier'), password: config('bluesky.password')) ->searchPosts(q: '#laravel', until: $cursor, limit: $limit); $cursor = data_get($response->collect('posts')->last(), 'indexedAt'); $feed = $response->collect('posts')->map(function (array $post) { return ['post' => data_get($post, 'uri')]; })->toArray(); info('user: '.$user); info('header', $request->header()); return compact('cursor', 'feed'); } } ``` ```php theme={null} // AppServiceProvider::boot() use Revolution\Bluesky\FeedGenerator\FeedGenerator; use App\FeedGenerator\ArtisanFeed; FeedGenerator::register(name: 'artisan', algo: ArtisanFeed::class); ``` ## 認証 公式スターターキットの認証機能はデフォルトで有効になっています。無効化するには `validateAuthUsing` に単純にユーザー DID を返すクロージャを渡します。 ```php theme={null} // AppServiceProvider::boot() use Illuminate\Http\Request; use Revolution\Bluesky\Crypto\JsonWebToken; use Revolution\Bluesky\FeedGenerator\FeedGenerator; FeedGenerator::validateAuthUsing(function (?string $jwt, Request $request): ?string { [, $payload] = JsonWebToken::explode($jwt); return data_get($payload, 'iss'); }); ``` フィードがアカウントの「言語設定」に影響されます。FeedGenerator が投稿を取得しているにもかかわらず Bluesky 上でフィードが表示されない場合は、アカウントの言語設定を確認してください。 ## 高度な使い方 Artisan コマンドとタスクスケジュールを使って投稿をデータベースに保存し、アルゴリズムでは DB から取得するだけにすると、API 呼び出しなしで高速なフィードを実現できます。 ```php theme={null} // アルゴリズムで DB からフィードを返す例 FeedGenerator::register(name: 'cached-feed', algo: function(int $limit, ?string $cursor): array { $query = \App\Models\Post::query() ->orderByDesc('indexed_at') ->limit($limit); if ($cursor) { $query->where('indexed_at', '<', $cursor); } $posts = $query->get(); $cursor = $posts->last()?->indexed_at; $feed = $posts->map(fn ($post) => ['post' => $post->uri])->toArray(); return compact('cursor', 'feed'); }); ``` ```php theme={null} // スケジュールで定期的に投稿を収集する例(routes/console.php) use Illuminate\Support\Facades\Schedule; Schedule::command('bluesky:collect-posts')->everyFiveMinutes(); ``` Source: [docs/feed-generator.md](https://github.com/invokable/laravel-bluesky/blob/main/docs/feed-generator.md) # Identity Source: https://kawax.biz/jp/packages/laravel-bluesky/identity Bluesky の Handle と DID の相互解決方法。Bluesky::identity() を使った操作をまとめます。 ## 概要 Bluesky では Handle、DID などの相互解決が頻繁に必要になります。 * [https://docs.bsky.app/docs/advanced-guides/resolving-identities](https://docs.bsky.app/docs/advanced-guides/resolving-identities) * [https://atproto.com/guides/identity](https://atproto.com/guides/identity) ## Handle から DID を解決する ### API 経由 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; $did = Bluesky::resolveHandle('***.bsky.social')->json('did'); ``` ### DNS/well-known 経由 DNS TXT レコードまたは `/.well-known/atproto-did` を確認します。 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; $did = Bluesky::identity()->resolveHandle('alice.test'); ``` 2つは似ていますが動作が異なります。API 経由はネットワーク越しに Bluesky サービスへ問い合わせるのに対し、DNS/well-known 経由はカスタムドメインの Handle 解決に適しています。 ## DID から DID Document を取得する `resolveDID` は DID Document を返します。 [https://plc.directory/did:plc:ewvi7nxzyoun6zhxrhs64oiz](https://plc.directory/did:plc:ewvi7nxzyoun6zhxrhs64oiz) ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; $didDoc = Bluesky::identity()->resolveDID('did:plc:*** または did:web:***')->json(); ``` ## DID Document から Handle を取得する DID Document の `alsoKnownAs` フィールドに Handle が含まれています。 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Support\DidDocument; $didDoc = DidDocument::make(Bluesky::identity()->resolveDID('did:plc:*** または did:web:***')->json()); $handle = $didDoc->handle(); ``` ## resolveIdentity による統合解決 `resolveHandle` と `resolveDID` を組み合わせた `resolveIdentity` を使うと、DID または Handle のどちらからでも解決でき、DID Document を返します。 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Support\DidDocument; $didDoc = DidDocument::make(Bluesky::identity()->resolveIdentity('did または handle')->json()); $didDoc->id(); $didDoc->handle(); ``` ## 公開プロフィールの取得 公開プロフィールは DID または Handle のどちらからでも解決でき、レスポンスに DID と Handle の両方が含まれます。 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; $profile = Bluesky::getProfile('did または handle'); $profile->json('did'); $profile->json('handle'); ``` ## PDS URL / serviceEndpoint の取得 DID Document の `serviceEndpoint` は PDS の URL です。直接使用することは少ないですが、必要に応じて取得できます。 ``` https://***.***.host.bsky.network ``` ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Support\DidDocument; $didDoc = DidDocument::make(Bluesky::identity()->resolveIdentity('did または handle')->json()); $didDoc->pdsUrl(); ``` ## Authorization Server URL の取得 PDS を経由して Authorization Server の URL を解決します。 Bluesky に登録したアカウントの場合、通常は `https://bsky.social` になります。 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Support\DidDocument; $didDoc = DidDocument::make(Bluesky::identity()->resolveIdentity('did または handle')->json()); $pds = Bluesky::pds()->getProtectedResource($didDoc->pdsUrl()); $pds->authServer(); ``` Source: [docs/identity.md](https://github.com/invokable/laravel-bluesky/blob/main/docs/identity.md) # Laravel Bluesky Source: https://kawax.biz/jp/packages/laravel-bluesky/index revolution/laravel-bluesky の概要、要件、インストール、初期設定、最初の投稿までをまとめて確認します。 ## 概要 [revolution/laravel-bluesky](https://github.com/invokable/laravel-bluesky) は、AT Protocol(Bluesky)をLaravelから扱うためのパッケージです。 Facadeは `Revolution\Bluesky\Facades\Bluesky` です。投稿作成、フィード取得、検索、メディア投稿などをLaravelのHTTPクライアントに近い形で扱えます。 ## 要件 * PHP `>= 8.3` * Laravel `>= 12.x` ## インストール ```bash theme={null} composer require revolution/laravel-bluesky ``` ## 環境変数 App Password認証を使う場合は `.env` を設定します。 ```dotenv theme={null} BLUESKY_IDENTIFIER=your-handle.bsky.social BLUESKY_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx ``` ## クイックスタート ### 最初の投稿 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; $response = Bluesky::login( identifier: config('bluesky.identifier'), password: config('bluesky.password'), )->post('Hello Bluesky'); ``` ### TextBuilderでリンクとタグを扱う ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Record\Post; use Revolution\Bluesky\RichText\TextBuilder; $post = Post::build(function (TextBuilder $builder) { $builder->text('Hello Bluesky') ->newLine(count: 2) ->link('https://bsky.app/') ->newLine() ->tag('#Bluesky'); }); $response = Bluesky::login( identifier: config('bluesky.identifier'), password: config('bluesky.password'), )->post($post); ``` `searchPosts()` はAPIの一時的な制限により、現在は未認証では動作しません。検索を使う場合も認証済みセッションを使ってください。 ## ドキュメント App Password / OAuth、投稿、メディア、リプライ、引用、フィード、検索を確認します。 AT Protocol OAuth(DPoP)を使ったBluesky固有のSocialite設定と使い方。 Laravel Notification チャンネルとして Bluesky に投稿・DM を送信します。 独自のアルゴリズムフィードを作成・公開します。 ## 参考リンク * GitHub: [invokable/laravel-bluesky](https://github.com/invokable/laravel-bluesky) * DeepWiki: [invokable/laravel-bluesky](https://deepwiki.com/invokable/laravel-bluesky) # Labeler Source: https://kawax.biz/jp/packages/laravel-bluesky/labeler Laravel Bluesky で Labeler サーバーを構築・運用する方法。AbstractLabeler の継承、ラベル定義、WebSocket 接続、Laravel Forge でのデプロイまでを解説。 Labeler は上級者向け機能です。サーバーとして動かし続ける必要があるため、Laravel Forge を使っているか、自力でサーバーを構築できる方を対象にしています。Laravel 初心者には推奨しません。サポートはありません。 **Laravel Cloud は非対応です。** Labeler は WebSocket サーバーとして常時起動させて nginx の設定を変更する必要がありますが、Laravel Cloud は nginx の設定ができないので Labeler を動かすことができません。 ## Labeler とは Labeler は AT Protocol(Bluesky)上でコンテンツにラベルを付けるサービスです。モデレーション、コンテンツ分類、カスタムフィルタリングなどに活用できます。 Labeler の概念を事前に理解しておくことが必要です。 * [AT Protocol: Label spec](https://atproto.com/specs/label) * [Bluesky's Moderation Architecture](https://docs.bsky.app/blog/blueskys-moderation-architecture) 他言語向けのスターターキットも参考になります。 * [skyware.js.org — Labeler guide](https://skyware.js.org/guides/labeler/introduction/getting-started/) * [aliceisjustplaying/labeler-starter-kit-bsky](https://github.com/aliceisjustplaying/labeler-starter-kit-bsky) サンプル実装: * [laralabeler.bsky.social](https://bsky.app/profile/laralabeler.bsky.social) * [invokable/laralabeler](https://github.com/invokable/laralabeler) ```mermaid theme={null} sequenceDiagram participant Client as Bluesky
クライアント participant Labeler as Laravel
Labeler サーバー participant DB as データベース Client->>Labeler: WebSocket: subscribeLabels Labeler-->>Client: ラベルストリーム Client->>Labeler: POST /xrpc/tools.ozone.moderation.emitEvent Labeler->>DB: ラベルを保存 Labeler-->>Client: レスポンス ``` ## 準備 Labeler を動かすには以下が必要です。 * **Labeler 専用の新しい Bluesky アカウント**(普段使いのアカウントは使用しないこと) * **Labeler 専用の新しい Laravel プロジェクト**(プロジェクトは分けることを推奨) * **(サブ)ドメイン** * **VPS や AWS EC2 などの本番 Linux サーバー**(Laravel Cloud・Laravel Vapor・Vercel は不可) 共有サーバーしか使えない場合は Labeler の運用は困難です。 ## 追加パッケージのインストール ```bash theme={null} composer require workerman/workerman revolt/event-loop ``` ## 設定 最初に秘密鍵を生成します。 ```bash theme={null} php artisan bluesky:labeler:new-private-key ``` 生成した秘密鍵と関連する設定値を `.env` に追加します。 ```dotenv theme={null} BLUESKY_LABELER_DID=did:plc:*** BLUESKY_LABELER_IDENTIFIER=***.bsky.social BLUESKY_LABELER_APP_PASSWORD= BLUESKY_LABELER_PRIVATE_KEY="" ``` ## Labeler クラスの作成 `AbstractLabeler` を継承した独自の Labeler クラスを作成します。ファイルは任意の場所に置けます。 ```php theme={null} namespace App\Labeler; use Revolution\Bluesky\Labeler\AbstractLabeler; readonly class ArtisanLabeler extends AbstractLabeler { // 各メソッドを実装する } ``` パッケージが Labeler の処理の大部分を担うので、カスタマイズが必要な部分だけを実装します。 サンプル実装: [ArtisanLabeler.php](https://github.com/invokable/laralabeler/blob/main/app/Labeler/ArtisanLabeler.php) ### labels() ラベルの定義を返します。skyware スターターキットの定数定義が参考になります。 ```php theme={null} use Revolution\Bluesky\Labeler\LabelDefinition; use Revolution\Bluesky\Labeler\LabelLocale; public function labels(): array { return [ new LabelDefinition( identifier: 'artisan', locales: [ new LabelLocale( lang: 'en', name: 'artisan', description: 'Web artisan', ), ], severity: 'inform', blurs: 'none', defaultSetting: 'warn', adultOnly: false, ), ]; } ``` ### subscribeLabels() WebSocket 接続直後に呼び出されます。`SubscribeLabelResponse` をイテレータで返します。 ```php theme={null} use Revolution\Bluesky\Labeler\Labeler; use Revolution\Bluesky\Labeler\LabelerException; use Revolution\Bluesky\Labeler\Response\SubscribeLabelResponse; /** * @return iterable */ public function subscribeLabels(?int $cursor): iterable { if (is_null($cursor)) { return null; } // エラーレスポンスを返す場合は必ず LabelerException をスローする if ($cursor > Label::max('id')) { throw new LabelerException('FutureCursor', 'Cursor is in the future'); } foreach (Label::oldest()->where('id', '>', $cursor)->lazy() as $label) { $arr = $label->toArray(); $arr = Labeler::formatLabel($arr); yield new SubscribeLabelResponse( seq: $label->id, labels: [$arr], ); } } ``` ### emitEvent() ラベルを追加または削除するリクエストが来たときに呼び出されます。`UnsignedLabel` をイテレータで返します。 ```php theme={null} use Illuminate\Http\Request; use Revolution\Bluesky\Labeler\LabelerException; use Revolution\Bluesky\Labeler\UnsignedLabel; /** * @return iterable * * @link https://docs.bsky.app/docs/api/tools-ozone-moderation-emit-event */ public function emitEvent(Request $request, ?string $did, ?string $token): iterable { $type = data_get($request->input('event'), '$type'); if ($type !== 'tools.ozone.moderation.defs#modEventLabel') { throw new LabelerException('InvalidRequest', 'Unsupported event type'); } $subject = $request->input('subject'); $uri = data_get($subject, 'uri', data_get($subject, 'did')); $cid = data_get($subject, 'cid'); $createLabelVals = (array) data_get($request->input('event'), 'createLabelVals'); $negateLabelVals = (array) data_get($request->input('event'), 'negateLabelVals'); foreach ($createLabelVals as $val) { yield new UnsignedLabel( uri: $uri, cid: $cid, val: $val, src: config('bluesky.labeler.did'), cts: now()->micro(0)->toISOString(), ); } foreach ($negateLabelVals as $val) { yield new UnsignedLabel( uri: $uri, cid: $cid, val: $val, src: config('bluesky.labeler.did'), cts: now()->micro(0)->toISOString(), neg: true, ); } } ``` ### saveLabel() 署名済みラベルをデータベースに保存します。`SavedLabel` を返します。 ```php theme={null} use Revolution\Bluesky\Labeler\SavedLabel; use Revolution\Bluesky\Labeler\SignedLabel; public function saveLabel(SignedLabel $signed, string $sign): ?SavedLabel { // App\Models\Label は独自に作成する $saved = Label::create($signed->toArray()); return new SavedLabel( $saved->id, $signed, ); } ``` マイグレーションと Eloquent モデルはパッケージ内のサンプルを参照してください。 * [マイグレーション](https://github.com/invokable/laravel-bluesky/blob/main/workbench/database/migrations/2024_12_31_000000_create_labels_table.php) * [Eloquent モデル](https://github.com/invokable/laravel-bluesky/blob/main/workbench/app/Models/Label.php) ### createReport() ユーザーからアピールなどが送られてきたときに呼び出されます。 ```php theme={null} use Illuminate\Http\Request; /** * @link https://docs.bsky.app/docs/api/com-atproto-moderation-create-report */ public function createReport(Request $request): array { // レポートを処理して配列を返す // 必須フィールド: id, reasonType, reason, subject, reportedBy, createdAt return [ 'id' => 1, 'reasonType' => $request->input('reasonType'), 'reason' => $request->input('reason', ''), 'subject' => $request->input('subject'), 'reportedBy' => '', 'createdAt' => now()->toISOString(), ]; } ``` ### queryLabels() WebSocket の代わりに HTTP API 経由でラベルをクエリします。Bluesky 公式では使用されず、サードパーティが使用します。不要な場合は空配列を返します。 ```php theme={null} use Illuminate\Http\Request; /** * @link https://docs.bsky.app/docs/api/com-atproto-label-query-labels */ public function queryLabels(Request $request): array { return []; } ``` ## AppServiceProvider への登録 作成した Labeler クラスを `AppServiceProvider::boot()` に登録します。 ```php theme={null} use Revolution\Bluesky\Labeler\Labeler; use App\Labeler\ArtisanLabeler; class AppServiceProvider extends ServiceProvider { public function boot(): void { Labeler::register(ArtisanLabeler::class); } } ``` ## アカウントのセットアップ Labeler アカウントとして初期化します。 このコマンドではアプリパスワードではなく実際のアカウントパスワードを入力します。処理中に「PLC Update Operation Requested」というメール確認が届くので、コマンドの指示に従って入力してください。 ```bash theme={null} php artisan bluesky:labeler:setup ``` エンドポイント URL が正しく設定されていればローカル環境から実行することもできます。 ## ラベル定義の宣言 Labeler アカウントにラベル定義を登録します。 ```bash theme={null} php artisan bluesky:labeler:declare-labels ``` このコマンドもローカル環境から実行できます。 ## その他のコマンド ラベル定義の削除: ```bash theme={null} php artisan bluesky:labeler:delete-labels ``` Labeler アカウントを通常アカウントに戻す: ```bash theme={null} php artisan bluesky:labeler:restore ``` ## Laravel Forge での実行 SSL を有効化した後、以下の設定で Labeler サーバーを起動します。 ### nginx 設定 Forge の nginx 設定に 3 つの `location` ブロックを追加します。 ```nginx theme={null} # WebSocket: ラベル購読 location /xrpc/com.atproto.label.subscribeLabels { proxy_pass http://127.0.0.1:7000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; proxy_set_header X-Real-IP $remote_addr; } # HTTP: イベント送信 location /xrpc/tools.ozone.moderation.emitEvent { proxy_pass http://127.0.0.1:7001; proxy_http_version 1.1; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Connection ""; } # ヘルスチェック location /xrpc/_health { proxy_pass http://127.0.0.1:7001; proxy_http_version 1.1; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Connection ""; } ``` ### デプロイスクリプト デプロイ時に Labeler サーバーを停止します。停止後は Supervisor が自動的に再起動します。 ```bash theme={null} # 通常のデプロイ手順 $FORGE_PHP artisan bluesky:labeler:server stop # 上記が動作しない場合は直接デーモンを再起動 sudo -S supervisorctl restart daemon-{id}:* ``` ### バックグラウンドプロセス(デーモン)の設定 Forge のバックグラウンドプロセス設定画面で、Queue Worker タブではなく **Custom** タブを選択します。 **コマンド:** ```bash theme={null} php artisan bluesky:labeler:server start ``` Jetstream や Firehose と同時に起動する場合はオプションを指定します。 `bluesky:ws` や `bluesky:firehose` コマンドと同時実行はできません。 ```bash theme={null} # Jetstream と一緒に起動する場合 php artisan bluesky:labeler:server start --jetstream # 特定のコレクションをフィルタリングする場合 php artisan bluesky:labeler:server start --jetstream -C app.bsky.graph.follow -C app.bsky.feed.like # Firehose と一緒に起動する場合 php artisan bluesky:labeler:server start --firehose ``` ## ラベルの追加 どのようにラベルを付けるかはアプリケーションの実装次第です。サンプルでは Laravel のイベント機能を使い「フォローされたとき」にラベルを付けています。 ```php theme={null} // app/Listeners/FollowListener.php のイメージ use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Events\Jetstream\JetstreamCommitMessage; use Revolution\Bluesky\Types\RepoRef; public function handle(JetstreamCommitMessage $event): void { $message = $event->message; $followerDid = data_get($message, 'did'); // Bluesky の Ozone API 経由でラベルを付与 Bluesky::login( identifier: config('bluesky.labeler.identifier'), password: config('bluesky.labeler.password'), )->createLabels( subject: RepoRef::to($followerDid), labels: ['artisan'], ); } ``` イベント取りこぼしに備えて、タスクスケジュールでもラベル付けを行うことを推奨します。 Source: [docs/labeler.md](https://github.com/invokable/laravel-bluesky/blob/main/docs/labeler.md) Sample: [invokable/laralabeler](https://github.com/invokable/laralabeler) # 通知チャンネル - Laravel Bluesky Source: https://kawax.biz/jp/packages/laravel-bluesky/notification Laravel Notification チャンネルとして Bluesky に投稿・DM を送信する方法を解説します。 ## 概要 `laravel-bluesky` は Laravel の Notification システムに統合できます。`BlueskyChannel` で通常投稿、`BlueskyPrivateChannel` でダイレクトメッセージ(DM)を送れます。 ```mermaid theme={null} flowchart LR A["Laravel Notification"] --> B{"チャンネル選択"} B -->|"BlueskyChannel"| C["Bluesky 通常投稿"] B -->|"BlueskyPrivateChannel"| D["Bluesky DM送信"] ``` ## 利用可能なチャンネル | チャンネル | 用途 | | - | - | | `BlueskyChannel` | 通常の公開投稿として通知する | | `BlueskyPrivateChannel` | 受信者へのDM(プライベートチャット)として通知する | ## Notification クラス ### BlueskyChannel `via()` で `BlueskyChannel::class` を指定し、`toBluesky()` で送信内容を返します。 ```php theme={null} use Illuminate\Notifications\Notification; use Revolution\Bluesky\Notifications\BlueskyChannel; use Revolution\Bluesky\Record\Post; use Revolution\Bluesky\RichText\TextBuilder; use Revolution\Bluesky\Embed\External; class TestNotification extends Notification { public function via(object $notifiable): array { return [ BlueskyChannel::class ]; } public function toBluesky(object $notifiable): Post { $external = External::create(title: 'Title', description: 'test', uri: 'https://'); return Post::build(function (TextBuilder $builder) { $builder->text('test') ->newLine() ->tag('#Laravel'); })->embed($external); } } ``` `Post` の使い方は [Basic client](/jp/packages/laravel-bluesky/basic-client) と同じです。TextBuilder、Embed なども同様に利用できます。 ### BlueskyPrivateChannel `BlueskyPrivateMessage` は `Post` とほぼ同じですが、text・facets・embed のみ対応しています。embed は `QuoteRecord` のみサポートします。 ```php theme={null} use Illuminate\Notifications\Notification; use Revolution\Bluesky\Notifications\BlueskyPrivateChannel; use Revolution\Bluesky\Notifications\BlueskyPrivateMessage; use Revolution\Bluesky\RichText\TextBuilder; use Revolution\Bluesky\Embed\QuoteRecord; use Revolution\Bluesky\Types\StrongRef; class TestNotification extends Notification { public function via(object $notifiable): array { return [ BlueskyPrivateChannel::class ]; } public function toBlueskyPrivate(object $notifiable): BlueskyPrivateMessage { $quote = QuoteRecord::create(StrongRef::to(uri: 'at://', cid: 'cid')); return BlueskyPrivateMessage::build(function (TextBuilder $builder) { $builder->text('test') ->newLine() ->tag('#Laravel'); })->embed($quote); } } ``` ## オンデマンド通知 モデルを使わずにその場で通知先を指定する場合は `Notification::route()` を使います。 ### BlueskyChannel ```php theme={null} use Illuminate\Support\Facades\Notification; use Revolution\Bluesky\Notifications\BlueskyRoute; use Revolution\Bluesky\Session\OAuthSession; use App\Models\User; // App password Notification::route('bluesky', BlueskyRoute::to(identifier: config('bluesky.identifier'), password: config('bluesky.password'))) ->notify(new TestNotification()); // OAuth $user = User::find(1); $session = OAuthSession::create([ 'did' => $user->did, 'iss' => $user->iss, 'refresh_token' => $user->refresh_token, ]); Notification::route('bluesky', BlueskyRoute::to(oauth: $session)) ->notify(new TestNotification()); ``` ### BlueskyPrivateChannel DM には `receiver`(送信先の DID またはハンドル)の指定が必須です。また、受信者側でDM受信が有効になっている必要があります。 * App password の場合は DM 送信権限が必要です。 * OAuth の場合は `transition:chat.bsky` スコープが必要です。 ```php theme={null} use Illuminate\Support\Facades\Notification; use Revolution\Bluesky\Notifications\BlueskyRoute; use Revolution\Bluesky\Session\OAuthSession; use App\Models\User; // App password Notification::route('bluesky-private', BlueskyRoute::to(identifier: config('bluesky.identifier'), password: config('bluesky.password'), receiver: 'did or handle')) ->notify(new TestNotification()); // OAuth $user = User::find(1); $session = OAuthSession::create([ 'did' => $user->did, 'iss' => $user->iss, 'refresh_token' => $user->refresh_token, ]); Notification::route('bluesky-private', BlueskyRoute::to(oauth: $session, receiver: 'did or handle')) ->notify(new TestNotification()); ``` Bluesky では自分自身に DM を送ることができません。自分宛に通知したい場合は、送信用に別アカウントを用意してください。 ```php theme={null} // 自分宛に DM を送る場合 use Illuminate\Support\Facades\Notification; use Revolution\Bluesky\Notifications\BlueskyRoute; Notification::route('bluesky-private', BlueskyRoute::to(identifier: 'sender identifier', password: 'sender password', receiver: 'your did or handle')) ->notify(new TestNotification()); ``` 個人用途など、常に同じ送信者・受信者を使う場合は `.env` で設定できます。送信専用アカウントを作成してください。 ```dotenv theme={null} BLUESKY_SENDER_IDENTIFIER=sender did or handle BLUESKY_SENDER_APP_PASSWORD=sender password BLUESKY_RECEIVER=your did or handle ``` ```php theme={null} Notification::route('bluesky-private', BlueskyRoute::to( identifier: config('bluesky.notification.private.sender.identifier'), password: config('bluesky.notification.private.sender.password'), receiver: config('bluesky.notification.private.receiver'), ))->notify(new TestNotification()); ``` ## ユーザー通知 `Notifiable` トレイトを使うモデルに通知ルーティングを定義します。 ### BlueskyChannel ```php theme={null} use Illuminate\Notifications\Notifiable; use Revolution\Bluesky\Notifications\BlueskyRoute; use Revolution\Bluesky\Session\OAuthSession; class User { use Notifiable; public function routeNotificationForBluesky($notification): BlueskyRoute { // App password return BlueskyRoute::to(identifier: $this->bluesky_identifier, password: $this->bluesky_password); // OAuth $session = OAuthSession::create([ 'did' => $this->did, 'iss' => $this->iss, 'refresh_token' => $this->refresh_token, ]); return BlueskyRoute::to(oauth: $session); } } $user->notify(new TestNotification()); ``` ### BlueskyPrivateChannel ```php theme={null} use Illuminate\Notifications\Notifiable; use Revolution\Bluesky\Notifications\BlueskyRoute; use Revolution\Bluesky\Session\OAuthSession; class User { use Notifiable; public function routeNotificationForBlueskyPrivate($notification): BlueskyRoute { // App password return BlueskyRoute::to(identifier: $this->bluesky_identifier, password: $this->bluesky_password, receiver: $this->receiver); // OAuth $session = OAuthSession::create([ 'did' => $this->did, 'iss' => $this->iss, 'refresh_token' => $this->refresh_token, ]); return BlueskyRoute::to(oauth: $session, receiver: $this->receiver); } } $user->notify(new TestNotification()); ``` ユーザー自身を受信者にすることもできます。 ```php theme={null} public function routeNotificationForBlueskyPrivate($notification): BlueskyRoute { // App password return BlueskyRoute::to(identifier: 'sender identifier', password: 'sender password', receiver: $this->did); } ``` ## BlueskyRoute 認証方式(App password または OAuth)によって指定方法が異なります。名前付き引数を使うことを推奨します。 ```php theme={null} use Revolution\Bluesky\Notifications\BlueskyRoute; use Revolution\Bluesky\Session\OAuthSession; // App password BlueskyRoute::to(identifier: config('bluesky.identifier'), password: config('bluesky.password')) // OAuth $session = OAuthSession::create([ 'did' => '...', 'iss' => '...', 'refresh_token' => '...', ]); BlueskyRoute::to(oauth: $session); ``` 自分のアカウントへの通知だけであれば **App password** が最もシンプルです。refresh\_token の更新を考慮する必要がなく、`.env` を設定するだけで使えます。 ```dotenv theme={null} BLUESKY_IDENTIFIER= BLUESKY_APP_PASSWORD= ``` ## 通知結果の確認 通常の Laravel と同じように `NotificationSent` イベントで通知後のレスポンスを確認できます。 ```php theme={null} use Illuminate\Notifications\Events\NotificationSent; use Illuminate\Http\Client\Response; class Listener { public function handle(NotificationSent $event): void { // $event->channel BlueskyChannel // $event->notifiable // $event->notification // $event->response null|Response } } ``` Source: [docs/notification.md](https://github.com/invokable/laravel-bluesky/blob/main/docs/notification.md) # ルート Source: https://kawax.biz/jp/packages/laravel-bluesky/route laravel-bluesky パッケージが自動で定義する AT Protocol / XRPC ルートの一覧と設定方法。 ## パッケージが定義するルート パッケージは以下のルートを自動的に定義します。config または `.env` で個別に無効化できます。 | パス | ルート名 | | - | - | | `/bluesky/oauth/client-metadata.json` | `bluesky.oauth.client-metadata` | | `/bluesky/oauth/jwks.json` | `bluesky.oauth.jwks` | | `/xrpc/app.bsky.feed.getFeedSkeleton` | `bluesky.feed.skeleton` | | `/xrpc/app.bsky.feed.describeFeedGenerator` | `bluesky.feed.describe` | | `/xrpc/com.atproto.label.queryLabels` | `bluesky.labeler.query` | | `/xrpc/com.atproto.moderation.createReport` | `bluesky.labeler.report` | | `/.well-known/did.json` | `bluesky.well-known.did` | | `/.well-known/atproto-did` | `bluesky.well-known.atproto` | レスポンスを変更するには [OAuthConfig](https://github.com/invokable/laravel-bluesky/blob/main/src/Socialite/OAuthConfig.php) または [WellKnownConfig](https://github.com/invokable/laravel-bluesky/blob/main/src/WellKnown/WellKnownConfig.php) を使用してください。 Source: [docs/route.md](https://github.com/invokable/laravel-bluesky/blob/main/docs/route.md) # Socialite - Laravel Bluesky Source: https://kawax.biz/jp/packages/laravel-bluesky/socialite Laravel BlueskyのOAuth認証。AT Protocol固有のDPoP・PARエンドポイントを使ったSocialiteの設定と使い方。 ## 概要 Bluesky の OAuth は AT Protocol に基づいており、GitHub や Google などの通常の Socialite プロバイダーとは大きく異なります。 Bluesky の OAuth は他の Socialite プロバイダーとは実装が根本的に異なります。DPoP(Demonstrated Proof of Possession)と PAR(Pushed Authorization Requests)エンドポイントを使用します。`client_secret` は不要で、代わりに秘密鍵を使います。 ### 通常の OAuth との違い | 項目 | 通常の Socialite | Bluesky Socialite | | - | - | - | | 認証方式 | OAuth 2.0 | AT Protocol OAuth(DPoP) | | クライアント識別 | `client_id` + `client_secret` | 秘密鍵 + `client-metadata.json` URL | | `client_id` | 登録済みの固定文字列 | `client-metadata.json` の URL | | ユーザー識別子 | メールアドレス等 | DID(`did:plc:...`)またはハンドル | | ログインヒント | 不要 | ハンドル / DID / PDS URL を渡せる | ### 認証フロー ```mermaid theme={null} sequenceDiagram participant User as ユーザー participant App as Laravel
アプリ participant Bluesky as Bluesky
サーバー User->>App: ログインフォーム送信(ハンドル入力) App->>Bluesky: PAR リクエスト(login_hint 含む) Bluesky-->>App: request_uri App->>Bluesky: 認可エンドポイントへリダイレクト User->>Bluesky: Bluesky でログイン承認 Bluesky-->>App: コールバック(code + iss) App->>Bluesky: DPoP を使ったトークン交換 Bluesky-->>App: アクセストークン + OAuthSession App->>User: ログイン完了 ``` ## インストールと設定 ### 秘密鍵の作成 まず秘密鍵を生成します。これは Bluesky への登録なしに行えます。 ```bash theme={null} php artisan bluesky:new-private-key ``` ``` Please set this private key in .env BLUESKY_OAUTH_PRIVATE_KEY="...url-safe base64 encoded key..." ``` 出力された値を `.env` にコピーします。 ```dotenv theme={null} BLUESKY_OAUTH_PRIVATE_KEY="..." ``` Bluesky の場合、`client_id` や `client_secret` の登録は不要です。秘密鍵の設定だけで OAuth 認証を使えます。 ### デフォルトのOAuthスコープ パッケージは3つの主要なユースケースをサポートするデフォルトのOAuthスコープで設定されています。 1. **Socialiteログイン** — `atproto`、`account:email`、`include:app.bsky.authViewAll` でユーザー認証とメールアクセスを有効にします 2. **投稿** — `include:app.bsky.authCreatePosts` と `blob:*/*` で投稿の作成と画像・動画のアップロードを許可します 3. **DM通知** — `rpc:chat.bsky.convo.sendMessage` と `rpc:chat.bsky.convo.getConvoForMembers` で通知用のダイレクトメッセージ送信を有効にします `BLUESKY_OAUTH_SCOPE` 環境変数を設定することでスコープをカスタマイズできます。 ```dotenv theme={null} BLUESKY_OAUTH_SCOPE="atproto account:email include:app.bsky.authViewAll" ``` 利用可能なスコープの詳細については、[AT Protocol Permission Requestsドキュメント](https://atproto.com/guides/permission-requests)を参照してください。 ### ローカル開発 デフォルトでは `http://localhost` と `http://127.0.0.1:8000/` が設定されているため、ローカル開発では追加の設定は不要です。 ```dotenv theme={null} # ポートを変更している場合のみ設定 BLUESKY_CLIENT_ID= BLUESKY_REDIRECT=http://127.0.0.1:8080/ ``` ### 本番環境 ルート名 `bluesky.oauth.redirect` が存在する場合、`.env` への設定は不要です。デフォルトのルート名を変更した場合は設定します。 ```dotenv theme={null} BLUESKY_REDIRECT=/bluesky/callback ``` ## ルート設定 コールバックルートのルート名は `bluesky.oauth.redirect` を推奨します。パッケージがこの名前を内部で使用します。 ```php theme={null} // routes/web.php use Illuminate\Support\Facades\Route; use App\Http\Controllers\SocialiteController; Route::get('login', [SocialiteController::class, 'login'])->name('login'); Route::match(['get', 'post'], 'redirect', [SocialiteController::class, 'redirect']); Route::get('bluesky/callback', [SocialiteController::class, 'callback']) ->name('bluesky.oauth.redirect'); ``` ### ローカル開発でのコールバック処理 ローカル開発中は、Bluesky からのコールバック URL が `http://127.0.0.1:8000/` に固定されます。ルートレベルで振り分けると便利です。 ```php theme={null} use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; Route::get('/', function (Request $request) { if (app()->isLocal() && $request->has('iss')) { return to_route('bluesky.oauth.redirect', $request->query()); } // ... }); ``` ## コントローラー実装 ```php theme={null} input('login_hint'); $request->session()->put('hint', $hint); return Socialite::driver('bluesky') ->hint($hint) ->redirect(); } public function callback(Request $request) { if ($request->missing('code')) { // 開発中のデバッグ用。本番では適切なエラー処理に置き換えること dd($request); } $hint = $request->session()->pull('hint'); /** @var \Laravel\Socialite\Two\User $user */ $user = Socialite::driver('bluesky') ->hint($hint) ->user(); /** @var OAuthSession $session */ $session = $user->session; // OAuthSession を Laravel セッションに保存 $request->session()->put('bluesky_session', $session->toArray()); $loginUser = User::updateOrCreate([ 'did' => $session->did(), ], [ 'iss' => $session->issuer(), 'handle' => $session->handle(), 'name' => $session->displayName(), 'avatar' => $session->avatar(), 'access_token' => $session->token(), 'refresh_token' => $session->refresh(), ]); auth()->login($loginUser, true); return to_route('bluesky.dashboard'); } } ``` ## ユーザー情報(OAuthSession) `$user->session` から取得できる `OAuthSession` の主なメソッドを示します。 | メソッド | 説明 | 例 | | - | - | - | | `did()` | Bluesky DID | `did:plc:xxxxxx` | | `handle()` | Bluesky ハンドル | `alice.bsky.social` | | `displayName()` | 表示名 | `Alice` | | `avatar()` | アバター URL | `https://...` | | `issuer()` | PDS の URL | `https://bsky.social` | | `token()` | アクセストークン | | | `refresh()` | リフレッシュトークン | | 全プロパティを確認するには `toArray()` を使います。 ```php theme={null} /** @var OAuthSession $session */ dump($session->toArray()); ``` ## データベース設定 `users` テーブルに Bluesky 固有のカラムを追加します。DID が Bluesky ユーザーの一意識別子です。 ```php theme={null} // database/migrations/xxxx_add_bluesky_columns_to_users_table.php Schema::table('users', function (Blueprint $table) { $table->string('did')->nullable()->unique(); $table->string('iss')->nullable(); $table->string('handle')->nullable(); $table->string('avatar')->nullable(); $table->text('access_token')->nullable(); $table->text('refresh_token')->nullable(); }); ``` ## OAuthSession の再利用 セッションに保存した OAuthSession を使って API を呼び出せます。 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Session\OAuthSession; $session = OAuthSession::create(session('bluesky_session')); $timeline = Bluesky::withToken($session)->getTimeline(); ``` Job や Console など Laravel セッションが使えない場合は、DBからデータを取得して OAuthSession を組み立てます。 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Session\OAuthSession; $session = OAuthSession::create([ 'did' => $user->did, 'refresh_token' => $user->refresh_token, // bsky.social 以外のアカウントは iss も指定 // 'iss' => $user->iss, ]); $timeline = Bluesky::withToken($session) ->refreshSession() ->getTimeline(); ``` ## トークンの自動更新 リフレッシュトークンは一度しか使えないため、更新後は必ずDBに保存し直す必要があります。`OAuthSessionUpdated` イベントを使います。 ```bash theme={null} php artisan make:listener OAuthSessionUpdatedListener ``` ```php theme={null} namespace App\Listeners; use App\Models\User; use Revolution\Bluesky\Events\OAuthSessionUpdated; class OAuthSessionUpdatedListener { public function handle(OAuthSessionUpdated $event): void { if (empty($event->session->did())) { return; } session()->put('bluesky_session', $event->session->toArray()); User::firstWhere('did', $event->session->did()) ->fill([ 'iss' => $event->session->issuer(), 'handle' => $event->session->handle(), 'name' => $event->session->displayName(), 'avatar' => $event->session->avatar(), 'access_token' => $event->session->token(), 'refresh_token' => $event->session->refresh(), ])->save(); } } ``` リフレッシュ開始時には `OAuthSessionRefreshing` イベントも発行されます。この時点で `refresh_token` が無効になるため、DBから削除しておくと安全です。 ```php theme={null} use Revolution\Bluesky\Events\OAuthSessionRefreshing; public function handle(OAuthSessionRefreshing $event): void { if (empty($event->session->did())) { return; } User::firstWhere('did', $event->session->did()) ->fill(['refresh_token' => '']) ->save(); } ``` ## WithBluesky トレイト User モデルに `WithBluesky` トレイトを追加し `tokenForBluesky()` を実装すると、`$user->bluesky()` で認証済みクライアントを取得できます。 ```php theme={null} namespace App\Models; use Illuminate\Foundation\Auth\User as Authenticatable; use Revolution\Bluesky\Session\OAuthSession; use Revolution\Bluesky\Traits\WithBluesky; class User extends Authenticatable { use WithBluesky; protected function tokenForBluesky(): OAuthSession { return OAuthSession::create([ 'did' => $this->did, 'refresh_token' => $this->refresh_token, 'iss' => $this->iss, ]); } } ``` ```php theme={null} $profile = auth()->user() ->bluesky() ->refreshSession() ->getProfile(); ``` ## client-metadata のカスタマイズ パッケージは `bluesky.oauth.client-metadata` と `bluesky.oauth.jwks` のルートを自動で定義します。通常変更は不要ですが、`OAuthConfig` でカスタマイズできます。 ```php theme={null} // AppServiceProvider use Revolution\Bluesky\Socialite\OAuthConfig; public function boot(): void { OAuthConfig::clientMetadataUsing(function () { return collect(config('bluesky.oauth.metadata')) ->merge([ 'client_id' => route('bluesky.oauth.client-metadata'), 'jwks_uri' => route('bluesky.oauth.jwks'), 'redirect_uris' => [url('bluesky/callback')], ]) ->reject(fn ($item) => is_null($item)) ->toArray(); }); } ``` ## 未認証時の動作 `OAuthSession` が null またはリフレッシュトークンがない場合、`Unauthenticated` 例外がスローされ `login` ルートへリダイレクトされます。 ```php theme={null} // bootstrap/app.php ->withMiddleware(function (Middleware $middleware) { $middleware->redirectGuestsTo('/bluesky/login'); }) ``` Source: [docs/socialite.md](https://github.com/invokable/laravel-bluesky/blob/main/docs/socialite.md) # テスト Source: https://kawax.biz/jp/packages/laravel-bluesky/testing laravel-bluesky を使ったコードのテスト方法。Facade モック、HTTP フェイク、テストできない機能の解説。 ## 概要 `laravel-bluesky` の主要機能は `Bluesky` Facade 経由で提供されるため、標準的な Laravel モックを使ってテストできます。パッケージ開発のテスト全般については [package-testing](/jp/advanced/package-testing) も参照してください。 ## Facade モック `Bluesky::expects()` を使ってメソッドチェーンごとモックします。 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Illuminate\Support\Facades\Http; use Illuminate\Http\Client\Response; Bluesky::expects('login->getProfile->json')->once()->andReturn([]); Bluesky::expects('login->getProfile')->once()->andReturn(new Response(Http::response([])->wait())); ``` ## HTTP フェイク `laravel-bluesky` は内部で Laravel の HTTP クライアントを使用しています。`Http::preventStrayRequests()` を `setUp()` に書いておくと、予期しない外部リクエストを即座に検知できます。 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Illuminate\Support\Facades\Http; protected function setUp(): void { parent::setUp(); Http::preventStrayRequests(); } public function test_post(): void { Http::fake(); Http::fakeSequence() ->push(); Bluesky::expects('resolveHandle->json')->once()->andReturn('did'); } ``` `Http::preventStrayRequests()` はテスト実行中に本物の外部リクエストが発生した場合に例外をスローします。意図しない外部通信をすぐに発見できるため、テストの信頼性が上がります。 ## モックできない機能 ### FeedGenerator FeedGenerator は Bluesky サーバー側から呼び出されるため、エンドツーエンドのモックはありません。ただし FeedGenerator のアルゴリズム部分はモック可能です。 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\FeedGenerator\FeedGenerator; public function test_feed_generator(): void { FeedGenerator::register(name: 'test', algo: function (?int $limit, ?string $cursor) { // API 制限により認証が必要 $posts = Bluesky::login(identifier: config('bluesky.identifier'), password: config('bluesky.password')) ->searchPosts(q: '#bluesky')->collect('posts'); $feed = $posts->map(function (array $post) { return ['post' => data_get($post, 'uri')]; })->toArray(); return ['feed' => $feed]; }); Bluesky::expects('login->searchPosts->collect')->once()->andReturn(collect([['uri' => 'at://']])); $response = $this->get(route('bluesky.feed.skeleton', ['feed' => 'at://did:/app.bsky.feed.generator/test'])); $response->assertSuccessful(); $response->assertJson(['feed' => [['post' => 'at://']]]); } ``` ### Core Bluesky/AtProtocol のコア機能は外部アクセスを伴わないため、モックは必要ありません。 Source: [docs/testing.md](https://github.com/invokable/laravel-bluesky/blob/main/docs/testing.md) # TextBuilder Source: https://kawax.biz/jp/packages/laravel-bluesky/text-builder Bluesky のリッチテキスト(facets)を PHP で構築するための TextBuilder クラスの詳細な使い方。 ## TextBuilder とは `TextBuilder` は、Bluesky の AT Protocol が定義する **facets**(リッチテキスト注釈)をメソッドチェーンで組み立てるためのクラスです。 Bluesky の投稿本文はプレーンテキストですが、メンション・リンク・ハッシュタグを表示するためにはテキスト中の位置(バイトオフセット)と種別を示す `facets` 配列を一緒に送る必要があります。`TextBuilder` はこのオフセット計算と配列構築を自動的に行います。 ```mermaid theme={null} sequenceDiagram participant App as Laravel アプリ participant TB as TextBuilder participant Post as Post レコード participant AT as AT Protocol App->>TB: TextBuilder::make('Hello') App->>TB: ->mention('@alice.bsky.social') App->>TB: ->link('https://example.com') App->>TB: ->tag('#Laravel') TB->>TB: バイトオフセットを計算して
facets 配列を構築 TB->>Post: toPost() Post->>AT: Bluesky::post($post) AT-->>App: レスポンス ``` ## 基本的なテキスト作成 ### TextBuilder::make() `TextBuilder::make()` で初期テキストを指定してインスタンスを生成します。初期テキストは省略可能です。 ```php theme={null} use Revolution\Bluesky\RichText\TextBuilder; $builder = TextBuilder::make(text: 'Hello Bluesky'); ``` ### text() `text()` でテキストを末尾に追加します。 ```php theme={null} $builder = TextBuilder::make() ->text('Hello ') ->text('Bluesky'); // $builder->text === 'Hello Bluesky' ``` ### newLine() 改行を追加します。`count` で行数を指定できます(デフォルト: 1)。 ```php theme={null} $builder = TextBuilder::make('1行目') ->newLine() ->text('2行目') ->newLine(count: 2) ->text('4行目'); ``` ### toPost() `TextBuilder` インスタンスを `Post` レコードに変換します。`Bluesky::post()` に直接渡せます。 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\RichText\TextBuilder; $post = TextBuilder::make('シンプルな投稿')->toPost(); $response = Bluesky::withToken()->post($post); ``` ### Post::build() `Post::build()` にクロージャを渡す方法でも使えます。戻り値が `Post` になります。 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Record\Post; use Revolution\Bluesky\RichText\TextBuilder; $post = Post::build(function (TextBuilder $builder) { $builder->text('Hello Bluesky'); }); $response = Bluesky::withToken()->post($post); ``` ## メンション(`@mention`)の追加 `mention()` でメンション facet を追加します。 ```php theme={null} $builder->mention(text: '@alice.bsky.social'); ``` ### DID の自動解決 `did` を省略すると、ハンドルから DID を自動解決します(`Bluesky::resolveHandle()` が呼ばれます)。 ```php theme={null} // DID を自動解決(API 呼び出しが発生します) $builder->mention('@alice.bsky.social'); ``` ### DID を明示指定 DID が既にわかっている場合は明示的に渡すことで API 呼び出しを回避できます。 ```php theme={null} // DID を直接指定(推奨: API 呼び出しなし) $builder->mention(text: '@alice.bsky.social', did: 'did:plc:xxxxxxxxxxxxxxxxxxxx'); ``` 本番環境でメンションを多用する場合は、DID をキャッシュしておくと API 呼び出しを削減できます。 ## リンク(URL)の埋め込み `link()` でリンク facet を追加します。 ```php theme={null} // URL そのものを表示テキストにする $builder->link('https://laravel.com'); // 表示テキストと URL を別々に指定 $builder->link(text: 'Laravel 公式サイト', uri: 'https://laravel.com'); ``` `uri` を省略すると `text` がそのまま URI として使われます。 ## ハッシュタグの追加 `tag()` でハッシュタグ facet を追加します。 ```php theme={null} // # から始まるテキストを渡すとタグを自動抽出 $builder->tag('#Laravel'); // タグ文字列を明示指定(# なしの文字列) $builder->tag(text: '#Laravel', tag: 'Laravel'); ``` ## 複合テキストの構築 複数の facet を組み合わせてリッチな投稿テキストを作れます。 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\RichText\TextBuilder; $post = TextBuilder::make('新しい記事を公開しました!') ->newLine(count: 2) ->mention('@alice.bsky.social', did: 'did:plc:xxxx') ->text(' さんにもぜひ読んでほしいです。') ->newLine() ->link(text: '記事を読む', uri: 'https://example.com/article/1') ->newLine() ->tag('#Laravel') ->text(' ') ->tag('#PHP') ->toPost(); $response = Bluesky::withToken()->post($post); ``` ### Post::build() を使った記述 ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Record\Post; use Revolution\Bluesky\RichText\TextBuilder; $post = Post::build(function (TextBuilder $builder) { $builder->text('新しい記事を公開しました!') ->newLine(count: 2) ->mention('@alice.bsky.social', did: 'did:plc:xxxx') ->text(' さんにもぜひ読んでほしいです。') ->newLine() ->link(text: '記事を読む', uri: 'https://example.com/article/1') ->newLine() ->tag('#Laravel') ->text(' ') ->tag('#PHP'); }); $response = Bluesky::withToken()->post($post); ``` ## 投稿との統合 ### Bluesky::post() との組み合わせ ```php theme={null} use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\RichText\TextBuilder; $post = TextBuilder::make('test') ->newLine() ->link('https://bsky.app/') ->toPost(); $response = Bluesky::withToken()->post($post); ``` ### Notification チャンネルとの組み合わせ `BlueskyChannel` の `toBluesky()` メソッドで `Post::build()` を使えます。 ```php theme={null} use Illuminate\Notifications\Notification; use Revolution\Bluesky\Notifications\BlueskyChannel; use Revolution\Bluesky\Record\Post; use Revolution\Bluesky\RichText\TextBuilder; class DeployedNotification extends Notification { public function __construct( private string $url, ) {} public function via(object $notifiable): array { return [BlueskyChannel::class]; } public function toBluesky(object $notifiable): Post { return Post::build(function (TextBuilder $builder) { $builder->text('デプロイが完了しました') ->newLine() ->link(text: '確認する', uri: $this->url) ->newLine() ->tag('#Laravel'); }); } } ``` ## facets の自動検出 テキスト中の `@mention`・URL・`#hashtag` を自動で検出して facets を設定することもできます。 ```php theme={null} use Revolution\Bluesky\RichText\TextBuilder; $builder = TextBuilder::make('@alice.bsky.social test https://example.com #alice') ->detectFacets(); // detectFacets() 後にさらに facets を追加することも可能 $builder->newLine()->tag('#bob'); $post = $builder->toPost(); ``` `detectFacets()` は正規表現ベースの検出です。確実にリンクしたい場合は `link()`・`mention()`・`tag()` を明示的に使う方が確実です。 ## カスタム facet の追加 `facet()` メソッドで任意の facet 配列を直接追加できます。 ```php theme={null} use Revolution\Bluesky\RichText\TextBuilder; $builder = TextBuilder::make(); $builder->facet([ 'index' => [ 'byteStart' => 0, 'byteEnd' => 5, ], 'features' => [ [ '$type' => 'app.bsky.richtext.facet#link', 'uri' => 'https://example.com', ], ], ]); ``` ## 文字数制限と注意事項 ### バイトオフセットと grapheme AT Protocol の facet インデックスは **UTF-8 バイトオフセット**で指定します。`TextBuilder` は内部で `strlen()` を使ってバイト数を計算しています。 日本語・絵文字などのマルチバイト文字は 1 文字でも複数バイトを消費するため、**文字数ではなくバイト数**でオフセットが決まります。 ```php theme={null} // 'Hello' は UTF-8 で 5 バイト(1 文字 = 1 バイト) // '日本語' は UTF-8 で 9 バイト(1 文字 = 3 バイト) $builder = TextBuilder::make('日本語'); // strlen($builder->text) === 9 ``` ### 投稿の文字数制限 Bluesky の投稿は **grapheme(表示上の文字数)で最大 300 文字**です。バイト数ではなく grapheme で制限されるため、日本語でも 300 文字書けます。 ```php theme={null} use Illuminate\Support\Str; $text = 'これが投稿テキストです。'; // grapheme での文字数確認 $length = Str::length($text); // mb_strlen() と同等 ``` `TextBuilder` 自体は文字数チェックを行いません。300 grapheme を超える投稿を送ると AT Protocol API がエラーを返します。 ## メソッド一覧 | メソッド | 説明 | | - | - | | `TextBuilder::make(string $text = '')` | インスタンスを生成 | | `text(string $text)` | テキストを末尾に追加 | | `newLine(int $count = 1)` | 改行を追加 | | `mention(string $text, ?string $did = null)` | メンション facet を追加 | | `link(string $text, ?string $uri = null)` | リンク facet を追加 | | `tag(string $text, ?string $tag = null)` | ハッシュタグ facet を追加 | | `detectFacets()` | テキストから facets を自動検出 | | `facet(array $facet)` | カスタム facet を追加 | | `resetFacets()` | 全 facets をリセット | | `toPost()` | `Post` レコードへ変換 | | `toArray()` | `['text' => ..., 'facets' => ...]` 配列へ変換 | Source: [src/RichText/TextBuilder.php](https://github.com/invokable/laravel-bluesky/blob/main/src/RichText/TextBuilder.php) # 検証(Verify) Source: https://kawax.biz/jp/packages/laravel-bluesky/verify Bluesky / AT Protocol の検証機能。投稿・画像・CAR ファイルの CID 検証方法。 ## 概要 検証可能性は Bluesky / AT Protocol の重要なコンセプトです。 ## 投稿の検証 [com.atproto.repo.getRecord](https://docs.bsky.app/docs/api/com-atproto-repo-get-record) で取得した投稿データの CID がレコードと一致するか検証できます。 ```php theme={null} $block = [ 'uri' => 'at://did:plc:***/app.bsky.feed.post/***', 'cid' => 'bafyreih5y47li4zuvvzevmq4xl7woqxchfc2pnfclv3kfz3zefb2qd3bzm', 'value' => [ 'text' => 'Hello, Bluesky!', '$type' => 'app.bsky.feed.post', 'createdAt' => '2025-01-01T00:00:00.000Z', ], ]; ``` ```php theme={null} use Revolution\Bluesky\Core\CBOR; use Revolution\Bluesky\Core\CID; $cid = data_get($block, 'cid'); $record = data_get($block, 'value'); // DAG-CBOR にエンコード $cbor = CBOR::encode($record); $bool = CID::verify($cbor, $cid); ``` サンプル実装: [DownloadRecordCommand](https://github.com/invokable/laravel-bluesky/blob/main/src/Console/DownloadRecordCommand.php) ## 画像ファイルの検証 画像を含む投稿では、`$link` が画像の CID になります。 ```php theme={null} $block = [ 'uri' => 'at://did:plc:***/app.bsky.feed.post/***', 'cid' => 'b+++', 'value' => [ 'text' => '画像付き投稿', '$type' => 'app.bsky.feed.post', 'embed' => [ '$type' => 'app.bsky.embed.images', 'images' => [ [ 'alt' => '', 'image' => [ 'ref' => [ '$link' => 'b***image' ], 'size' => 100000, '$type' => 'blob', 'mimeType' => 'image/jpeg' ], 'aspectRatio' => [ 'width' => 1000, 'height' => 1000 ] ] ] ], 'createdAt' => '2025-01-01T00:00:00.000Z' ] ]; ``` [com.atproto.sync.getBlob](https://docs.bsky.app/docs/api/com-atproto-sync-get-blob) でダウンロードした画像の生データを検証します。 ```php theme={null} use Revolution\Bluesky\Core\CID; // 画像の CID は $link から取得 $cid = data_get($block, 'value.embed.images.0.image.ref.$link'); $file = file_get_contents('path/to/b***image.jpg'); $bool = CID::verify($file, $cid, codec: CID::RAW); ``` サンプル実装: [DownloadBlobsCommand](https://github.com/invokable/laravel-bluesky/blob/main/src/Console/DownloadBlobsCommand.php) ## CAR ファイルの検証 CAR ファイルにはユーザーのすべてのレコードが含まれます。 [com.atproto.sync.getRepo](https://docs.bsky.app/docs/api/com-atproto-sync-get-repo) でダウンロードした CAR ファイルが対象ユーザーのものかどうか検証できます。 ```php theme={null} use Revolution\Bluesky\Core\CAR; use Revolution\Bluesky\Crypto\DidKey; use Revolution\Bluesky\Facades\Bluesky; use Revolution\Bluesky\Support\DidDocument; $did = 'did:plc:***'; // 検証には didDoc の公開鍵が必要 $didDoc = DidDocument::make(Bluesky::identity()->resolveDID($did)->json()); $didKey = DidKey::parse($didDoc->publicKey()); $car = 'CAR ファイルの生データまたはファイルへのストリーム'; $signed = CAR::signedCommit($car); $bool = CAR::verifySignedCommit($signed, $didKey); ``` サンプル実装: [DownloadRepoCommand](https://github.com/invokable/laravel-bluesky/blob/main/src/Console/DownloadRepoCommand.php) ## CAR ファイルの展開後の検証 CAR ファイルを展開して取得したレコードの検証は、最初の投稿の例と同じ方法で行います。 サンプル実装: [UnpackRepoCommand](https://github.com/invokable/laravel-bluesky/blob/main/src/Console/UnpackRepoCommand.php) Source: [docs/verify.md](https://github.com/invokable/laravel-bluesky/blob/main/docs/verify.md) # WebSocket (Jetstream / Firehose) Source: https://kawax.biz/jp/packages/laravel-bluesky/websocket Laravel Bluesky の WebSocket 機能。Bluesky Jetstream と AT Protocol Firehose を使ったリアルタイムイベント処理の方法。JetstreamServeCommand と FirehoseServeCommand の使い方を解説。 ## 概要 `laravel-bluesky` は Bluesky のリアルタイムストリームに接続するための2つの WebSocket コマンドを提供します。 * **Jetstream** — Bluesky 独自のフィルタリング済み WebSocket エンドポイント。JSON 形式で軽量。 * **Firehose** — AT Protocol の生のイベントストリーム。DAG-CBOR バイナリ形式で全データを受信。 ```mermaid theme={null} graph LR subgraph "Bluesky ネットワーク" J["Jetstream
jetstream1.us-west.bsky.network
JSON / フィルター可能"] F["Firehose
bsky.network
DAG-CBOR / 全データ"] end subgraph "Laravel アプリ" WS["JetstreamServeCommand
FirehoseServeCommand
(Workerman)"] E["Laravel Events
JetstreamCommitMessage
FirehoseCommitMessage など"] L["Listener / Job"] end J -->|WebSocket| WS F -->|WebSocket| WS WS --> E --> L ``` WebSocket による長時間実行プロセスは **VPS や EC2 などの常時起動サーバー** または **Laravel Cloud のカスタムワーカー** が必要です。Laravel Vapor や Vercel などのサーバーレス環境では動作しません。 ## インストール WebSocket 機能には [Workerman](https://github.com/walkor/workerman) が必要です。 ```bash theme={null} composer require workerman/workerman ``` ## Jetstream ### 概要 Jetstream は Bluesky が提供するフィルタリング済みの WebSocket サービスです。コレクション種別やユーザー DID でフィルタリングできるため、必要なイベントだけを効率的に受信できます。 | 特徴 | 説明 | | - | - | | データ形式 | JSON | | フィルタリング | コレクション・DID でフィルター可能 | | データ量 | フィルター次第で軽量 | | 用途 | 投稿・いいね・フォロー等の監視 | ### 起動方法 ```bash theme={null} # すべてのメッセージを受信(フィルターなし) php artisan bluesky:ws start # デバッグ: 受信したすべてのメッセージを表示 php artisan bluesky:ws start -v ``` ### コレクションフィルター `-C` オプションで受信するコレクションを絞り込みます。複数指定可能です。 ```bash theme={null} # 投稿といいねのみ受信 php artisan bluesky:ws start -C app.bsky.feed.post -C app.bsky.feed.like # フォローのみ受信 php artisan bluesky:ws start -C app.bsky.graph.follow ``` 主なコレクション: | コレクション | 内容 | | - | - | | `app.bsky.feed.post` | 投稿の作成・削除 | | `app.bsky.feed.like` | いいね | | `app.bsky.feed.repost` | リポスト | | `app.bsky.graph.follow` | フォロー | | `app.bsky.graph.block` | ブロック | ### DID フィルター `-D` オプションで特定ユーザーのイベントのみ受信します。 ```bash theme={null} # 特定ユーザーの投稿のみ受信 php artisan bluesky:ws start -C app.bsky.feed.post -D did:plc:xxx -D did:plc:yyy ``` ### イベント処理 Jetstream コマンドは受信したメッセージの種別に応じて Laravel イベントを発火します。 | イベントクラス | タイミング | | - | - | | `JetstreamMessageReceived` | 全メッセージ受信時 | | `JetstreamCommitMessage` | レコードの作成・更新・削除時 | | `JetstreamIdentityMessage` | ハンドル変更などの identity イベント時 | | `JetstreamAccountMessage` | アカウント有効化・無効化時 | イベントリスナーを作成してイベントを処理します。 ```bash theme={null} php artisan make:listener JetstreamPostListener ``` ```php theme={null} namespace App\Listeners; use Revolution\Bluesky\Events\Jetstream\JetstreamCommitMessage; class JetstreamPostListener { public function handle(JetstreamCommitMessage $event): void { // コレクション種別を確認 $collection = data_get($event->message, 'commit.collection'); if ($collection !== 'app.bsky.feed.post') { return; } // 操作種別: create / update / delete $operation = $event->operation; // 投稿者の DID $did = $event->message['did']; // レコードの内容 $record = data_get($event->message, 'commit.record'); $text = data_get($record, 'text', ''); info("[$operation] $did: $text"); } } ``` ## Firehose ### 概要 Firehose は AT Protocol の生のイベントストリームです。Bluesky ネットワーク上のすべてのレコード操作をバイナリ(DAG-CBOR)形式で受信します。 | 特徴 | 説明 | | - | - | | データ形式 | DAG-CBOR バイナリ(パッケージが自動デコード) | | フィルタリング | なし(全データを受信) | | データ量 | 非常に大量 | | 用途 | 全データの収集・アーカイブ | DAG-CBOR のデコードはパッケージが自動的に行います。イベントリスナーでは通常の PHP 配列としてデータを受け取れます。 ### 起動方法 ```bash theme={null} php artisan bluesky:firehose start # デバッグ: 受信したメッセージを表示 php artisan bluesky:firehose start -v ``` ### イベント処理 Firehose コマンドも Laravel イベントを使ってメッセージを処理します。 | イベントクラス | タイミング | | - | - | | `FirehoseMessageReceived` | 全メッセージ受信時(生データ含む) | | `FirehoseCommitMessage` | レコードの作成・更新・削除時 | | `FirehoseIdentityMessage` | identity イベント時 | | `FirehoseAccountMessage` | アカウントイベント時 | | `FirehoseSyncMessage` | リポジトリ同期イベント時 | ```bash theme={null} php artisan make:listener FirehosePostListener ``` ```php theme={null} namespace App\Listeners; use Revolution\Bluesky\Events\Firehose\FirehoseCommitMessage; class FirehosePostListener { public function handle(FirehoseCommitMessage $event): void { // コレクション種別を確認 if ($event->collection !== 'app.bsky.feed.post') { return; } // 操作種別: create / update / delete $action = $event->action; // 投稿者の DID $did = $event->did; // レコードの内容(デコード済み配列) $record = $event->record; $text = data_get($record, 'value.text', ''); info("[$action] $did: $text"); } } ``` ## 設定 `config/bluesky.php` で接続先ホストやログ設定を変更できます。 ```php theme={null} // Jetstream 'jetstream' => [ 'host' => env('BLUESKY_JETSTREAM_HOST', 'jetstream1.us-west.bsky.network'), 'max' => env('BLUESKY_JETSTREAM_MAX', 0), // maxMessageSizeBytes (0 = 無制限) 'logging' => [ 'driver' => env('BLUESKY_JETSTREAM_LOG_DRIVER', 'daily'), 'days' => 7, 'path' => env('BLUESKY_JETSTREAM_LOG_PATH', storage_path('logs/jetstream.log')), ], ], // Firehose 'firehose' => [ 'host' => env('BLUESKY_FIREHOSE_HOST', 'bsky.network'), 'logging' => [ 'driver' => env('BLUESKY_FIREHOSE_LOG_DRIVER', 'daily'), 'days' => 7, 'path' => env('BLUESKY_FIREHOSE_LOG_PATH', storage_path('logs/firehose.log')), ], ], ``` `.env` での設定例: ```ini theme={null} BLUESKY_JETSTREAM_HOST=jetstream2.us-east.bsky.network BLUESKY_JETSTREAM_MAX=1000000 ``` ## Labeler との組み合わせ Labeler サーバーと Jetstream / Firehose を同時に起動できます。Labeler が受け取ったラベリングリクエストの処理に Jetstream や Firehose のデータを利用できます。 ```bash theme={null} # Labeler + Jetstream(フォローイベントを監視) php artisan bluesky:labeler:server start --jetstream -C app.bsky.graph.follow # Labeler + Firehose(全データを受信) php artisan bluesky:labeler:server start --firehose ``` Labeler の詳細は [Labeler ページ](/jp/packages/laravel-bluesky/labeler) を参照してください。 ## 長時間実行プロセスの運用 WebSocket コマンドは長時間起動し続けるプロセスです。本番環境では Supervisor などのプロセス管理ツールを使用してください。 ### Supervisor 設定例 `/etc/supervisor/conf.d/bluesky-jetstream.conf`: ```ini theme={null} [program:bluesky-jetstream] process_name=%(program_name)s_%(process_num)02d command=php /var/www/html/artisan bluesky:ws start -C app.bsky.feed.post autostart=true autorestart=true stopasgroup=true killasgroup=true user=www-data numprocs=1 redirect_stderr=true stdout_logfile=/var/www/html/storage/logs/jetstream-worker.log stopwaitsecs=3600 ``` ```bash theme={null} # Supervisor を再読み込みして起動 sudo supervisorctl reread sudo supervisorctl update sudo supervisorctl start bluesky-jetstream:* ``` ### Laravel Forge のデーモン設定 Laravel Forge を使用している場合は、**Daemons** セクションからデーモンを追加します。 * **Command**: `php artisan bluesky:ws start -C app.bsky.feed.post` * **Directory**: `/var/www/html` * **User**: `forge` ### Laravel Cloud でのバックグラウンドプロセス設定 WebSocket コマンドは **WebSocket クライアント** として Bluesky のストリームに接続するため、Laravel Cloud でも動作します。Laravel Cloud のバックグラウンドプロセス(カスタムワーカー)として設定してください。 Laravel Cloud のバックグラウンドプロセス設定で **Custom Worker** を追加します。 **Jetstream の場合:** ```bash theme={null} php artisan bluesky:ws start ``` **Firehose の場合:** ```bash theme={null} php artisan bluesky:firehose start ``` デプロイ時のプロセス停止・再起動などはすべて Laravel Cloud 側で自動的に処理されます。バックグラウンドプロセスの設定以外に追加の設定は不要です。 ### 注意事項 * プロセスが予期せず終了した場合、`autorestart=true` で自動再起動されます。 * メモリリークを防ぐため、定期的な再起動を検討してください。 * 大量のメッセージを受信する Firehose では、リスナー内の処理は非同期(Queue Job)にすることを推奨します。 ```php theme={null} // リスナーで Queue Job にディスパッチする例 namespace App\Listeners; use App\Jobs\ProcessFirehosePost; use Revolution\Bluesky\Events\Firehose\FirehoseCommitMessage; class FirehosePostListener { public function handle(FirehoseCommitMessage $event): void { if ($event->collection !== 'app.bsky.feed.post') { return; } // 重い処理は Queue Job に委譲 ProcessFirehosePost::dispatch($event->did, $event->record, $event->action); } } ``` Source: [src/Console/WebSocket](https://github.com/invokable/laravel-bluesky/tree/main/src/Console/WebSocket) # Laravel Boost Custom Agent for GitHub Copilot CLI Source: https://kawax.biz/jp/packages/laravel-boost-copilot-cli Laravel Boost に GitHub Copilot CLI 用のカスタムエージェントを追加するパッケージ。Testbench 対応が特徴。 ## 概要 `revolution/laravel-boost-copilot-cli` は、[Laravel Boost](https://github.com/laravel/boost) の拡張として GitHub Copilot CLI 用のカスタムエージェントを追加するパッケージです。 Laravel Boost 本体にも `GitHub Copilot` はありますが、こちらは主に VS Code の GitHub Copilot 連携向けです。このパッケージは Copilot CLI 向けの導線を明確にし、特に Testbench を使うパッケージ開発で扱いやすい構成を提供します。 ## 要件 * PHP 8.2+ * Laravel 12+ * Laravel Boost 2.0+ * Copilot CLI 1.0.61+ ## 対応プラットフォーム * macOS * WSL(Native Windows も可。WSL 推奨) * Linux Laravel Sail も利用できます。先に `vendor/bin/sail up -d` を実行し、`copilot` コマンドは Sail の外側で実行します。 ## インストール ```shell theme={null} composer require revolution/laravel-boost-copilot-cli --dev ``` ## 使い方 ```shell theme={null} php artisan boost:install # "GitHub Copilot CLI" を選択 copilot ``` ガイドラインはカスタムインストラクションとして `.github/instructions/laravel-boost.instructions.md` に生成されます。 Copilot CLI はプロジェクトルートの `.github/mcp.json` を自動読み込みするため、追加設定は不要です。 ## カスタマイズ `config/boost.php` を公開することで、ガイドラインファイルやMCP設定ファイルの生成先パスを変更できます。 ```shell theme={null} php artisan vendor:publish --tag=boost-config ``` `config/boost.php` に `agents` を追加します。Copilot CLI を公式の Copilot(VS Code)と揃えるには `AGENTS.md` を指定します。逆に Copilot(VS Code)側を変更することもできます。 ```php theme={null} 'executable_paths' => [], 'agents' => [ // Copilot CLI 'copilot_cli' => [ 'guidelines_path' => 'AGENTS.md', 'mcp_config_path' => '.github/mcp.json', ], // Copilot(VS Code) 'copilot' => [ 'guidelines_path' => '.github/instructions/laravel-boost.instructions.md', 'mcp_config_path' => '.vscode/mcp.json', ], ], ``` `agents` はドキュメントに記載されていない機能のため、バージョンアップで変更される可能性があります。 ## Testbench 対応(パッケージ開発者向け) このパッケージは、Laravel Boost 公式ではカバーしづらい Testbench 前提の運用に対応しやすい構成を提供します。 ```shell theme={null} vendor/bin/testbench workbench:install ``` `post-autoload-dump` に `@build` を追加します。 ```json theme={null} { "scripts": { "post-autoload-dump": [ "@clear", "@prepare", "@build" ] } } ``` ```shell theme={null} vendor/bin/testbench boost:install ``` ## 関連リンク * GitHub: [invokable/laravel-boost-copilot-cli](https://github.com/invokable/laravel-boost-copilot-cli) * PhpStorm 向け関連パッケージ: [invokable/laravel-boost-phpstorm-copilot](https://github.com/invokable/laravel-boost-phpstorm-copilot) * Copilot SDK for Laravel: [invokable/laravel-copilot-sdk](https://github.com/invokable/laravel-copilot-sdk) # Laravel Boost Custom Agent for PhpStorm with GitHub Copilot Source: https://kawax.biz/jp/packages/laravel-boost-phpstorm-copilot PhpStorm の GitHub Copilot プラグイン向けに Laravel Boost のカスタムエージェントを追加するパッケージ。macOS/Windows/Linux/WSL 対応。 ## パッケージの概要 [revolution/laravel-boost-phpstorm-copilot](https://github.com/invokable/laravel-boost-phpstorm-copilot) は、Laravel Boost の拡張として **PhpStorm の GitHub Copilot プラグイン**向けのカスタムエージェントを追加するパッケージです。 `php artisan boost:install` の AI エージェント選択に `PhpStorm with GitHub Copilot` が追加されます。 `PhpStorm` は `PhpStorm Junie` 用の選択肢です。必ず `PhpStorm with GitHub Copilot` を選んでください。 ## 要件 * PHP 8.3 以上 * Laravel 12 以上 * Laravel Boost 2.0 以上 * PhpStorm に GitHub Copilot プラグインをインストール済み ## インストール ```shell theme={null} composer require revolution/laravel-boost-phpstorm-copilot --dev ``` ## 使い方 ```shell theme={null} php artisan boost:install ``` `AI Guidelines` / `Agent Skills` / `Boost MCP Server Configuration` から必要な機能を選択します。 `Which AI agents would you like to configure?` で `PhpStorm with GitHub Copilot` を選択します。 ## MCP 設定ファイルの保存先 | OS | パス | | - | - | | macOS, Linux | `~/.config/github-copilot/intellij/mcp.json` | | Windows | `%LOCALAPPDATA%\github-copilot\intellij\mcp.json` | ## 重要: プロジェクト切り替え時は再実行が必要 PhpStorm + GitHub Copilot プラグインでは、MCP 設定が**プロジェクト単位ではなくシステム全体**に保存されます。\ 設定ファイルには Laravel プロジェクトの**絶対パス**が入るため、プロジェクトを切り替えるたびに更新が必要です。 ```shell theme={null} php artisan boost:install --guidelines --skills --mcp --no-interaction ``` Boost 1.8 以降の `boost:update` では MCP 設定ファイルは更新されません。 ## WSL 対応 このパッケージは、**PhpStorm は Windows 側、PHP は WSL 側**で動かす構成をサポートします。 ### 要件 * `wslu` パッケージが必要 * 未導入なら `sudo apt install wslu` ### 仕組み * `WSL_DISTRO_NAME` で WSL 環境を検出 * `wslvar LOCALAPPDATA` で Windows 側の AppData パスを取得 * PowerShell 経由で `%LOCALAPPDATA%\github-copilot\intellij\mcp.json` を書き込み ### 注意事項 WSL 内で PhpStorm をリモート開発として使う場合はサポート対象外です。必要な場合は MCP ファイルを手動で設定してください。 ## Testbench 非対応と代替案 このパッケージは Testbench での利用をサポートしていません。\ パッケージ開発で Testbench を使う場合は、代わりに [laravel-boost-copilot-cli](https://github.com/invokable/laravel-boost-copilot-cli) を使ってください。 ## 関連リンク * [laravel-boost-phpstorm-copilot (GitHub)](https://github.com/invokable/laravel-boost-phpstorm-copilot) * [Laravel Boost (GitHub)](https://github.com/laravel/boost) * [GitHub Copilot plugin for PhpStorm](https://plugins.jetbrains.com/plugin/17718-github-copilot) # エージェントループ Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/agent-loop Copilot CLI が prompt 受信から session.idle までをどのように処理するかを説明します。 ## エージェントループ Copilot CLI がユーザーメッセージをどのように end-to-end で処理するか(プロンプト送信から `session.idle` まで)を説明します。 ## アーキテクチャ ```mermaid theme={null} graph LR App["Your App"] -->|send prompt| SDK["SDK Session"] SDK -->|JSON-RPC| CLI["Copilot CLI"] CLI -->|API calls| LLM["LLM"] LLM -->|response| CLI CLI -->|events| SDK SDK -->|events| App ``` **SDK** はトランスポート層です。JSON-RPC で **Copilot CLI** にプロンプトを送り、イベントをアプリに中継します。エージェント的なツール利用ループを実行し、タスク完了まで 1 回以上の LLM API 呼び出しをオーケストレーションするのは **CLI** 側です。 ## ツール利用ループ `session.send({ prompt })` を呼ぶと、CLI は次のループに入ります。 ```mermaid theme={null} flowchart TD A["User prompt"] --> B["LLM API call
(= one turn)"] B --> C{"toolRequests
in response?"} C -->|Yes| D["Execute tools
Collect results"] D -->|"Results fed back
as next turn input"| B C -->|No| E["Final text
response"] E --> F(["session.idle"]) style B fill:#1a1a2e,stroke:#58a6ff,color:#c9d1d9 style D fill:#1a1a2e,stroke:#3fb950,color:#c9d1d9 style F fill:#0d1117,stroke:#f0883e,color:#f0883e ``` モデルは各呼び出しで **会話履歴全体**(system prompt / user message / それまでの全ツール呼び出しと結果)を参照します。 **重要:** このループの 1 反復は LLM API 呼び出し 1 回と完全に一致し、イベントログ上では `assistant.turn_start` / `assistant.turn_end` の 1 ペアとして見えます。隠れた呼び出しはありません。 ## Turn(ターン)とは何か **Turn** は、LLM API 呼び出し 1 回とその結果のことです。 1. CLI が会話履歴を LLM に送る 2. LLM が応答する(ツール要求を含む場合あり) 3. ツール要求があれば CLI が実行する 4. `assistant.turn_end` が発行される 1 回のユーザーメッセージで、通常は **複数ターン** 発生します。例えば「このコードベースで X はどう動く?」のような質問では次のようになります。 | Turn | モデルの動作 | toolRequests? | | - | - | - | | 1 | `grep` と `glob` でコードベースを検索 | ✅ Yes | | 2 | 検索結果に基づいて特定ファイルを読む | ✅ Yes | | 3 | さらに深い文脈のために追加で読む | ✅ Yes | | 4 | 最終テキスト回答を生成 | ❌ No → loop ends | モデルは各ターンで「さらにツールを使うか」「最終回答に進むか」を判断します。各呼び出しは **蓄積済みの全コンテキスト**(過去のツール呼び出しと結果を含む)を見られるため、十分な情報があるかを判断できます。 ## 複数ターン時のイベントフロー ```mermaid theme={null} flowchart TD send["session.send({ prompt: "Fix the bug in auth.ts" })"] subgraph Turn1 ["Turn 1"] t1s["assistant.turn_start"] t1m["assistant.message (toolRequests)"] t1ts["tool.execution_start (read_file)"] t1tc["tool.execution_complete"] t1e["assistant.turn_end"] t1s --> t1m --> t1ts --> t1tc --> t1e end subgraph Turn2 ["Turn 2 — auto-triggered by CLI"] t2s["assistant.turn_start"] t2m["assistant.message (toolRequests)"] t2ts["tool.execution_start (edit_file)"] t2tc["tool.execution_complete"] t2e["assistant.turn_end"] t2s --> t2m --> t2ts --> t2tc --> t2e end subgraph Turn3 ["Turn 3"] t3s["assistant.turn_start"] t3m["assistant.message (no toolRequests)
"Done, here's what I changed""] t3e["assistant.turn_end"] t3s --> t3m --> t3e end idle(["session.idle — ready for next message"]) send --> Turn1 --> Turn2 --> Turn3 --> idle ``` ## 各ターンを誰が開始するか | Actor | Responsibility | | - | - | | **Your app** | `session.send()` で最初のプロンプトを送る | | **Copilot CLI** | ツール利用ループを実行し、ツール実行結果を次ターン入力として LLM に返す | | **LLM** | ツール要求して継続するか、最終応答を返して終了するかを決める | | **SDK** | イベントを中継するのみ。ループ制御はしない | CLI の挙動は機械的です(「モデルがツール要求 → 実行 → 再度モデル呼び出し」)。停止判定の意思決定者は **モデル** です。 ## `session.idle` と `session.task_complete` の違い この 2 つは完了シグナルですが、保証内容が大きく異なります。 ### `session.idle` * ツール利用ループ終了時に **必ず発行** される * **揮発的(ephemeral)**。ディスク永続化されず、セッション再開時に再生されない * 意味: 「エージェントは処理を止め、次のメッセージを受け付け可能」 * 信頼できる「完了」シグナルとしてはこちらを使う SDK の `sendAndWait()` はこのイベントを待機します。 ```typescript theme={null} // session.idle が発行されるまで待機 const response = await session.sendAndWait({ prompt: "Fix the bug" }); ``` ### `session.task_complete` * **任意発行**(モデルが明示的にシグナルした場合のみ) * **永続化** される(セッションイベントログに保存される) * 意味: 「エージェント自身が全体タスク達成と判断した」 * 任意の `summary` を含められる ```typescript theme={null} session.on("session.task_complete", (event) => { console.log("Task done:", event.data.summary); }); ``` ### Autopilot モード: CLI が `task_complete` を促す **autopilot mode**(headless / autonomous)では、CLI はモデルが `task_complete` を呼んだかを追跡します。ツール利用ループが `task_complete` なしで終わると、CLI は次の合成ユーザーメッセージを挿入してモデルに促します。 > *"You have not yet marked the task as complete using the task\_complete tool. If you were planning, stop planning and start implementing. You aren't done until you have fully completed the task."* これによりツール利用ループが実質的に再開します。モデルはこの促しを新しいユーザーメッセージとして受け取り、作業を続行します。促しには「早すぎる `task_complete` を呼ばないこと」も含まれます。 * 未解決の疑問があるなら呼ばない。判断して作業を続ける * エラーに遭遇しただけなら呼ばない。解決を試みる * 残タスクがあるなら呼ばない。先に完了させる autopilot では次の **2 段階完了メカニズム** になります。 1. モデルが summary 付きで `task_complete` を呼ぶ → CLI が `session.task_complete` を発行 → 完了 2. モデルが呼ばずに停止 → CLI が促す → モデルが継続または `task_complete` を呼ぶ ### `task_complete` が出ない理由 **interactive mode**(通常チャット)では、CLI は `task_complete` を促しません。モデルが省略することがあります。主な理由は次のとおりです。 * **会話型 Q\&A**: 質問に答えて終了し、離散的な「完了タスク」がない * **モデル判断**: `task_complete` を呼ばずに最終テキストを返す * **中断セッション**: モデルが完了点に達する前にセッションが終了する 一方で CLI は常に `session.idle` を発行します。これは意味論的シグナル(モデルが完了と判断)ではなく、機械的シグナル(ループ終了)だからです。 ### どちらを使うべきか | Use case | Signal | | - | - | | 「エージェントの処理終了を待つ」 | `session.idle` ✅ | | 「コーディングタスクが完了したことを知る」 | `session.task_complete`(best-effort) | | 「タイムアウト/エラー処理」 | `session.idle` + `session.error` ✅ | ## LLM 呼び出し回数を数える イベントログ上の `assistant.turn_start` / `assistant.turn_end` ペア数は、LLM API 呼び出し総数と一致します。計画・評価・完了確認の隠れた呼び出しはありません。 セッションのターン数確認例: ```bash theme={null} # セッションイベントログ内の turn 数をカウント grep -c "assistant.turn_start" ~/.copilot/session-state//events.jsonl ``` ## 関連ドキュメント * [ストリーミングイベント](/jp/packages/laravel-copilot-sdk/streaming-events) — 各イベントタイプのフィールドレベル参照 * [セッション再開](/jp/packages/laravel-copilot-sdk/resume) — セッション保存と再開 * [セッションフック](/jp/packages/laravel-copilot-sdk/hooks) — ループ内イベントのインターセプト(権限・ツール) # Laravel AI SDK連携 Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/ai-sdk Laravel Copilot SDK で実験的な Laravel AI SDK 連携を有効化し、テキスト生成とストリーミングを使う方法を説明します。 ## Laravel AI SDK連携 * 実験的な実装 * テキスト生成のみ対応。他の機能は未対応です。 Laravel AI SDK をインストールしている時だけ有効化されるオプトイン機能です。 ```shell theme={null} composer require laravel/ai php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider" ``` `config/ai.php` に設定を追加します。 ```php theme={null} // config/ai.php 'default' => 'copilot', 'providers' => [ 'copilot' => [ 'driver' => 'copilot', 'key' => '', ], ], ``` `agent()` ヘルパーでの使い方です。 ```php theme={null} use function Laravel\Ai\agent; $response = agent( instructions: 'You are an expert at software development.', )->prompt('Tell me about Laravel'); echo $response->text; ``` ストリーミングにも対応しています。`TextDelta` 以外は未実装です。 ```php theme={null} use Laravel\Ai\Streaming\Events\TextDelta; use function Laravel\Ai\agent; $stream = agent( instructions: 'You are an expert at software development.', )->stream('Tell me about Laravel'); foreach ($stream as $event) { if ($event instanceof TextDelta) { echo $event->delta; } } ``` 通常の Laravel AI SDK と同様に Agent クラスを作る使い方も可能ですが、一部機能のみの対応です。 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # Ask user Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/ask-user Laravel Copilot SDK の onUserInputRequest ハンドラーで、ask_user ツール経由のユーザー質問に応答します。 ## ユーザー入力リクエスト `onUserInputRequest` ハンドラーを設定すると、エージェントが `ask_user` ツールでユーザーに質問できます。 ```php theme={null} use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Types\UserInputRequest; use Revolution\Copilot\Types\UserInputResponse; Copilot::start(function (CopilotSession $session) { $response = $session->sendAndWait(prompt: 'ユーザーに好きな言語を聞いて'); dump($response->content()); }, config: [ 'model' => 'gpt-5', 'onUserInputRequest' => function (UserInputRequest $request): UserInputResponse { // $request->question - 質問内容 // $request->choices - 複数選択肢の配列(オプション) // $request->allowFreeform - 自由入力が許可されているか(デフォルト: true) dump("Question from agent: {$request->question}"); if ($request->choices) { dump('Choices: '.implode(', ', $request->choices)); } // ユーザーの回答を返す return new UserInputResponse( answer: 'PHP', wasFreeform: true, // 選択肢からではなく自由入力だったか ); }, ]); ``` ## UserInputRequest クラス エージェントからのユーザー入力リクエストです。 | プロパティ | 型 | 説明 | | - | - | - | | `question` | `string` | ユーザーに尋ねる質問 | | `choices` | `?array` | 複数選択の場合の選択肢(オプション) | | `allowFreeform` | `?bool` | 自由入力が許可されているか(デフォルト: `true`) | ## UserInputResponse クラス ユーザー入力リクエストへの応答です。 | プロパティ | 型 | 説明 | | - | - | - | | `answer` | `string` | ユーザーの回答 | | `wasFreeform` | `bool` | 回答が自由入力だったか(選択肢からではない場合 `true`) | ## 実践例 インタラクティブなコマンドで使う場合です。 ```php theme={null} use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Types\UserInputRequest; use Revolution\Copilot\Types\UserInputResponse; // Artisan コマンド内で Copilot::start(function (CopilotSession $session) use ($command) { $response = $session->sendAndWait(prompt: 'プロジェクトの設定を行います'); $command->info($response->content()); }, config: [ 'onUserInputRequest' => function (UserInputRequest $request) use ($command): UserInputResponse { if ($request->choices) { // 選択肢がある場合は choice メソッドを使用 $answer = $command->choice( $request->question, $request->choices, $request->choices[0] ?? null ); return new UserInputResponse(answer: $answer, wasFreeform: false); } // 自由入力 $answer = $command->ask($request->question); return new UserInputResponse(answer: $answer, wasFreeform: true); }, ]); ``` 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # Attachments Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/attachments Laravel Copilot SDK でファイル、ディレクトリ、選択範囲、Blob を添付してプロンプト送信する方法を説明します。 ## File attachments 以下のようにファイルかディレクトリを配列で指定します。 ```php theme={null} use Revolution\Copilot\Facades\Copilot; $attachments = [ [ 'type' => 'file', 'path' => '/path/to/file.php', 'displayName' => 'My File',// 省略可 ], [ 'type' => 'directory', 'path' => '/path/to/dir/', 'displayName' => 'dir',// 省略可 ], [ 'type' => 'selection', 'filePath' => '/path/to/file.php', 'displayName' => 'My File',// ここは省略不可 'selection' => [ 'start' => ['line' => 1, 'character' => 10], 'end' => ['line' => 5, 'character' => 10], ], 'text' => '...', ], [ 'type' => 'blob', 'data' => base64_encode(file_get_contents('/path/to/image.png')), 'mimeType' => 'image/png', 'displayName' => 'screenshot.png',// 省略可 ], ]; $response = Copilot::run(prompt: '...', attachments: $attachments); ``` こんなフォーマットを覚えるのは難しいので簡単に使えるようにヘルパーを用意しています。 ```php theme={null} use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Support\Attachment; $attachments = [ Attachment::file(path: '/path/to/file.php', displayName: 'My File'), Attachment::directory(path: '/path/to/dir/', displayName: 'dir'), Attachment::selection(filePath: '/path/to/file.php', displayName: 'My File', selection: ['start' => ['line' => 1, 'character' => 10], 'end' => ['line' => 5, 'character' => 10]], text: '...'), Attachment::blob(data: base64_encode(file_get_contents('/path/to/image.png')), mimeType: 'image/png', displayName: 'screenshot.png'), ]; $response = Copilot::run(prompt: '...', attachments: $attachments); ``` ## Blob attachments `blob` タイプは Base64 エンコードされたデータをインラインで添付するために使います。ディスクI/Oなしで画像などのバイナリデータを直接送信できます。 ```php theme={null} use Revolution\Copilot\Support\Attachment; // 画像ファイルをBase64エンコードして添付 $attachment = Attachment::blob( data: base64_encode(file_get_contents('/path/to/image.png')), mimeType: 'image/png', displayName: 'screenshot.png', ); ``` `selection` は公式 SDK にまだドキュメントがないので詳細は不明です。`github_reference` も公式情報が不足しているため今後対応予定です。 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # 認証 - GitHub Copilot SDK for Laravel Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/auth Laravel Copilot SDK の認証方式を理解し、ローカル開発・CI・サーバー実行・BYOKに適した方法を選択します。 ## 認証 Copilot SDKは複数の認証方法をサポートします。用途に合わせて選択してください。 ## 認証方法 | 方法 | 主な用途 | Copilotサブスクリプション | 備考 | | - | - | - | - | | GitHubログイン(CLI) | 開発・手動実行 | 必要 | `copilot` CLIのログイン情報を利用 | | OAuth GitHub App / PAT | ユーザーごとのトークン | 必要 | `github_token`で指定 | | 環境変数 | CI/CD・サーバー運用 | 必要 | `COPILOT_GITHUB_TOKEN` など | | BYOK(Bring Your Own Key) | 自前のモデルプロバイダー | 不要 | カスタムプロバイダー設定 | BYOKは [Custom providers](/en/packages/laravel-copilot-sdk/custom-providers) を参照。 ## GitHubログイン(CLI) `copilot` もしくは `gh` CLIでログイン済みの場合は、何も指定しなければ保存済みの認証情報が使われます。 ```php theme={null} use Revolution\Copilot\Facades\Copilot; $response = Copilot::run(prompt: 'Hello'); ``` ## OAuth GitHub App / PAT OAuthフローで取得したユーザーアクセストークン、またはFine-grained PATを`github_token`に渡します。 * `gho_` / `ghu_` / `github_pat_` を想定 * `ghp_`(Classic PAT)は非推奨 ```php theme={null} use Revolution\Copilot\Facades\Copilot; $config = array_merge(config('copilot'), [ 'github_token' => $user->github_token, ]); $response = Copilot::useStdio($config)->run(prompt: '...'); Copilot::stop(); ``` `github_token`を指定すると`use_logged_in_user`は自動で`false`になります。明示的に切り替えたい場合は次の通り。 ```php theme={null} $config = [ 'github_token' => $user->github_token, 'use_logged_in_user' => true, ]; ``` 詳細は [GitHub token](/en/packages/laravel-copilot-sdk/github-token) を参照。 `github_token`と`use_logged_in_user`はstdioモード専用です。TCPモードではCopilot CLI側で認証済みである必要があります。 ## 環境変数 環境変数でトークンを渡す場合は、CLIが自動的に検出します。優先順位は上から順です。 1. `COPILOT_GITHUB_TOKEN` 2. `GH_TOKEN` 3. `GITHUB_TOKEN` ```dotenv theme={null} COPILOT_GITHUB_TOKEN=github_pat_xxx ``` ```php theme={null} use Revolution\Copilot\Facades\Copilot; $response = Copilot::run(prompt: 'Hello'); ``` TCPモードではCLIサーバー側の環境変数を設定してください。 ## 認証の優先順位 SDKは以下の順で認証情報を解決します。 1. `github_token`(明示指定) 2. 環境変数トークン(`COPILOT_GITHUB_TOKEN` → `GH_TOKEN` → `GITHUB_TOKEN`) 3. CLIに保存されたOAuth認証情報 4. `gh` CLIの認証情報 ## 自動ログインを無効化 保存済みの認証情報を使いたくない場合は`use_logged_in_user`を`false`にします。 ```php theme={null} use Revolution\Copilot\Facades\Copilot; $response = Copilot::useStdio([ 'use_logged_in_user' => false, ])->run(prompt: 'Hello'); ``` `github_token`と同様にstdioモードのみ対応です。TCPモードではCopilot CLI側で認証済みである必要があります。 ## 認証状態の確認 `auth.getStatus`の結果を取得して確認できます。 ```php theme={null} use Revolution\Copilot\Facades\Copilot; $status = Copilot::client()->getAuthStatus(); if (! $status->isAuthenticated) { // 未認証 } ``` 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # 公式 SDK 互換レイヤーの使い方 Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/bare-usage Laravel Copilot SDK を公式 SDK スタイルで使う手順を、Laravel での推奨スタイルと併せて確認します。 ## 公式 SDK スタイルで使う このパッケージは、公式 SDK の使い方を再現した pure PHP スタイルにも対応しています。 そのうえで Laravel らしい開発体験を提供します。 このページは「Facade の内部で公式 SDK を再現しているため同じ使い方もできる」ことの説明です。 通常は Facade を使う方法を推奨します。 ```php theme={null} use Revolution\Copilot\Client; use Revolution\Copilot\Session; use Revolution\Copilot\Types\SessionEvent; $client = new Client([ 'cli_path' => 'copilot', 'cli_args' => [], 'cwd' => base_path(), 'log_level' => 'info', 'env' => null, ]); $client->start(); /** @var Session $session */ $session = $client->createSession([ 'model' => 'gpt-5', ]); $done = $session->on(function (SessionEvent $event) { if($event->isAssistantMessage()) { echo $event->content(); } }); $session->send(prompt: 'Tell me something about Laravel.'); $session->wait(timeout: 60.0); // True async が正式実装されるまでは wait() で待機します。 $done(); $session->disconnect(); $client->stop(); ``` 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # Copilot CLIをバンドル Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/bundle-cli Copilot CLI のバージョンを厳密に管理したい場合に、Laravel プロジェクトへ npm 依存としてバンドルする方法を説明します。 ## プロジェクトにCopilot CLIを含める Copilot CLI のバージョンを厳密に管理したい場合、npm パッケージとしてプロジェクトに含めることができます。 一般的にはデプロイ時に別途 Copilot CLI をインストールする方法で十分です。\ [Laravel Cloudでの使い方](/jp/packages/laravel-copilot-sdk/laravel-cloud) と [GitHub Actionsでの使い方](/jp/packages/laravel-copilot-sdk/github-actions) も参照してください。 ## 前提条件 Laravel プロジェクトに `package.json` があり、本番サーバーにも Node.js 実行環境があるため、npm パッケージをインストールできます。 ## セットアップ ### 1. npmパッケージをインストール `dependencies` に `@github/copilot` をインストールします。 ```shell theme={null} npm install @github/copilot ``` インストール後、以下のパスで Copilot CLI を直接実行できます。 ```shell theme={null} node node_modules/@github/copilot/npm-loader.js --version ``` Copilot CLI 1.0.64以降は`index.js`はプラットフォームごとのパッケージにのみ含まれているので`index.js`を使う場合は環境に合わせて使用してください。 ```shell theme={null} node node_modules/@github/copilot-linux-x64/index.js --version ``` ### 2. `.env` を設定 `COPILOT_CLI_PATH` に `npm-loader.js` までのフルパスを指定します。 ```dotenv theme={null} COPILOT_CLI_PATH=/path/to/project/node_modules/@github/copilot/npm-loader.js ``` `base_path()` を使って動的にパスを設定したい場合は `config/copilot.php` を直接編集します。 ```php theme={null} 'cli_path' => base_path('node_modules/@github/copilot/npm-loader.js'), ``` ## デプロイ 本番サーバーへデプロイする時は、アセットビルド後に `devDependencies` を除外して再インストールします。`@github/copilot` は `dependencies` にあるため残ります。 ```shell theme={null} npm install npm run build npm install --omit=dev ``` `npm install --omit=dev` により `devDependencies` のパッケージが削除され、`@github/copilot` を含む `dependencies` のパッケージだけが `node_modules` に残ります。 ## メリットと注意点 | メリット | 注意点 | | - | - | | CLI のバージョンを厳密に管理できる | `node_modules` のサイズが増加する | | `package.json` でバージョンを固定できる | CLI の更新は手動で行う必要がある | | チーム全員が同じバージョンを使える | Node.js 実行環境が必要 | ## 認証 認証方法はインストール方法に関係なく同じです。`.env` で `COPILOT_GITHUB_TOKEN` を設定します。 ```dotenv theme={null} COPILOT_GITHUB_TOKEN=your-token ``` 詳しくは [認証](/jp/packages/laravel-copilot-sdk/auth) を参照してください。 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # Cloud Sessions Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/cloud-sessions ローカルの Copilot CLI ではなく GitHub ホスト環境でセッションを実行する Cloud Sessions の使い方を説明します。 ## Cloud Sessions Cloud Sessions は、ローカルの Copilot CLI プロセスではなく GitHub 側のホスト環境でセッションを実行する機能です。Mission Control 上でタスクが予約され、クラウド側の `copilot-agent` が接続して処理を進めます。 通常の Remote Sessions は「ローカルで動くセッションを GitHub Web/モバイルから見えるようにする」機能です。実行場所自体を GitHub ホスト環境にしたい場合は Cloud Sessions を使います。 ## 前提条件 * ユーザーが Cloud Agent を利用できる Copilot 権限を持っている * GitHub トークン、またはログイン済み Copilot CLI ユーザーで認証できる * 可能なら GitHub リポジトリ情報を関連付ける * 組織ポリシーでクラウド実行とリモート閲覧が許可されている ## 基本的な使い方 `SessionConfig` の `cloud` に `CloudSessionOptions` を指定します。リポジトリ情報は SDK 型としては任意ですが、Mission Control とクラウドエージェントに文脈を渡すため指定することを推奨します。 ```php theme={null} use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Support\PermissionHandler; use Revolution\Copilot\Types\CloudSessionOptions; use Revolution\Copilot\Types\CloudSessionRepository; use Revolution\Copilot\Types\SessionConfig; Copilot::start(function (CopilotSession $session): void { $session->sendAndWait(prompt: 'READMEを要約して'); }, config: new SessionConfig( onPermissionRequest: PermissionHandler::approveSafety(), cloud: new CloudSessionOptions( repository: new CloudSessionRepository( owner: 'myorg', name: 'myrepo', branch: 'main', ), ), )); ``` 配列でも指定できます。 ```php theme={null} Copilot::run( prompt: 'このリポジトリのテスト方針を確認して', config: [ 'cloud' => [ 'repository' => [ 'owner' => 'myorg', 'name' => 'myrepo', 'branch' => 'main', ], ], ], ); ``` ## 最初のプロンプトを送るタイミング Cloud Session は 2 段階で初期化されます。`session.create` は Mission Control でタスクが予約された時点で戻りますが、クラウド側の `copilot-agent` が接続して `session.start` を発行するまで少し時間があります。 最初のプロンプトを確実に届けるには、先にイベントを購読し、`producer` が `copilot-agent` の `session.start` を確認してから送信してください。 ```php theme={null} use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Enums\SessionEventType; use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Support\PermissionHandler; use Revolution\Copilot\Types\CloudSessionOptions; use Revolution\Copilot\Types\CloudSessionRepository; use Revolution\Copilot\Types\SessionConfig; $config = new SessionConfig( streaming: true, onPermissionRequest: PermissionHandler::approveSafety(), cloud: new CloudSessionOptions( repository: new CloudSessionRepository(owner: 'myorg', name: 'myrepo'), ), ); Copilot::start(function (CopilotSession $session): void { $cloudAgentStarted = false; foreach ($session->stream(timeout: 30.0) as $event) { if ( $event->is(SessionEventType::SESSION_START) && $event->data('producer') === 'copilot-agent' ) { $cloudAgentStarted = true; break; } } if (! $cloudAgentStarted) { throw new RuntimeException('Cloud session did not become ready.'); } $session->sendAndWait(prompt: 'READMEを要約して'); }, config: $config); ``` 実アプリでは、キューや非同期処理側で `session.start` 確認後にプロンプトを送る構成にすると扱いやすいです。 ## Remote Sessions との違い | 機能 | 実行場所 | 主な用途 | | - | - | - | | Remote Sessions | ローカルまたは自前サーバー | Web/モバイルから既存セッションを閲覧・操作する | | Cloud Sessions | GitHub ホスト環境 | ユーザー端末や自前サーバーで Copilot CLI を動かさずに処理する | ## 注意点 * Cloud Sessions は権限と組織ポリシーの影響を受けます * `streaming: true` にすると `assistant.message_delta` などのリアルタイムイベントを受け取れます * 権限リクエストの扱いは通常セッションと同じです。Laravel 版の Facade 経由ではデフォルトで deny-all なので、必要に応じて `PermissionHandler::approveSafety()` などを指定してください ## 参考 * [Remote Sessions](/jp/packages/laravel-copilot-sdk/remote-sessions) * [SessionConfig](/jp/packages/laravel-copilot-sdk/session-config) * [ストリーミングイベント](/jp/packages/laravel-copilot-sdk/streaming-events) 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # Concurrency Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/concurrency Laravel Copilot SDK の stdio / TCP モードで Laravel Concurrency を使って並行実行する際の実践ポイントを説明します。 ## Laravel の Concurrency を使った並行実行 ## stdioモードの場合 それぞれでプロセスを起動するのは無駄ですがこの書き方でしか動きませんでした。Concurrency 内部では複雑なシリアライズをしているので SessionEvent クラスではなく `content()` の文字列だけを返します。 ```php theme={null} use Revolution\Copilot\Facades\Copilot; use Illuminate\Support\Facades\Concurrency; $prompt = 'Tell me something about Copilot.'; [$gpt5_response, $sonnet_response] = Concurrency::run([ fn () => Copilot::run($prompt, config: ['model' => 'gpt-5.2'])->content(), fn () => Copilot::run($prompt, config: ['model' => 'claude-sonnet-4.5'])->content(), ]); echo 'GPT-5 Response: '.$gpt5_response; echo 'Claude Sonnet Response: '.$sonnet_response; ``` ## TCPモードの場合 プロセスの起動がない分早そうですが実際にはあまり変わりませんでした。 Process ドライバーではどちらでも 60 秒でタイムアウトになることが多いです。CLI 環境専用 `fork` ドライバーでは片方だけ成功しました。例外で全部失敗にはなりません。`gpt-5` だけ失敗していたので `gpt-5.2` に変えたら両方成功しました。単純にモデルの応答時間の問題だったのかもしれません。 Concurrency は機能としては存在しますが実用できるかは不明です。時間がかかる処理はキューを使うのが基本です。 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # カスタムエージェント Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/custom-agents SessionConfig の customAgents で用途別サブエージェントを定義する方法を説明します。 ## カスタムエージェント カスタムエージェントを定義すると、用途ごとに役割を分けたサブエージェント構成を作れます。\ Laravel版では `SessionConfig` の `customAgents` を使って設定します。 複数のサブエージェントを並列に走らせるオーケストレーションについては [Fleet Mode](/jp/packages/laravel-copilot-sdk/fleet-mode) を参照してください。 ## 基本的な使い方 ```php theme={null} use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Types\SessionConfig; Copilot::start(function (CopilotSession $session) { $response = $session->sendAndWait(prompt: '認証周りの設計を調査して改善案を提案して'); dump($response->content()); }, config: new SessionConfig( customAgents: [ [ 'name' => 'researcher', 'displayName' => 'Research Agent', 'description' => 'コードを調査して要点を整理する', 'tools' => ['grep', 'glob', 'view'], 'prompt' => 'You are a research assistant. Analyze code and summarize findings.', ], [ 'name' => 'editor', 'displayName' => 'Editor Agent', 'description' => '必要最小限のコード変更を行う', 'tools' => ['view', 'edit', 'bash'], 'prompt' => 'You are a code editor. Make minimal, surgical changes only.', ], ], )); ``` 配列形式でも同様に指定できます。 ```php theme={null} Copilot::run( prompt: 'READMEを改善して', config: [ 'customAgents' => [ [ 'name' => 'docs-writer', 'description' => 'ドキュメント執筆を担当する', 'prompt' => 'Write clear technical documentation.', ], ], ], ); ``` ## 設定項目 | プロパティ | 型 | 必須 | 説明 | | - | - | - | - | | `name` | `string` | ✅ | エージェントの識別子 | | `displayName` | `string` | | 表示名 | | `description` | `string` | | ランタイムが委譲先を選ぶ際の説明 | | `tools` | `?array` | | 使用可能ツール。`null` / 省略時はすべて利用可能 | | `prompt` | `string` | ✅ | エージェント専用のシステムプロンプト | | `mcpServers` | `array` | | このエージェント専用のMCPサーバー設定 | | `infer` | `bool` | | 自動選択対象にするか(デフォルト: `true`) | | `skills` | `array` | | 起動時に読み込むスキル名のリスト | > `description` は具体的に書くほど、自動選択の精度が上がります。 ## セッション開始時に特定エージェントを選ぶ `agent` を指定すると、最初のターンから特定のカスタムエージェントを有効化できます。 ```php theme={null} use Revolution\Copilot\Types\SessionConfig; $config = new SessionConfig( customAgents: [ [ 'name' => 'researcher', 'prompt' => 'You analyze code and answer questions.', ], [ 'name' => 'editor', 'prompt' => 'You edit code with minimal changes.', ], ], agent: 'researcher', ); ``` ## 自動選択を無効にする(`infer: false`) 危険度の高い操作を行うエージェントは、自動選択を無効にして明示的に使う運用が安全です。 ```php theme={null} $config = new SessionConfig( customAgents: [ [ 'name' => 'dangerous-cleanup', 'description' => '不要ファイルを削除する', 'tools' => ['bash', 'edit', 'view'], 'prompt' => 'Remove dead code and unused files carefully.', 'infer' => false, ], ], ); ``` ## デフォルトエージェントからツールを隠す `defaultAgent.excludedTools` を使うと、メインエージェントだけから特定ツールを隠せます。\ そのツールを使えるカスタムエージェントへ委譲しやすくなります。 ```php theme={null} $config = new SessionConfig( tools: [...], defaultAgent: [ 'excludedTools' => ['analyze-codebase'], ], customAgents: [ [ 'name' => 'researcher', 'tools' => ['analyze-codebase'], 'prompt' => 'You perform deep codebase analysis.', ], ], ); ``` ## サブエージェントイベントを監視する サブエージェント実行中は `subagent.*` イベントが流れます。\ `onEvent` または `$session->on()` で監視できます。 ```php theme={null} use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Enums\SessionEventType; use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Types\SessionEvent; Copilot::start(function (CopilotSession $session) { $session->on(function (SessionEvent $event): void { if ($event->is(SessionEventType::SUBAGENT_STARTED)) { info('Sub-agent started', $event->toArray()); } if ($event->is(SessionEventType::SUBAGENT_COMPLETED)) { info('Sub-agent completed', $event->toArray()); } if ($event->is(SessionEventType::SUBAGENT_FAILED)) { info('Sub-agent failed', $event->toArray()); } }); $session->sendAndWait(prompt: 'このリポジトリの認証実装を調べて'); }); ``` ## ベストプラクティス * `researcher`(読み取り中心)と `editor`(変更担当)を分ける * `tools` は必要最小限にし、権限を絞る * `description` を具体的に書いて自動委譲精度を上げる * `subagent.failed` を拾ってリトライやフォールバック戦略を用意する ## 参考 * [SessionConfig](/jp/packages/laravel-copilot-sdk/session-config) * [SessionEvent](/jp/packages/laravel-copilot-sdk/session-event) * [Fleet Mode](/jp/packages/laravel-copilot-sdk/fleet-mode) * [MCP](/jp/packages/laravel-copilot-sdk/mcp) * [Tools](/jp/packages/laravel-copilot-sdk/tools) # カスタムプロバイダー Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/custom-providers Laravel Copilot SDK で OpenAI互換 API、Ollama、Azure OpenAI などの BYOK プロバイダーを設定する方法を説明します。 ## カスタムプロバイダー カスタムプロバイダー(BYOK - Bring Your Own Key)を使用すると、独自の API キーで OpenAI 互換の API エンドポイントに接続できます。Ollama などのローカルプロバイダーもサポートしています。 ## ProviderConfig `ProviderConfig` クラスは以下のプロパティを持ちます。 | プロパティ | 型 | 説明 | | - | - | - | | `baseUrl` | `string` | **必須**。APIエンドポイントのURL | | `type` | `string\|null` | プロバイダータイプ。`openai`(デフォルト)、`azure`、`anthropic` | | `wireApi` | `string\|null` | APIフォーマット(openai/azureのみ)。`completions`(デフォルト)または`responses` | | `apiKey` | `string\|null` | APIキー。Ollamaなどのローカルプロバイダーでは不要 | | `bearerToken` | `string\|null` | Bearer Token認証用。`apiKey`より優先される | | `azure` | `array\|null` | Azure固有のオプション。`['apiVersion' => '2024-10-21']`など | ## 基本的な使い方 カスタムプロバイダーを使用する場合、`model` パラメータは**必須**です。 ```php theme={null} use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Types\ProviderConfig; use Revolution\Copilot\Types\SessionConfig; $response = Copilot::run( prompt: 'Hello!', config: new SessionConfig( model: 'gpt-4', // カスタムプロバイダー使用時は必須 provider: new ProviderConfig( baseUrl: 'https://my-api.example.com/v1', apiKey: config('services.openai.key'), ), ), ); ``` 配列形式でも指定可能です。 ```php theme={null} $response = Copilot::run( prompt: 'Hello!', config: [ 'model' => 'gpt-4', 'provider' => [ 'baseUrl' => 'https://my-api.example.com/v1', 'apiKey' => config('services.openai.key'), ], ], ); ``` ## Ollama(ローカルプロバイダー) Ollama などのローカルプロバイダーでは `apiKey` は不要です。 ```php theme={null} use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Types\ProviderConfig; use Revolution\Copilot\Types\SessionConfig; $response = Copilot::run( prompt: 'Hello!', config: new SessionConfig( model: 'deepseek-coder-v2:16b', provider: new ProviderConfig( type: 'openai', baseUrl: 'http://localhost:11434/v1', ), ), ); ``` ## Azure OpenAI Azure OpenAI を使用する場合、以下の点に注意してください。 * `type` は必ず `azure` を指定(`openai` ではなく) * `baseUrl` にはホストのみを指定(`/openai/v1` などのパスは含めない) ```php theme={null} use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Types\ProviderConfig; use Revolution\Copilot\Types\SessionConfig; $response = Copilot::run( prompt: 'Hello!', config: new SessionConfig( model: 'gpt-4', provider: new ProviderConfig( type: 'azure', // Azureエンドポイントでは必ず'azure'を指定 baseUrl: 'https://my-resource.openai.azure.com', // ホストのみ apiKey: config('services.azure.openai_key'), azure: [ 'apiVersion' => '2024-10-21', ], ), ), ); ``` ## 重要な注意点 カスタムプロバイダー使用時は `model` パラメータが必須です。指定しないとエラーになります。 * Azureエンドポイント(`*.openai.azure.com`)では、必ず `type: 'azure'` を使用してください * `baseUrl` はホストのみを指定し、パスの構築は SDK が自動的に行います ## onListModels ハンドラー BYOK モードでカスタムプロバイダーを使用している場合、`client->listModels()` が CLI サーバーではなくカスタムハンドラーを呼び出すように設定できます。 `listModelsUsing()` メソッドを使って設定します。client は初期化されやすいため、`listModels()` の前に常に `listModelsUsing()` を設定してください。 ```php theme={null} use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Types\ModelInfo; $models = Copilot::client()->listModelsUsing(function (): array { // カスタムプロバイダーで利用可能なモデルを返す return [ ['id' => 'my-model-1', 'name' => 'My Model 1', 'version' => '1.0'], ['id' => 'my-model-2', 'name' => 'My Model 2', 'version' => '2.0'], ]; })->listModels(); ``` `null` を渡すとハンドラーを解除してデフォルトの CLI サーバー動作に戻ります。 ```php theme={null} // ハンドラーを解除してデフォルト動作(CLIサーバーに問い合わせ)に戻す $models = Copilot::client()->listModelsUsing(null)->listModels(); ``` `listModelsUsing()` が設定されている場合、`listModels()` は CLI サーバーへの接続を必要とせず、キャッシュも使用しません。 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # イベント Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/events Laravel Copilot SDK が発火する Laravel イベントを、ログ収集やデバッグ、非同期処理に活用する方法を説明します。 ## Laravel イベント [Events](https://github.com/invokable/laravel-copilot-sdk/tree/main/src/Events) のイベントクラスを各所に仕込んでいるのでログやデバッグに活用できます。 [フック](/jp/packages/laravel-copilot-sdk/hooks) でも似たことができますが、フックがない場所にも仕込めます。 ```text theme={null} src/ ├── Events/ │ ├── Client/ │ │ ├── ClientStarted.php │ │ ├── ToolCall.php │ │ └── PingPong.php │ ├── JsonRpc/ │ │ ├── MessageReceived.php │ │ ├── MessageSending.php │ │ └── ResponseReceived.php │ ├── Process/ │ │ └── ProcessStarted.php │ └── Session/ │ ├── CreateSession.php │ ├── MessageSend.php │ ├── MessageSendAndWait.php │ ├── SessionEventReceived.php │ └── ResumeSession.php ``` 例えば少し時間のかかる処理をキューや `defer` に投げることができます。この `Copilot::run()` の結果は直接受け取れませんが、代わりに `MessageSendAndWait` イベントのリスナーで受け取ることができます。 ```php theme={null} dispatch(fn() => Copilot::run('')); ``` ```php theme={null} use function Illuminate\Support\defer; defer(fn() => Copilot::run('')); ``` 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # テスト - GitHub Copilot SDK for Laravel Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/fake Laravel Copilot SDK の Copilot::fake()、アサーション、Stray request 防止を使ったテスト方法を説明します。 Laravel ならこういうテストコードが書けます。 ## Copilot::fake() `Copilot::fake()` は Copilot Facade から使う機能のためのモックです。他の機能はモックしません。 ```php theme={null} use Revolution\Copilot\Facades\Copilot; Copilot::fake('2'); // 常に'2'を返すモック $response = Copilot::run(prompt: '1 + 1'); // ここで実際にはCopilot CLIは呼ばれない // Pest expect($response->content())->toBe('2'); // PHPUnit $this->assertEquals('2', $response->content()); ``` `Copilot::start` で複数呼び出した場合。 ```php theme={null} use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Contracts\CopilotSession; Copilot::fake([ '*' => Copilot::sequence() ->push(Copilot::response('2')) ->push(Copilot::response('4')), ]); Copilot::start(function (CopilotSession $session) use (&$response1, &$response2) { $response1 = $session->sendAndWait(prompt: '1 + 1'); // '2'を返す $response2 = $session->sendAndWait(prompt: '2 + 2'); // '4'を返す }); expect($response1->content())->toBe('2'); ``` ## アサーション 特定のプロンプトが呼び出されたことを確認。 ```php theme={null} Copilot::assertPrompt('1 + *'); ``` プロンプトが呼び出されなかったことを確認。 ```php theme={null} Copilot::assertNotPrompt('1 + *'); ``` プロンプトが呼び出された回数を確認。 ```php theme={null} Copilot::assertPromptCount(3); ``` プロンプトが一度も呼び出されなかったことを確認。 ```php theme={null} Copilot::assertNothingSent(); ``` ## Prevent stray requests JSON-RPC リクエストをすべて防止します。呼び出した場合は例外 `Revolution\Copilot\Exceptions\StrayRequestException` が発生します。 ```php theme={null} Copilot::preventStrayRequests(); ``` 一部のコマンドだけは許可する場合。 ```php theme={null} Copilot::preventStrayRequests(allow: ['ping']); ``` 防止の停止。 ```php theme={null} Copilot::preventStrayRequests(false); ``` 防止するのは JSON-RPC リクエストだけなので `Client::start()` は防止しません。 ## 正常に動かないかもしれない使い方 Artisan コマンド内で Copilot を使っている場合、`fake()` でのモックは有効ですが、その後の `assertPrompt()` などが正しく動かない場合があります。使用例が少ないので調査中です。 ```php theme={null} use Revolution\Copilot\Facades\Copilot; Copilot::fake('Hello'); $this->artisan('copilot:hi'); Copilot::assertPrompt('Hi'); ``` ## shouldReceive() / expects() Mockery でお馴染みの `shouldReceive()` や `expects()` も当然使えます。 Facade を使えば自動的に対応している機能なので説明は省略します。 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # Fleet Mode Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/fleet-mode 1 つの親セッションから複数のサブエージェントを並列に動かす Fleet Mode の使い方を説明します。 ## Fleet Mode Fleet Mode は、1 つの親セッションから複数のサブエージェントを並列に動かすための実験的なオーケストレーション機能です。作業を独立した単位に分割できる場合に、親セッションが担当範囲を割り当て、各サブエージェントの結果を集約します。 Laravel 版では typed RPC layer から `session.fleet.start` を呼び出せます。 ## 向いている用途 * ファイル、パッケージ、言語 SDK ごとに分けられるリファクタリング * 複数モジュールを別々に調べる調査タスク * 差分、アラート、ドキュメントページ単位のレビュー * 移行作業のように、各担当範囲を独立して検証できるタスク 逆に、前のステップの具体的な結果が次のステップに必須な処理、同じファイルを複数ワーカーが編集する処理、小さな単発タスクには向きません。 ## Fleet Mode を開始する ```php theme={null} use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Types\Rpc\FleetStartRequest; Copilot::start(function (CopilotSession $session): void { $result = $session->rpc()->fleet()->start( new FleetStartRequest( prompt: 'docs/jpの各ページを独立して確認し、必要な更新だけ報告して', ), ); if ($result->started) { info('Fleet mode started'); } }); ``` 配列でも指定できます。 ```php theme={null} $result = $session->rpc()->fleet()->start([ 'prompt' => '各パッケージを独立してレビューし、最後にリスクをまとめて', ]); ``` `prompt` は任意です。省略するとランタイム側の Fleet Mode 指示だけで開始します。 ## Plan Mode から開始する Plan Mode UI を自前で扱っている場合は、`onExitPlanModeRequest` で `autopilot_fleet` を選ぶことで、承認済みプランから Fleet Mode へ進められます。 ```php theme={null} use Revolution\Copilot\Types\ExitPlanModeRequest; use Revolution\Copilot\Types\ExitPlanModeResult; use Revolution\Copilot\Types\SessionConfig; $config = new SessionConfig( onExitPlanModeRequest: function (ExitPlanModeRequest $request): ExitPlanModeResult { return new ExitPlanModeResult( approved: true, selectedAction: 'autopilot_fleet', ); }, ); ``` `autopilot` は単一の自律ワーカー、`interactive` はユーザーが継続的に関与する運用、`autopilot_fleet` は独立した作業単位を並列に進める運用に使います。 ## サブエージェントの協調 Fleet Mode は暗黙の共有メモリではなく、明示的な作業単位で協調します。親セッションがタスクを分割し、各サブエージェントは自分の担当範囲だけを処理し、完了またはブロック状態を返します。 作業単位を設計するときは以下を明確にしてください。 * 1 ワーカーが担当する範囲 * そのワーカーが編集してよいファイルやディレクトリ * 完了時に返すべき成果物や検証結果 * 他タスクとの依存関係 ## 注意点 * Fleet RPC は生成 RPC 層の実験的な機能です。利用する場合は Copilot CLI と SDK のバージョンを合わせてください * 競合しやすいファイル編集は避け、担当範囲を明確に分けてください * 親セッション側で結果を集約する前提で、各ワーカーの報告形式をプロンプトに含めると扱いやすくなります ## 参考 * [カスタムエージェント](/jp/packages/laravel-copilot-sdk/custom-agents) * [RPC](/jp/packages/laravel-copilot-sdk/rpc) * [ストリーミングイベント](/jp/packages/laravel-copilot-sdk/streaming-events) 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # はじめに - GitHub Copilot SDK for Laravel Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/getting-started Copilot を使う最初の Laravel コマンドを作り、Laravel Copilot SDK の基本ワークフローを学びます。 ## Copilot 対応の Laravel アプリを最初に作る このガイドでは、Laravel Copilot SDK を使ってコマンドラインアシスタントを作ります。 1 つのプロンプト送信から始めて、セッション、イベント、ツールへ進みます。 ## 前提条件 開始前に次を確認してください。 * GitHub Copilot CLI をインストールし、認証済みであること * PHP `8.4+` * Laravel `13.x` CLI が利用可能か確認します。 ```bash theme={null} copilot --version ``` ## SDK をインストールする Composer でパッケージをインストールします。 ```bash theme={null} composer require revolution/laravel-copilot-sdk ``` 必要に応じて設定を公開します。 ```bash theme={null} php artisan vendor:publish --tag=copilot-config ``` 必要なら `.env` で CLI のパスを設定します。 ```dotenv theme={null} COPILOT_CLI_PATH=copilot ``` ## 最初のメッセージを送る コマンドを作成し、`Copilot::run()` を呼び出します。 ```php theme={null} info($response->content()); } } ``` コマンドを実行します。 ```bash theme={null} php artisan copilot:demo ``` ## セッションでコンテキストを維持する 1 つの会話で複数のプロンプトを使う場合は `Copilot::start()` を使います。 ```php theme={null} use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Facades\Copilot; Copilot::start(function (CopilotSession $session) { $response = $session->sendAndWait(prompt: 'What is 2 + 2?'); $this->info('Answer: '.$response->content()); $response = $session->sendAndWait(prompt: 'Now multiply that by 3'); $this->info('Answer: '.$response->content()); }); ``` ## セッションイベントを処理する `on()` でイベントハンドラーを登録し、アシスタントのメッセージや失敗を確認します。 ```php theme={null} use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Types\SessionEvent; Copilot::start(function (CopilotSession $session) { $session->on(function (SessionEvent $event): void { if ($event->isAssistantMessage()) { $this->info($event->content()); } elseif ($event->failed()) { $this->error($event->errorMessage() ?? 'Unknown error'); } }); $session->sendAndWait(prompt: 'Tell me a short Laravel joke'); }); ``` ## カスタムツールを追加する JSON スキーマとハンドラーを使ってツールを定義します。 ```php theme={null} use Illuminate\JsonSchema\JsonSchema; use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Types\SessionConfig; use Revolution\Copilot\Types\Tool; $parameters = JsonSchema::object([ 'topic' => JsonSchema::string() ->description('Topic to look up') ->required(), ])->toArray(); $config = new SessionConfig( tools: [ Tool::define( name: 'lookup_fact', description: 'Returns a fact for a topic.', parameters: $parameters, handler: function (array $params): array { $topic = $params['topic'] ?? ''; return [ 'textResultForLlm' => "Fact for {$topic}", 'resultType' => 'success', 'sessionLog' => "lookup_fact: {$topic}", 'toolTelemetry' => [], ]; }, ), ], ); Copilot::start(function (CopilotSession $session) { $response = $session->sendAndWait( prompt: 'Use lookup_fact to tell me something about Laravel.' ); $this->info($response->content()); }, config: $config); ``` ## 対話型アシスタントを作る セッション、イベント、ツールをまとめると、対話型の Artisan コマンドを作成できます。 ```php theme={null} 'A popular general-purpose scripting language for web development.', 'Laravel' => 'A web application framework with expressive, elegant syntax.', 'Composer' => 'Dependency manager for PHP.', ]; $parameters = JsonSchema::object([ 'topic' => JsonSchema::string() ->description('Topic to look up') ->required(), ])->toArray(); $config = new SessionConfig( tools: [ Tool::define( name: 'lookup_fact', description: 'Returns a fun fact about a given topic.', parameters: $parameters, handler: function (array $params) use ($facts) { $topic = $params['topic'] ?? ''; $fact = $facts[$topic] ?? "No fact available for {$topic}."; return new ToolResultObject( textResultForLlm: $fact, resultType: 'success', sessionLog: "lookup_fact: {$topic}", toolTelemetry: [], ); }, ), ], ); Copilot::start(function (CopilotSession $session) { info('Copilot assistant'); info("Session: {$session->id()}"); info("Try: Use lookup_fact to tell me about Laravel"); $session->on(function (SessionEvent $event): void { if ($event->isAssistantMessage()) { note($event->content()); } elseif ($event->failed()) { error($event->errorMessage() ?? 'Unknown error'); } }); while (true) { $prompt = text( label: 'You', placeholder: 'Ask me anything...', required: true, hint: 'Ctrl+C to exit', ); spin( callback: fn () => $session->sendAndWait($prompt), message: 'Thinking...', ); echo "\n"; } }, config: $config); } } ``` コマンドを実行します。 ```bash theme={null} php artisan copilot:assistant ``` ## ツールの仕組み ツールを定義するときは次の 3 点を指定します。 1. ツールが何をするか。 2. どのパラメーターを受け取るか。 3. どのハンドラーコードを実行するか。 Copilot はユーザー入力に応じてツール呼び出しを判断し、SDK がハンドラーを実行して結果を返します。 ## 次に試す機能 ### MCP サーバーを接続する ```php theme={null} use Revolution\Copilot\Types\SessionConfig; $config = new SessionConfig( mcpServers: [ 'github' => [ 'type' => 'http', 'url' => 'https://api.githubcopilot.com/mcp/', ], ], ); ``` ### カスタムエージェントを作る ```php theme={null} use Revolution\Copilot\Types\SessionConfig; $config = new SessionConfig( customAgents: [ [ 'name' => 'pr-reviewer', 'displayName' => 'PR Reviewer', 'description' => 'Reviews pull requests for best practices', 'prompt' => 'You are an expert code reviewer. Focus on security, performance, and maintainability.', ], ], ); ``` ### システムメッセージをカスタマイズする ```php theme={null} use Revolution\Copilot\Types\SessionConfig; use Revolution\Copilot\Types\SystemMessageConfig; $config = new SessionConfig( systemMessage: new SystemMessageConfig( content: 'You are a helpful assistant for our engineering team. Always be concise.', ), ); ``` ## 外部 CLI サーバーに接続する Copilot CLI をサーバーモードで起動します。 ```bash theme={null} copilot --headless --port 4321 ``` 次に `.env` で接続先 URL を設定します。 ```dotenv theme={null} COPILOT_URL=tcp://127.0.0.1:4321 ``` `COPILOT_URL` を設定すると、SDK は新しい CLI プロセスを起動せず、既存のサーバーに接続します。 ## テレメトリーと可観測性 `config/copilot.php` でテレメトリーを設定します。 ```php theme={null} 'telemetry' => [ 'otlpEndpoint' => 'http://localhost:4318', ], ``` または直接設定します。 ```php theme={null} use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Types\TelemetryConfig; Copilot::useStdio([ 'telemetry' => new TelemetryConfig( otlpEndpoint: 'http://localhost:4318', ), ]); ``` ## さらに学ぶ * [認証](/jp/packages/laravel-copilot-sdk/auth) * [MCP](/jp/packages/laravel-copilot-sdk/mcp) * [カスタムプロバイダー](/en/packages/laravel-copilot-sdk/custom-providers) * [テレメトリー](/en/packages/laravel-copilot-sdk/telemetry) * [SessionConfig](/jp/packages/laravel-copilot-sdk/session-config) * [SessionEvent](/en/packages/laravel-copilot-sdk/session-event) * [Laravel Copilot SDK 概要](/jp/packages/laravel-copilot-sdk) * [公式 SDK リポジトリ](https://github.com/github/copilot-sdk) ## 次のステップ ローカル環境と CI に適した認証方式を選びます。 モデル、フック、MCP サーバー、実行時の挙動を設定します。 Laravel 固有のイベントヘルパーとライフサイクル処理を理解します。 明示的な承認でツールを実行する権限フローを確認します。 Copilot からアプリケーションコードを呼び出します。 MCP サーバーの既成ツールを接続します。 ここまでで、単発プロンプトからツール連携セッションまでの Laravel Copilot SDK の中核フローを一通り確認できました。 参考: [Laravel Copilot SDK README](https://github.com/invokable/laravel-copilot-sdk/blob/main/README.md)、[Laravel Copilot SDK Getting Started](https://github.com/invokable/laravel-copilot-sdk/blob/main/docs/getting-started.md)、[GitHub Copilot SDK](https://github.com/github/copilot-sdk) # GitHub Actions - GitHub Copilot SDK for Laravel Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/github-actions Laravel Copilot SDK を GitHub Actions で使うための Copilot CLI インストールとトークン認証設定を説明します。 ## GitHub Actions での使い方 GitHub Actions で Artisan コマンドを実行してその中で Copilot CLI を使う例です。 GitHub Actions では stdio モードで使うのが一般的です。 ## `copilot` コマンドを直接インストールする場合 ```yaml theme={null} - name: Install Copilot CLI run: | curl -fsSL https://gh.io/copilot-install | bash copilot version - name: Run command run: php artisan copilot:ping env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} ``` * `copilot-install` を使ってインストールします。path は `copilot` のまま追加の設定不要で使えます。 * `COPILOT_GITHUB_TOKEN` で認証します。`GH_TOKEN` や `GITHUB_TOKEN` など Copilot CLI がサポートしている認証方法ならなんでもよいです。 * トークンは `Copilot Requests` の権限が必要なのでデフォルトの `GITHUB_TOKEN` ではおそらく使えません。 ## `gh` コマンドを使う場合 GitHub CLI 2.86.0 で Copilot のインストール機能が追加されました。普通の GitHub Actions 環境では `gh` コマンドがプリインストールされているので追加のインストール不要で使えます。おそらく最初に `gh copilot ...` コマンドが実行された時に Copilot が自動でインストールされるので `gh copilot version` だけ実行します。 > gh version 2.86.0 (2026-01-21) > [https://github.com/cli/cli/releases/tag/v2.86.0](https://github.com/cli/cli/releases/tag/v2.86.0) > ✓ Copilot CLI installed successfully ```yaml theme={null} - name: Install Copilot CLI run: | gh --version gh copilot version - name: Run command run: php artisan copilot:ping env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} COPILOT_CLI_PATH: "gh copilot" ``` `COPILOT_CLI_PATH` は `gh copilot` の時のみ特別な対応をしているので、`gh copilot` 以外の設定では動きません。 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # GitHubトークン Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/github-token Laravel Copilot SDK でユーザーごとの GitHub トークンを実行時に切り替える方法を説明します。 ## 認証するGitHubトークンの実行時切り替え Socialite などを使って認証したユーザーごとの GitHub トークンで Copilot CLI を実行する方法です。 Personal Access Token では `Copilot Requests` の権限が必要です。Socialite のトークンでも同様の要件になるはずです。 Socialite から使えるトークンを取得できない場合は、PAT を直接入力してもらう方式にします。 この方法は stdio モードでのみ利用できます。 TCP モードは Copilot CLI プロセスを起動したまま使うため、実行時に切り替えできません。 TCP モード(`cli_url`)で `github_token` や `use_logged_in_user` を指定するとエラーになります。 下記の「セッションごとのGitHubトークン(v0.3.0+)」は TCP モードでも利用可能です。 ## `github_token` オプション SDK の公式オプションです。トークンは環境変数 `COPILOT_SDK_AUTH_TOKEN` 経由で CLI に渡されます。 ```php theme={null} use Revolution\Copilot\Facades\Copilot; $config = array_merge( config('copilot'), [ 'github_token' => $user->github_token, ] ); $response = Copilot::useStdio($config)->run(prompt: '...'); Copilot::stop(); // 念のためユーザートークンを持ったクライアントを破棄 ``` ## `use_logged_in_user` オプション `github_token` を指定すると自動的に `use_logged_in_user` は `false` になります。 これにより CLI は保存済み OAuth トークンや `gh` CLI 認証を使わず、明示的に渡したトークンだけを使います。 明示的に `use_logged_in_user` を `true` にすることもできます。 ```php theme={null} $config = [ 'github_token' => $user->github_token, 'use_logged_in_user' => true, // 明示的に有効化 ]; ``` `github_token` なしで `use_logged_in_user` を `false` にすると、`--no-auto-login` フラグが追加され、CLI は自動ログインを行いません。 ## セッションごとのGitHubトークン(v0.3.0+) `SessionConfig` の `gitHubToken` フィールドを使うと、同一 CLI プロセス内でセッションごとに異なる GitHub トークンを指定できます(マルチテナント対応)。 このトークンは `session.create` JSON-RPC のパラメーターとして渡されるため、**stdio モードと TCP モードの両方で利用可能**です。CLI がすでに起動している TCP モードでも、セッション単位でトークンを切り替えられます。 ```php theme={null} use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Types\SessionConfig; $response = Copilot::run( prompt: '...', config: new SessionConfig(gitHubToken: $user->github_token), ); ``` TCP モードでの使用例: ```php theme={null} // CLIサーバーは起動済み(TCPモード) // セッションごとに異なるユーザートークンを渡せる $response = Copilot::useTcp(url: 'tcp://127.0.0.1:12345')->run( prompt: '...', config: new SessionConfig(gitHubToken: $user->github_token), ); ``` クライアントレベルの `github_token`(CLI プロセス起動時のトークン)とは別に機能します。 セッション単位でユーザートークンを切り替えたい場合に使用してください。 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # セッションフック Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/hooks SessionHooks で Copilot セッションのライフサイクルに処理を差し込み、実行制御や監査、復旧ロジックを実装します。 ## セッションフック `hooks` を使うと、Copilot セッションの各ライフサイクルで処理を差し込めます。\ ツール実行制御、監査ログ、プロンプト補強、エラーハンドリングなどを、コア実装を変更せずに追加できます。 ## フックの流れ ```mermaid theme={null} flowchart LR A[Session starts] -->|onSessionStart| B[User prompt] B -->|onUserPromptSubmitted| C[Pre tool] C -->|onPreToolUse| D[Tool execution] D -->|onPostToolUse| E{Continue?} E -->|yes| C E -->|no| F[Session ends] F -->|onSessionEnd| G((Done)) C -. error .-> H[onErrorOccurred] D -. error .-> H ``` ## 基本的な使い方 ```php theme={null} use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Types\SessionHooks; use Revolution\Copilot\Types\Hooks\ErrorOccurredHookInput; use Revolution\Copilot\Types\Hooks\ErrorOccurredHookOutput; use Revolution\Copilot\Types\Hooks\PostToolUseHookInput; use Revolution\Copilot\Types\Hooks\PostToolUseHookOutput; use Revolution\Copilot\Types\Hooks\PreToolUseHookInput; use Revolution\Copilot\Types\Hooks\PreToolUseHookOutput; use Revolution\Copilot\Types\Hooks\SessionEndHookInput; use Revolution\Copilot\Types\Hooks\SessionEndHookOutput; use Revolution\Copilot\Types\Hooks\SessionStartHookInput; use Revolution\Copilot\Types\Hooks\SessionStartHookOutput; use Revolution\Copilot\Types\Hooks\UserPromptSubmittedHookInput; use Revolution\Copilot\Types\Hooks\UserPromptSubmittedHookOutput; Copilot::start(function (CopilotSession $session) { $response = $session->sendAndWait(prompt: 'READMEの要点をまとめて'); dump($response->content()); }, config: [ 'model' => 'gpt-5', 'hooks' => new SessionHooks( onSessionStart: function (SessionStartHookInput $input): ?SessionStartHookOutput { return new SessionStartHookOutput( additionalContext: "Project root: {$input->cwd}", ); }, onUserPromptSubmitted: function (UserPromptSubmittedHookInput $input): ?UserPromptSubmittedHookOutput { if (str_starts_with($input->prompt, '/fix')) { return new UserPromptSubmittedHookOutput( modifiedPrompt: '現在のエラーを修正して、変更点を要約して。', ); } return null; }, onPreToolUse: function (PreToolUseHookInput $input): ?PreToolUseHookOutput { $blocked = ['bash', 'shell', 'delete_file']; if (in_array($input->toolName, $blocked, true)) { return new PreToolUseHookOutput( permissionDecision: 'deny', permissionDecisionReason: "{$input->toolName} はこの環境では許可されていません", ); } return new PreToolUseHookOutput(permissionDecision: 'allow'); }, onPostToolUse: function (PostToolUseHookInput $input): ?PostToolUseHookOutput { if ($input->toolName === 'read_file') { return new PostToolUseHookOutput( additionalContext: '必要なら関連ファイルも探索して比較してください。', ); } return null; }, onErrorOccurred: function (ErrorOccurredHookInput $input): ?ErrorOccurredHookOutput { if ($input->errorContext === 'model_call' && $input->recoverable) { return new ErrorOccurredHookOutput( errorHandling: 'retry', retryCount: 2, userNotification: '一時的なモデルエラーのためリトライします。', ); } return null; }, onSessionEnd: function (SessionEndHookInput $input): ?SessionEndHookOutput { if ($input->reason !== 'complete') { return new SessionEndHookOutput( sessionSummary: "Session ended with reason: {$input->reason}", ); } return null; }, ), ]); ``` ## 利用可能なフック | Hook | 発火タイミング | 主な用途 | | - | - | - | | `onSessionStart` | セッション開始(`new` / `resume` / `startup`) | 初期コンテキスト注入、設定上書き | | `onUserPromptSubmitted` | ユーザープロンプト送信時 | プロンプト補強、テンプレート展開、入力フィルタ | | `onPreToolUse` | ツール実行前 | 許可/拒否/要確認、引数改変、出力抑制 | | `onPostToolUse` | ツール実行後 | 結果改変、機密情報マスク、監査ログ | | `onErrorOccurred` | セッション内エラー発生時 | リトライ、通知、エラー分類 | | `onSessionEnd` | セッション終了時 | クリーンアップ、メトリクス、終了サマリ | `null` を返すとデフォルト動作が継続されます。 ## 代表的なユースケース ### 1) Permission control(実行制御) * `onPreToolUse` で許可ツールを allow-list 方式にする * 破壊的操作は `permissionDecision: 'ask'` で人間承認 * `permissionDecisionReason` で拒否理由を明示 * `toolName` の候補は [Tools](/jp/packages/laravel-copilot-sdk/tools) の一覧で確認する(例: `view`, `glob`, `bash`) ### 2) Auditing / compliance(監査) * ライフサイクルフックを組み合わせて監査イベントを収集 * 収集データはセッションID単位で永続化 ### 3) Prompt enrichment(入力補強) * `onSessionStart` でプロジェクト情報(言語、FW、規約)を `additionalContext` に追加 * `onUserPromptSubmitted` でショートカット(`/fix`, `/test`)を展開 ### 4) Result filtering(結果整形) * `onPostToolUse` で API key / token / password などをマスク * 長すぎる結果は要約化し、必要時のみ詳細を返す ### 5) Error recovery(障害復旧) * `onErrorOccurred` で `model_call` かつ `recoverable=true` のときだけ `retry` * 非回復系は `userNotification` で利用者向けに簡潔に通知 ### 6) Session metrics(計測) * `onSessionStart` で開始時刻を記録 * `onPreToolUse` / `onUserPromptSubmitted` でカウンタ更新 * `onSessionEnd` で所要時間・ツール回数・終了理由を出力 ## Hook input / output 型 ### 共通入力(`BaseHookInput`) | プロパティ | 型 | 説明 | | - | - | - | | `timestamp` | `int` | フック発火時刻(Unix ms) | | `cwd` | `string` | 現在の作業ディレクトリ | ### `PreToolUseHookInput` | プロパティ | 型 | 説明 | | - | - | - | | `toolName` | `string` | 実行予定ツール名 | | `toolArgs` | `mixed` | 実行予定引数 | ### `PreToolUseHookOutput` | プロパティ | 型 | 説明 | | - | - | - | | `permissionDecision` | `?string` | `allow` / `deny` / `ask` | | `permissionDecisionReason` | `?string` | 拒否/確認時の理由 | | `modifiedArgs` | `mixed` | 上書き後の引数 | | `additionalContext` | `?string` | 追加コンテキスト | | `suppressOutput` | `?bool` | ツール出力を抑制 | ### `PostToolUseHookInput` | プロパティ | 型 | 説明 | | - | - | - | | `toolName` | `string` | 実行済みツール名 | | `toolArgs` | `mixed` | 実行時引数 | | `toolResult` | `ToolResultObject\|array` | ツール結果 | ### `PostToolUseHookOutput` | プロパティ | 型 | 説明 | | - | - | - | | `modifiedResult` | `ToolResultObject\|array\|null` | 改変後の結果 | | `additionalContext` | `?string` | 追加コンテキスト | | `suppressOutput` | `?bool` | ツール結果表示を抑制 | ### `UserPromptSubmittedHookInput` | プロパティ | 型 | 説明 | | - | - | - | | `prompt` | `string` | ユーザー入力プロンプト | ### `UserPromptSubmittedHookOutput` | プロパティ | 型 | 説明 | | - | - | - | | `modifiedPrompt` | `?string` | 改変後プロンプト | | `additionalContext` | `?string` | 補足コンテキスト | | `suppressOutput` | `?bool` | 応答表示の抑制 | ### `SessionStartHookInput` | プロパティ | 型 | 説明 | | - | - | - | | `source` | `string` | `startup` / `resume` / `new` | | `initialPrompt` | `?string` | 初期プロンプト | ### `SessionStartHookOutput` | プロパティ | 型 | 説明 | | - | - | - | | `additionalContext` | `?string` | セッション初期コンテキスト | | `modifiedConfig` | `?array` | セッション設定の部分上書き | ### `SessionEndHookInput` | プロパティ | 型 | 説明 | | - | - | - | | `reason` | `string` | `complete` / `error` / `abort` / `timeout` / `user_exit` | | `finalMessage` | `?string` | 最終メッセージ | | `error` | `?string` | 終了時エラー | ### `SessionEndHookOutput` | プロパティ | 型 | 説明 | | - | - | - | | `suppressOutput` | `?bool` | 最終出力の抑制 | | `cleanupActions` | `?array` | 実行したクリーンアップ情報 | | `sessionSummary` | `?string` | セッション要約 | ### `ErrorOccurredHookInput` | プロパティ | 型 | 説明 | | - | - | - | | `error` | `string` | エラーメッセージ | | `errorContext` | `?string` | `model_call` / `tool_execution` / `system` / `user_input` のいずれか | | `recoverable` | `bool` | 回復可能か | ### `ErrorOccurredHookOutput` | プロパティ | 型 | 説明 | | - | - | - | | `suppressOutput` | `?bool` | エラー表示の抑制 | | `errorHandling` | `?string` | `retry` / `skip` / `abort` | | `retryCount` | `?int` | リトライ回数 | | `userNotification` | `?string` | 利用者への通知文 | ## ToolResultObject ツール実行結果の標準オブジェクトです。 | プロパティ | 型 | 説明 | | - | - | - | | `textResultForLlm` | `?string` | LLM に渡すテキスト結果 | | `resultType` | `?string` | `success` / `failure` / `rejected` / `denied` | | `resultForAssistant` | `?array` | アシスタント向け結果データ | ## ベストプラクティス 1. 重い同期処理はフック内で直接実行しすぎない。必要なら非同期化する。 2. 変更不要なときは `null` を返してデフォルト動作に任せる。 3. `permissionDecision` は可能な限り明示する。 4. クリティカルエラーは抑制しすぎず、ログ/通知経路を持つ。 5. セッション単位の状態は session id ベースで管理し、`onSessionEnd` で掃除する。 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # GitHub Copilot SDK for Laravel Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/index Laravel Copilot SDK が提供する内容を理解し、セットアップ・セッション・ツール・実行時設定の主要ガイドへ移動します。 GitHub Copilot SDK for Laravel は、公式の [GitHub Copilot SDK](https://github.com/github/copilot-sdk) のワークフローを Laravel アプリケーションで使えるようにするコミュニティ SDK です。 Artisan コマンド、コントローラー、ジョブなど、Laravel のさまざまな実行コンテキストから Copilot を呼び出せます。 ## Laravel 固有のポイント このパッケージは、SDK 互換レイヤーの上に Laravel ネイティブな使い勝手を追加します。 * `Copilot::run()` と `Copilot::start()` を中心にした Facade ベースの API * `config/copilot.php` と `.env` を使う Laravel スタイルの設定 * Laravel Prompts、キュー、コンソール UX への統合ポイント * `Copilot::fake()` やプロンプトアサーションなどのテストユーティリティ ## 使用例 ### 単一プロンプトを実行 ```php theme={null} use Revolution\Copilot\Facades\Copilot; $response = Copilot::run(prompt: 'Tell me something about Laravel.'); dump($response->content()); ``` ### 1つのセッションで複数プロンプトを実行 ```php theme={null} use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Facades\Copilot; $content = Copilot::start(function (CopilotSession $session) { dump('Starting Copilot session: '.$session->id()); $response = $session->sendAndWait(prompt: 'Tell me something about PHP.'); dump($response->content()); $response = $session->sendAndWait(prompt: 'Tell me something about Laravel.'); dump($response->content()); return $response->content(); }); dump($content); ``` ### `copilot()` ヘルパー ```php theme={null} use Revolution\Copilot\Contracts\CopilotSession; use function Revolution\Copilot\copilot; // プロンプトを指定した場合はCopilot::run()と同じ $response = copilot('Tell me something about Laravel.'); // クロージャを指定した場合はCopilot::start()と同じ copilot(function (CopilotSession $session) { $response = $session->sendAndWait(prompt: 'Tell me something about PHP.'); }); // 何も渡さない場合はFacadeと同じ dump(copilot()->client()->ping()); ``` ## ここから始める Copilot を使う Laravel アプリの最初の一歩を進めます。 GitHub ログイン、トークン認証、BYOK ワークフローを設定します。 ツール、モデル、MCP サーバー、セッション動作を制御します。 Laravel 固有のイベントヘルパーとライフサイクル処理を理解します。 明示的な承認でツールを実行する権限フローを確認します。 Copilot から呼び出せるカスタムツールを定義します。 逐次出力とライブ応答の扱い方を学びます。 MCP サーバーを接続して機能を拡張します。 Laravel Cloud 環境で SDK を動かします。 公式 SDK と同じ bare API を Laravel から使う方法を確認します。 ## 公式リポジトリ * Laravel パッケージ: [invokable/laravel-copilot-sdk](https://github.com/invokable/laravel-copilot-sdk) * 公式 SDK: [github/copilot-sdk](https://github.com/github/copilot-sdk) # Laravel Cloud - GitHub Copilot SDK for Laravel Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/laravel-cloud Laravel Copilot SDK を Laravel Cloud で動かすための、Copilot CLI インストール、環境変数、stdio/TCP モードの運用ポイントを解説します。 ## Laravel Cloud で使う ## インストール **Settings > Deployments > Build commands** に次を追加してデプロイします。 ```shell theme={null} export PATH="$PATH:/var/www/.local/bin" curl -fsSL https://gh.io/copilot-install | bash /var/www/.local/bin/copilot --version ``` CLI のパスは `/var/www/.local/bin/copilot` なので、**Custom environment variables** に次を設定します。 ```dotenv theme={null} COPILOT_CLI_PATH=/var/www/.local/bin/copilot COPILOT_GITHUB_TOKEN= ``` `COPILOT_GITHUB_TOKEN` には、`GH_TOKEN` や `GITHUB_TOKEN` など Copilot CLI がサポートする認証方式を使えます。 デフォルトの stdio モードを使う場合は、このインストールだけで十分です。 ## TCP モードで運用する **App cluster > Background processes > Custom worker** で `/var/www/.local/bin/copilot --headless --port 12345` を設定します。 バックグラウンドプロセスとして継続稼働し、デプロイ時に自動再起動します。 ポート番号は任意です。 **Custom environment variables** で `COPILOT_URL` を設定します。 ```dotenv theme={null} COPILOT_URL=tcp://127.0.0.1:12345 # COPILOT_CLI_PATH は TCP モードでは不要。両方ある場合は TCP モードが優先されます。 ``` 常時稼働のためメモリ使用量が高く、2 GiB RAM 以上のサーバーが必要になる場合があります。 HTTP リクエスト内で使うと 60 秒以内に終わらず失敗しやすいため、キュー実行を推奨します。 Laravel Cloud では TCP モードの実用性が高くないため、stdio モード推奨です。 上位プランの Worker cluster でも利用できます。App cluster のメモリ不足を避けたい場合に有効です。 ## 注意点 * 低価格サーバーはメモリ不足になりやすいです。 * デフォルトの HTTP タイムアウトは 20 秒です。失敗が多い場合は最大 60 秒まで延ばしてください。 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # MCP Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/mcp Laravel Copilot SDK のセッション設定と Server RPC API で MCP サーバーを構成・管理する方法を説明します。 ## MCP SessionConfigの`mcpServers`にMCPサーバーを指定すれば使うことができます。 Copilot CLIでLaravel Boostを使う場合は [laravel-boost-copilot-cli](https://github.com/invokable/laravel-boost-copilot-cli) も使いましょう。 `'type' => 'stdio'`(または`'local'`)と`'tools' => ['*']`が必須なことが分かっています。これがないとMCPサーバーとして認識されません。 ```php theme={null} Artisan::command('copilot:mcp', function () { $config = new SessionConfig( mcpServers: [ 'laravel-boost' => [ 'type' => 'stdio', 'command' => 'php', 'args' => ['artisan', 'boost:mcp'], 'tools' => ['*'], ], ], ); Copilot::start(function (CopilotSession $session) { info('Starting Copilot with Laravel Boost MCP: '.$session->id()); $prompt = 'どのMCPが読み込まれている? laravel-boostが使える場合はapplication-infoでアプリ情報を取得して。'; warning($prompt); $response = spin( callback: fn () => $session->sendAndWait($prompt), message: 'Thinking...', ); note($response->content()); }, config: $config); }); ``` ## MCPサーバーのステータス セッション内のMCPサーバーのステータスは以下の値を取ります: | ステータス | 説明 | | - | - | | `connected` | 接続済み | | `failed` | 接続失敗 | | `needs-auth` | 認証が必要 | | `pending` | 接続待ち | | `disabled` | 無効化済み | | `not_configured` | 未設定 | ## MCPサーバー設定の管理(Server RPC) セッションとは別に、サーバーレベルでMCPサーバーの設定を管理できます。 ```php theme={null} use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Types\Rpc\McpConfigAddRequest; use Revolution\Copilot\Types\Rpc\McpConfigUpdateRequest; use Revolution\Copilot\Types\Rpc\McpConfigRemoveRequest; use Revolution\Copilot\Types\Rpc\McpServerValue; // 設定済みのMCPサーバー一覧を取得 $result = Copilot::client()->rpc()->mcp()->list(); foreach ($result->servers as $name => $config) { dump($name, $config->type, $config->command); } // MCPサーバーを追加 Copilot::client()->rpc()->mcp()->add(new McpConfigAddRequest( name: 'laravel-boost', config: new McpServerValue( type: 'stdio', command: 'php', args: ['artisan', 'boost:mcp'], tools: ['*'], ), )); // MCPサーバーの設定を更新 Copilot::client()->rpc()->mcp()->update(new McpConfigUpdateRequest( name: 'laravel-boost', config: new McpServerValue( type: 'stdio', command: 'php', args: ['artisan', 'boost:mcp', '--verbose'], tools: ['*'], ), )); // MCPサーバーを削除 Copilot::client()->rpc()->mcp()->remove(new McpConfigRemoveRequest(name: 'laravel-boost')); ``` 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # モデル Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/models Copilot CLI がサポートするモデル一覧を確認し、Laravel Copilot SDK のセッション設定でモデル ID を指定します。 ## モデルリスト `Copilot::client()->listModels()` で Copilot CLI がサポートしているモデルの一覧を取得できます。 利用可能なモデルは Copilot CLI のアップデートや組織のポリシーで変わります。\ [モデルリスト](https://github.com/invokable/laravel-copilot-sdk/blob/main/docs/models-list.md) は参考情報として扱ってください。 ## モデル ID の指定 SessionConfig の `model` に指定するのは `ID` です。 ```php theme={null} Copilot::run($prompt, config: ['model' => 'auto']); ``` 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # 権限リクエスト Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/permission-request Laravel Copilot SDK の権限リクエストを、既定値、組み込みハンドラ、カスタム承認コールバックで制御します。 ## 権限リクエスト ## デフォルト動作(`deny-all`) `config/copilot.php` の `permission_approve` が `"deny-all"`(デフォルト)の場合、`Copilot::run()` や `Copilot::start()` では権限リクエストは自動で **拒否** されます。 テキスト生成中心の用途では権限を必要としないことが多いため、安全なデフォルトです。 ```php theme={null} // config/copilot.php 'permission_approve' => env('COPILOT_PERMISSION_APPROVE', 'deny-all'), ``` ## 設定できる値 | 値 | 動作 | | - | - | | `"deny-all"` | すべて自動拒否(**デフォルト**) | | `"approve-safety"` | `shell` と `write` のみ拒否し、他は自動許可 | | `"approve-all"` | すべて自動許可 | | `false` | ハンドラなし。`onPermissionRequest` が必須(公式 SDK と同じ) | ```php theme={null} // .env COPILOT_PERMISSION_APPROVE="approve-safety" ``` ユーザー入力プロンプトを受け付ける場合、`"approve-safety"` と `"approve-all"` は危険です。 必ず `false` または `"deny-all"` を使ってください。 read-only 権限でも Laravel プロジェクトのコードが読まれる可能性があります。 ## `PermissionHandler::approveAll()` `PermissionHandler::approveAll()` はすべてのリクエストを自動許可します。 ```php theme={null} use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Support\PermissionHandler; use Revolution\Copilot\Types\SessionConfig; $config = new SessionConfig( onPermissionRequest: PermissionHandler::approveAll(), ); $response = Copilot::run(prompt: 'Hello', config: $config); ``` ## `PermissionHandler::approveSafety()` `PermissionHandler::approveSafety()` は高リスクの権限(`shell`, `write`)のみ拒否し、他を自動許可します。 ```php theme={null} use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Support\PermissionHandler; use Revolution\Copilot\Types\SessionConfig; $config = new SessionConfig( onPermissionRequest: PermissionHandler::approveSafety(), ); $response = Copilot::run(prompt: 'Hello', config: $config); ``` これでも完全に安全とは限りません。 厳密に制御したい場合はカスタムハンドラを実装し、`$request['kind']` を見て判定してください。 ## `PermissionHandler::denyAll()` `PermissionHandler::denyAll()` はすべて拒否します。 ```php theme={null} use Revolution\Copilot\Support\PermissionHandler; use Revolution\Copilot\Types\SessionConfig; $config = new SessionConfig( onPermissionRequest: PermissionHandler::denyAll(), ); ``` ## Client を直接使う場合 `CopilotClient` を直接使う場合は、公式 SDK と同様に `onPermissionRequest` の指定が **必須** です。 ```php theme={null} use Revolution\Copilot\Client; use Revolution\Copilot\Support\PermissionHandler; $client = new Client([ 'cli_path' => 'copilot', 'cli_args' => [], 'cwd' => base_path(), 'log_level' => 'info', 'env' => null, ]); $client->start(); // onPermissionRequest は必須 $session = $client->createSession([ 'onPermissionRequest' => PermissionHandler::approveSafety(), ]); // 省略すると InvalidArgumentException // $session = $client->createSession([]); // Error! ``` ## カスタムハンドラ リクエスト種別ごとに許可・拒否を制御するにはクロージャを渡します。 `$request` と `$invocation` は以下のような配列です。 ```php theme={null} use Illuminate\Support\Facades\Artisan; use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Support\PermissionRequestResultKind; use Revolution\Copilot\Types\SessionConfig; use function Laravel\Prompts\{confirm, note, spin, text}; Artisan::command('copilot:chat', function () { $config = new SessionConfig( onPermissionRequest: function (array $request, array $invocation) { $confirm = confirm( label: 'Do you accept the requested permissions?', ); if ($confirm) { return PermissionRequestResultKind::approveOnce(); } else { return PermissionRequestResultKind::reject(); } }, ); Copilot::start(function (CopilotSession $session) use ($config) { while (true) { $prompt = text( label: 'Enter your prompt', placeholder: 'Ask me anything...', required: true, hint: 'Ctrl+C to exit', ); $response = spin( callback: fn () => $session->sendAndWait($prompt), message: 'Waiting for Copilot response...', ); note($response->content()); } }, config: $config); }); ``` ### `$request` `kind` と `toolCallId` 以外のフィールドは `kind` によって変わります。 ```text theme={null} kind: "shell" | "write" | "mcp" | "read" | "url" | "custom-tool" | "memory" | "hook" ``` ```php theme={null} [ "kind" => "shell", "toolCallId" => "toolu_...", "fullCommandText" => "...", "intention" => "Run copilot:ping to test permission request", "commands" => [ [ "identifier" => "bash", "readOnly" => false, ] ] "possiblePaths" => [], "possibleUrls" => [], "hasWriteFileRedirection" => false, "canOfferSessionApproval" => false, ] ``` ### `$invocation` ```php theme={null} [ "sessionId" => "...", ] ``` ## Response 権限判定結果は配列で返します。 `PermissionRequestResultKind` クラスを使うと読みやすくなります。 ```php theme={null} return PermissionRequestResultKind::approveOnce(); return PermissionRequestResultKind::reject(); ``` ## プロトコル詳細 Protocol v3(現在のデフォルト)では、権限リクエストは JSON-RPC リクエストではなくセッションイベント(`permission.requested`)として配信されます。 SDK は内部でこれを処理し、`session.permissions.handlePendingPermissionRequest` RPC で応答します。 **`SessionConfig` の使い方は変わりません。** `onPermissionRequest` にハンドラを渡せば、プロトコル差分は SDK が吸収します。 ## PermissionRequestResultKind `['kind' => 'approve-once']` の形式で直接返すこともできますが、`PermissionRequestResultKind` を使うと分かりやすくなります。 ```php theme={null} use Revolution\Copilot\Support\PermissionRequestResultKind; $confirm = confirm( label: 'Do you accept the requested permissions?', ); if ($confirm) { return PermissionRequestResultKind::approveOnce(); } else { return PermissionRequestResultKind::reject(); } ``` ### 利用可能なメソッド | メソッド | 値 | 説明 | | - | - | - | | `approveOnce()` | `approve-once` | 今回のリクエストのみ許可 | | `approveForSession()` | `approve-for-session` | このセッション中の同種リクエストをすべて許可 | | `approveForLocation()` | `approve-for-location` | この場所(ファイルパス等)からの同種リクエストをすべて許可 | | `reject()` | `reject` | リクエストを拒否 | | `userNotAvailable()` | `user-not-available` | ユーザーが応答できない状態(非インタラクティブ環境など) | | `noResult()` | `no-result` | ハンドラが結果を返せない場合(RPC 呼び出しをスキップ) | `Laravel\Prompts\select` を使いたい場合は `PermissionRequestResultKind::select()` で選択肢を取得できます。 ```php theme={null} use Revolution\Copilot\Support\PermissionRequestResultKind; use function Laravel\Prompts\select; $select = select( label: 'Do you accept the requested permissions?', options: PermissionRequestResultKind::select(), ); return ['kind' => $select]; ``` ### no-result ハンドラが結果を返せない場合(例: 非対話環境)は `no-result` を返せます。 `no-result` を返すと RPC 呼び出しをスキップし、Copilot CLI 側のデフォルト拒否が適用されます。 ```php theme={null} return PermissionRequestResultKind::noResult(); // または ['kind' => 'no-result'] ``` 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # Plugin Directories Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/plugin-directories スキル・フック・MCP・カスタムエージェントなどをまとめて読み込む Plugin Directory の使い方を説明します。 ## Plugin Directories Plugin Directory は、スキル、フック、MCP サーバー、カスタムエージェント、LSP 設定などを 1 つのディレクトリにまとめて読み込む仕組みです。再利用可能な機能セットをアプリやリポジトリに同梱したい場合に使います。 Laravel 版では `SessionConfig` または `ResumeSessionConfig` の `pluginDirectories` で指定できます。 ## 使いどころ * 複数の拡張機能を 1 つの能力パックとして配布する * リポジトリにプラグインを同梱し、全員が同じ設定を使えるようにする * Marketplace 公開前のプラグインをローカルで開発・検証する * インストール済みプラグインをローカル checkout で一時的に上書きする MCP サーバー 1 つ、フック 1 つ、カスタムエージェント 1 つだけなら、`mcpServers`、`hooks`、`customAgents` へ直接指定するほうが単純です。Plugin Directory は、関連する複数機能をまとめて配布したい場合に向いています。 ## ディレクトリ構成 Copilot CLI は各プラグインディレクトリから `plugin.json` またはルート直下の `SKILL.md` を探します。 ```text theme={null} my-plugin/ ├── plugin.json ├── SKILL.md ├── hooks.json ├── .mcp.json ├── agents/ │ └── code-reviewer.md └── skills/ └── lint-fix/ └── SKILL.md ``` `plugin.json` は `.github/plugin.json` や `.github/plugin/plugin.json` にも配置できます。スキル、フック、MCP、エージェントなどはそれぞれ独立したローダーを持つため、必要なものだけ含めれば十分です。 ## Laravel から読み込む ```php theme={null} use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Types\SessionConfig; Copilot::start(function (CopilotSession $session): void { $response = $session->sendAndWait( prompt: 'この変更をプラグインのレビュー方針で確認して', ); dump($response->content()); }, config: new SessionConfig( pluginDirectories: [ base_path('plugins/code-reviewer'), base_path('plugins/lint-fix'), ], )); ``` 配列形式でも指定できます。 ```php theme={null} Copilot::run( prompt: 'READMEを改善して', config: [ 'pluginDirectories' => [ base_path('plugins/docs-writer'), ], ], ); ``` ## `cli_args` との違い 公式 SDK ではランタイム起動時の `--plugin-dir` 引数として説明される場合があります。Laravel 版ではセッション設定の `pluginDirectories` を使うのが基本です。 一方で、CLI プロセス起動そのものに引数を渡したい場合は `config/copilot.php` の `cli_args` または `Copilot::useStdio()` の `cli_args` を使えます。ただし `cli_args` は stdio で SDK が CLI を起動する場合だけ有効で、`useTcp()` など外部 runtime 接続時は無視されます。 ```php theme={null} $stdioConfig = config('copilot'); $stdioConfig['cli_args'] = [ '--plugin-dir', base_path('plugins/code-reviewer'), ]; Copilot::useStdio($stdioConfig)->start(fn (CopilotSession $session) => ...); ``` 通常は `pluginDirectories` を優先してください。セッション単位で明示でき、他の `SessionConfig` 設定と同じ場所で管理できます。 ## 再現性のための運用 * 相対パスより `base_path()` などで絶対パス化する * リポジトリに同梱するプラグインはバージョン管理する * Marketplace や外部配布のプラグインを使う場合は、利用バージョンをドキュメント化する * 本番環境では、ユーザー入力から任意のプラグインパスを直接指定させない ## 参考 * [SessionConfig](/jp/packages/laravel-copilot-sdk/session-config) * [カスタムエージェント](/jp/packages/laravel-copilot-sdk/custom-agents) * [MCP](/jp/packages/laravel-copilot-sdk/mcp) * [Hooks](/jp/packages/laravel-copilot-sdk/hooks) * [Skills](/jp/packages/laravel-copilot-sdk/skills) 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # Remote Sessions Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/remote-sessions Laravel Copilot SDK でリモートセッションを有効にし、GitHub の Mission Control 経由でウェブやモバイルから同じセッションにアクセスする方法を説明します。 ## Remote Sessions Remote Sessions を有効にすると、GitHub の Mission Control 経由でウェブやモバイルから同じセッションにアクセスできます。 有効化すると各セッションが Mission Control に接続され、リンクや QR コードとして共有できる URL が発行されます。 ローカルや自前サーバーではなく GitHub ホスト環境でセッション自体を実行したい場合は [Cloud Sessions](/jp/packages/laravel-copilot-sdk/cloud-sessions) を使います。 ## 前提条件 * ユーザーが認証済みであること(GitHub トークンまたはログイン済みユーザー) * `workingDirectory` が GitHub リポジトリであること ## クライアントレベルで有効化(常時 ON) `remote: true` を有効化すると、GitHub リポジトリ内のすべてのセッションで自動的にリモート URL が発行されます。 `config/copilot.php` に固定で追加するより、必要な処理だけ `useStdio()` で `remote: true` を有効化する使い方が扱いやすいです。 `remote` オプションは SDK が CLI プロセスを起動する場合にのみ有効です。つまり **stdio モード専用**です。`useTcp()` など外部サーバーへ接続する場合は無視されます。 ```php theme={null} use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Enums\SessionEventType; use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Support\PermissionHandler; use Revolution\Copilot\Types\SessionConfig; use Revolution\Copilot\Types\SessionEvent; $config = new SessionConfig( workingDirectory: '/path/to/github-repo', onPermissionRequest: PermissionHandler::approveAll(), ); $stdioConfig = config('copilot'); $stdioConfig['remote'] = true; Copilot::useStdio($stdioConfig)->start(function (CopilotSession $session): void { $session->on(SessionEventType::SESSION_INFO, function (SessionEvent $event): void { if ($event->data('infoType') === 'remote') { echo 'Remote URL: '.$event->data('url').PHP_EOL; } }); }, config: $config); ``` ## セッションごとに有効化(オンデマンド) 途中からリモート共有を開始したい場合は RPC で切り替えます。 CLI の `/remote on` および `/remote off` コマンドと同等の操作です。 ```php theme={null} use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Facades\Copilot; Copilot::start(function (CopilotSession $session): void { $result = $session->rpc()->remote()->enable(); if ($result->url !== null) { echo 'Remote URL: '.$result->url.PHP_EOL; } // 共有停止 $session->rpc()->remote()->disable(); }); ``` ## QR コード生成 取得したリモート URL は QR コードにしてモバイルへ渡せます。 Laravel Fortify でも使われている [BaconQrCode](https://github.com/Bacon/BaconQRCode) が利用できます。 ```php theme={null} use BaconQrCode\Renderer\Image\SvgImageBackEnd; use BaconQrCode\Renderer\ImageRenderer; use BaconQrCode\Renderer\RendererStyle\RendererStyle; use BaconQrCode\Writer; $writer = new Writer( new ImageRenderer( new RendererStyle(256), new SvgImageBackEnd(), ), ); $svg = $writer->writeString($result->url ?? ''); ``` ## 注意点 * `remote` オプションは SDK が CLI プロセスを起動する場合にのみ有効です。`useTcp()` など外部サーバー接続時は無視されます。 * `workingDirectory` が GitHub リポジトリでない場合、常時 ON モードではリモート設定がスキップされます。オンデマンド有効化(`remote()->enable()`)ではエラーになる場合があります。 * 認証情報(`github_token` または `use_logged_in_user`)を適切に設定してください。 ## 参考 * [Cloud Sessions](/jp/packages/laravel-copilot-sdk/cloud-sessions) * [RPC](/jp/packages/laravel-copilot-sdk/rpc) * [TCP モード](/jp/packages/laravel-copilot-sdk/tcp-mode) 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # セッション再開 Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/resume listSessions()・固定セッション ID・ResumeSessionConfig・最後のセッション再開を使って Laravel Copilot SDK の会話を復元します。 ## セッション再開 以前のセッションを再開する方法。 ## Artisanコマンドで対話形式のチャットで使用している場合 ```php theme={null} // listSessionsでこれまでのセッション一覧を取得 $sessions = Copilot::client()->listSessions(); // Laravel\Prompts\selectなどで再開するセッションIDを選択 $sessions = collect(Copilot::client()->listSessions()) ->mapWithKeys(function (SessionMetadata $session) { return [$session->sessionId => $session->summary ?? '']; }) ->toArray(); $session_id = select( label: 'What session do you want to resume?', options: $sessions, ); // 選択されたIDでセッションを再開 $session = Copilot::client()->resumeSession($session_id); // これまでのメッセージを取得 $messages = $session->getMessages(); ``` ## 固定のセッションIDを使う場合 対話形式でない場合は選択できないので最初から固定のIDを指定します。ユーザーIDやなんらかのコンテキストに応じたIDを用意します。\ 例えばウェブサービス上でマルチユーザーで使うなら事前にIDを用意するこちらの方法を使います。 ```php theme={null} use Revolution\Copilot\Types\SessionConfig; use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Facades\Copilot; $config = new SessionConfig( sessionId: 'user-123-conversation', ); Copilot::start(function (CopilotSession $session) { dump('Starting Copilot session: '.$session->id()); $response = $session->sendAndWait(prompt: 'Tell me something about Laravel.'); }, config: $config); ``` 再開時の方法は `Copilot::start` なら `resume` 引数で指定することもできます。 ```php theme={null} use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Types\ResumeSessionConfig; // ResumeSessionConfigではsessionIdを指定できない $config = new ResumeSessionConfig(); Copilot::start(function (CopilotSession $session) { dump('Starting Copilot session: '.$session->id()); $response = $session->sendAndWait(prompt: 'Tell me something about Laravel.'); }, config: $config, resume: 'user-123-conversation'); ``` `Copilot::start` のクロージャ内で今のセッションを破棄して新しく再開する方法もあります。 ```php theme={null} use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Facades\Copilot; Copilot::start(function (CopilotSession $session) { dump('Starting Copilot session: '.$session->id()); $session->disconnect(); $session = Copilot::client()->resumeSession(sessionId: 'user-123-conversation'); $response = $session->sendAndWait(prompt: 'Tell me something about Laravel.'); }); ``` ## 最後のセッションを再開 セッションが存在しない場合は `getLastSessionId()` は `null` を返すので自動的に新規セッションとして開始されます。`SessionConfig` の切り替えもできます。 ```php theme={null} use Revolution\Copilot\Types\SessionConfig; use Revolution\Copilot\Types\ResumeSessionConfig; use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Facades\Copilot; $session_id = Copilot::client()->getLastSessionId(); if (empty($session_id)) { $config = new SessionConfig(); } else { $config = new ResumeSessionConfig(); } Copilot::start(function (CopilotSession $session) { dump('Starting Copilot session: '.$session->id()); $response = $session->sendAndWait(prompt: 'Tell me something about Laravel.'); }, config: $config, resume: $session_id); ``` ## SessionMetadataを使う `getSessionMetadata()` もセッションが存在しなければ `null` を返すので同様に新規セッションとして開始されます。セッションの作成日時や更新日時なども取得できます。 ```php theme={null} use Revolution\Copilot\Types\SessionMetadata; use Revolution\Copilot\Facades\Copilot; $meta = Copilot::client()->getSessionMetadata('user-123-conversation'); ``` 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # RPC(Remote Procedure Call) Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/rpc Laravel Copilot SDK の ServerRpc と SessionRpc を使って、モデル・ツール・権限などのプロトコルメソッドを呼び出します。 ## RPC(Remote Procedure Call) 公式SDKでは最近の新機能は `api.schema.json` を元にした自動コード生成で対応しています。Laravel版では生成後の他言語版を参考に同じように実装しています。 ## ServerRpc Client に紐づく RPC クラスです。 ```php theme={null} use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Types\Rpc\ModelList; // モデルリストの取得 // 返り値はModelList $result = Copilot::client()->rpc()->models()->list(); // modelsはModelInfoの配列 $models = $result->models; // 初期からあるlistModels()とほとんど同じ // 返り値はModelInfoの配列 Copilot::client()->listModels(); ``` 元から SDK に含まれていた機能も、自動コード生成版で再度追加されています。 ### メソッド一覧 ```php theme={null} Copilot::client()->rpc()->ping(); // models Copilot::client()->rpc()->models()->list(); // セッションごとのGitHubトークンで取得 Copilot::client()->rpc()->models()->list(new ModelsListRequest(gitHubToken: $token)); // tools Copilot::client()->rpc()->tools()->list(); // account Copilot::client()->rpc()->account()->getQuota(); // セッションごとのGitHubトークンで取得 Copilot::client()->rpc()->account()->getQuota(new AccountGetQuotaRequest(gitHubToken: $token)); // mcp config (MCPサーバー設定の管理) Copilot::client()->rpc()->mcp()->list(); Copilot::client()->rpc()->mcp()->add(new McpConfigAddRequest( name: 'my-server', config: new McpServerValue(type: 'local', command: 'php', args: ['artisan', 'mcp']), )); Copilot::client()->rpc()->mcp()->update(new McpConfigUpdateRequest( name: 'my-server', config: new McpServerValue(type: 'http', url: 'https://mcp.example.com'), )); Copilot::client()->rpc()->mcp()->remove(new McpConfigRemoveRequest(name: 'my-server')); // MCPサーバーを有効化/無効化(グローバル設定) Copilot::client()->rpc()->mcp()->enable(new McpConfigEnableRequest(names: ['my-server'])); Copilot::client()->rpc()->mcp()->disable(new McpConfigDisableRequest(names: ['my-server'])); // mcp discover (MCPサーバーの自動検出) Copilot::client()->rpc()->mcp()->discover(new McpDiscoverRequest( workingDirectory: '/path/to/project', )); // 引数なしで実行可能 $result = Copilot::client()->rpc()->mcp()->discover(); // $result->servers is an array of DiscoveredMcpServer // sessionFs (セッションファイルシステムプロバイダーの登録) Copilot::client()->rpc()->sessionFs()->setProvider(new SessionFsSetProviderRequest( initialCwd: '/path/to/project', sessionStatePath: '.copilot/sessions', conventions: 'posix', )); // sessions (experimental: セッションのフォーク) Copilot::client()->rpc()->sessions()->fork(new SessionsForkRequest( sessionId: 'source-session-id', toEventId: 'evt-boundary', // オプション: この ID より前のイベントのみ含める )); // skills (サーバーレベルのスキル管理) // スキルの探索 $result = Copilot::client()->rpc()->skills()->discover(); // $result->skills は ServerSkill の配列 // オプションでプロジェクトパスを指定 $result = Copilot::client()->rpc()->skills()->discover(new SkillsDiscoverRequest( projectPaths: ['/path/to/project'], skillDirectories: ['/custom/skills'], )); // 無効化するスキルの設定 Copilot::client()->rpc()->skills()->config()->setDisabledSkills( new SkillsConfigSetDisabledSkillsRequest(disabledSkills: ['skill-name']) ); ``` ## SessionRpc Session に紐づく RPC クラスです。 以前の SDK ではできなかったプランモードの利用なども可能です。 ```php theme={null} use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Types\Rpc\ModeSetRequest; use Revolution\Copilot\Types\Rpc\PlanReadResult; Copilot::start(function (CopilotSession $session) { $session->rpc()->mode()->set(new ModeSetRequest(mode: 'plan')); $response = $session->sendAndWait(prompt: '〇〇のプランを作成'); $result = $session->rpc()->plan()->read(); dump($result->content); $session->rpc()->mode()->set(new ModeSetRequest(mode: 'autopilot')); $response = $session->sendAndWait(prompt: 'プランに従って実装'); dump($response->content()); }); ``` ### メソッド一覧 ```php theme={null} // model $session->rpc()->model()->getCurrent(); $session->rpc()->model()->switchTo(new ModelSwitchToRequest(modelId: 'gpt-4')); // reasoningEffortを指定する場合(対応モデルのみ) $session->rpc()->model()->switchTo(new ModelSwitchToRequest(modelId: 'claude-opus-4.7', reasoningEffort: ReasoningEffort::HIGH)); // modelCapabilitiesをオーバーライドする場合 $session->rpc()->model()->switchTo(new ModelSwitchToRequest( modelId: 'gpt-4', modelCapabilities: new ModelCapabilitiesOverride( supports: new ModelCapabilitiesOverrideSupports(vision: true), ), )); // setModel()ヘルパーでも同様にreasoningEffortやmodelCapabilitiesを指定可能 $session->setModel('claude-opus-4.7', ReasoningEffort::HIGH); $session->setModel('claude-opus-4.7', 'high'); // String is also supported $session->setModel('gpt-4', modelCapabilities: ['supports' => ['vision' => true]]); // 配列でも指定可能 // mode $session->rpc()->mode()->get(); $session->rpc()->mode()->set(new ModeSetRequest(mode: 'plan')); // name $session->rpc()->name()->get(); $session->rpc()->name()->set(new NameSetRequest(name: 'My Session')); // plan $session->rpc()->plan()->read(); $session->rpc()->plan()->update(new PlanUpdateRequest(content: '...')); $session->rpc()->plan()->delete(); // workspaces $session->rpc()->workspaces()->getWorkspace(); $session->rpc()->workspaces()->listFiles(); $session->rpc()->workspaces()->readFile(new WorkspacesReadFileRequest(path: 'file.txt')); $session->rpc()->workspaces()->createFile(new WorkspacesCreateFileRequest(path: 'file.txt', content: '...')); // instructions (セッションのインストラクションソースを取得) $result = $session->rpc()->instructions()->getSources(); // $result->sources - InstructionsSources の配列 // fleet $session->rpc()->fleet()->start(new FleetStartRequest(prompt: '...')); // 詳細は Fleet Mode を参照 // agent $session->rpc()->agent()->list(); $session->rpc()->agent()->getCurrent(); $session->rpc()->agent()->select(new AgentSelectRequest(agentId: '...')); $session->rpc()->agent()->deselect(); $session->rpc()->agent()->reload(); // skills (experimental: スキルの管理) $session->rpc()->skills()->list(); $session->rpc()->skills()->enable(new SkillsEnableRequest(name: 'skill-name')); $session->rpc()->skills()->disable(new SkillsDisableRequest(name: 'skill-name')); $session->rpc()->skills()->reload(); // mcp (experimental: MCPサーバーの管理) $session->rpc()->mcp()->list(); $session->rpc()->mcp()->enable(new McpEnableRequest(serverName: 'server-name')); $session->rpc()->mcp()->disable(new McpDisableRequest(serverName: 'server-name')); $session->rpc()->mcp()->reload(); // MCP OAuthログイン(認証が必要なMCPサーバー向け) $result = $session->rpc()->mcp()->login(new McpOauthLoginRequest(serverName: 'my-server')); // $result->authorizationUrl - OAuthフローのURL(認証が必要な場合) // plugins (experimental: プラグインの一覧) $session->rpc()->plugins()->list(); // extensions (experimental: エクステンションの管理) $session->rpc()->extensions()->list(); $session->rpc()->extensions()->enable(new ExtensionsEnableRequest(id: 'project:my-ext')); $session->rpc()->extensions()->disable(new ExtensionsDisableRequest(id: 'project:my-ext')); $session->rpc()->extensions()->reload(); // compaction → history に名前変更 $session->rpc()->history()->compact(); // 特定のイベント以降の履歴を切り詰める $session->rpc()->history()->truncate(new HistoryTruncateRequest( eventId: 'evt-123', // このイベントとそれ以降のすべてのイベントが削除される )); // tools (プロトコルv3+: external_tool.requestedイベントへの応答) $session->rpc()->tools()->handlePendingToolCall(new ToolsHandlePendingToolCallRequest( requestId: '...', result: 'ツールの実行結果', )); // permissions (プロトコルv3+: permission.requestedイベントへの応答) $session->rpc()->permissions()->handlePendingPermissionRequest(new PermissionDecisionRequest( requestId: '...', result: PermissionRequestResultKind::approveOnce(), )); // セッション内のすべての権限リクエストを自動承認 $session->rpc()->permissions()->setApproveAll(new PermissionsSetApproveAllRequest(enabled: true)); // セッションスコープの権限承認をリセット $session->rpc()->permissions()->resetSessionApprovals(); // commands: コマンド呼び出しイベントへの応答 $session->rpc()->commands()->handlePendingCommand(new CommandsHandlePendingCommandRequest( requestId: '...', )); // ui: UIエリシテーションリクエストへの応答 $session->rpc()->ui()->elicitation(new UIElicitationRequest( message: 'ユーザーへの質問', requestedSchema: ['type' => 'object', 'properties' => [...]], )); // ui: 保留中のエリシテーションリクエストへの応答(elicitation.requestedイベント経由) $session->rpc()->ui()->handlePendingElicitation(new UIHandlePendingElicitationRequest( requestId: '...', result: ['action' => 'accept', 'content' => ['name' => 'John']], )); // log: セッションタイムラインへのメッセージ記録 $session->rpc()->log()->log(new LogRequest(message: '処理を開始しました')); $session->rpc()->log()->log(new LogRequest(message: 'ディスク使用量が多い', level: LogLevel::WARNING)); $session->rpc()->log()->log(new LogRequest(message: 'エラーが発生しました', level: LogLevel::ERROR)); $session->rpc()->log()->log(new LogRequest(message: 'デバッグ情報', ephemeral: true)); // shell: セッション内でシェルコマンドを実行 $result = $session->rpc()->shell()->exec(new ShellExecRequest(command: 'ls -la')); // $result->processId でプロセスIDを取得してkillや出力追跡に使用 $session->rpc()->shell()->exec(new ShellExecRequest( command: 'npm test', cwd: '/path/to/project', timeout: 60000, // ミリ秒 )); // 実行中のシェルプロセスを停止 $session->rpc()->shell()->kill(new ShellKillRequest( processId: $result->processId, signal: 'SIGTERM', // SIGTERM(デフォルト), SIGKILL, SIGINT )); // usage (experimental: セッション使用量メトリクス) $metrics = $session->rpc()->usage()->getMetrics(); // $metrics->totalPremiumRequestCost - プレミアムリクエストの合計コスト // $metrics->totalUserRequests - ユーザーリクエストの合計数 // $metrics->codeChanges - コード変更メトリクス(追加行数、削除行数、変更ファイル数) // $metrics->modelMetrics - モデルごとのトークン使用量とリクエスト数 // $metrics->currentModel - 現在のモデル識別子 // auth: セッションの認証状態を取得 $status = $session->rpc()->auth()->getStatus(); // $status->isAuthenticated - 認証済みかどうか // $status->authType - 認証タイプ(AuthInfoType enum: gh-cli, token, env など) // $status->login - GitHubログイン名 // $status->host - GitHubホスト // $status->copilotPlan - Copilotプラン(individual, business など) // $status->statusMessage - 認証状態のメッセージ ``` ## SessionFS コールバック型 セッションスコープのファイルシステム操作のためのコールバック型(Request/Result)が定義されています。これらは Copilot CLI がクライアントにコールバックする際のリクエスト/レスポンス型です。 | 型クラス | 用途 | | - | - | | `SessionFsReadFileRequest` / `SessionFsReadFileResult` | ファイル読み取り | | `SessionFsWriteFileRequest` | ファイル書き込み | | `SessionFsAppendFileRequest` | ファイル追記 | | `SessionFsExistsRequest` / `SessionFsExistsResult` | ファイル存在確認 | | `SessionFsStatRequest` / `SessionFsStatResult` | ファイルメタデータ取得 | | `SessionFsMkdirRequest` | ディレクトリ作成 | | `SessionFsReaddirRequest` / `SessionFsReaddirResult` | ディレクトリ一覧 | | `SessionFsReaddirWithTypesRequest` / `SessionFsReaddirWithTypesResult` | 型付きディレクトリ一覧 | | `SessionFsRmRequest` | ファイル/ディレクトリ削除 | | `SessionFsRenameRequest` | ファイル/ディレクトリ名変更 | これらの型クラスは `src/Types/Rpc/` に配置されています。 ## 配列での引数指定 引数も返り値も専用クラスを使いますが、引数は配列で指定することも可能です。 ```php theme={null} $session->rpc()->mode()->set(['mode' => 'plan']); ``` ## テスト `Copilot::fake()` でのモックは使えないため、`Copilot::expects('client')` や `Copilot::expects('start')` でモックしてください。 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # send() と on() Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/send-on Laravel Copilot SDK の send() / sendAndWait() と on() を組み合わせて、中間イベントと最終レスポンスを扱う方法を説明します。 ## Session `send()` と `on()` `sendAndWait()` はレスポンスがすぐに返って来るので分かりやすいですが、**最後のアシスタントメッセージ** しか受け取れません。 ```php theme={null} use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Facades\Copilot; Copilot::start(function (CopilotSession $session) { $response = $session->sendAndWait(prompt: 'Tell me something about Laravel.'); dump($response->content()); }); ``` 途中のメッセージも受け取りたい場合は `on()` でイベントリスナーを登録します。 ```php theme={null} use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Types\SessionEvent; Copilot::start(function (CopilotSession $session) { $session->on(function (SessionEvent $event): void { if ($event->isAssistantMessage()) { dump($event->content()); } else { dump($event); } }); $message_id = $session->send(prompt: 'Tell me something about Laravel.'); // ループでメッセージの受信を待つ $session->wait(timeout: 60.0); }); ``` PHP でこれは分かりにくいので `on()` と `sendAndWait()` の組み合わせがおすすめです。 ```php theme={null} use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Types\SessionEvent; Copilot::start(function (CopilotSession $session) { $session->on(function (SessionEvent $event): void { if ($event->isAssistantMessage()) { dump($event->content()); } else { dump($event); } }); $response = $session->sendAndWait(prompt: 'Tell me something about Laravel.'); // ループで待つ部分はsendAndWait()内で処理されているのでこの時点で最後のメッセージまで届いている。 // 途中のメッセージは上のon()で受け取っている。 // sendAndWaitからの最後のメッセージは不要。 // dump($response->content()); }); ``` ## `on()` で特定のイベントタイプを指定 `SessionEventType` enum か文字列でイベントタイプを指定すればそのイベントのみ購読できます。 ```php theme={null} use Revolution\Copilot\Enums\SessionEventType; use Revolution\Copilot\Types\SessionEvent; $session->on(SessionEventType::ASSISTANT_MESSAGE, function (SessionEvent $event): void { dump($event->content()); }); $session->on('assistant.message', function (SessionEvent $event): void { dump($event->content()); }); ``` ## すべてのイベントタイプを購読 イベントタイプを指定しなければすべて購読します。 ```php theme={null} use Revolution\Copilot\Enums\SessionEventType; use Revolution\Copilot\Types\SessionEvent; $session->on(function (SessionEvent $event): void { }); // 名前付き引数で指定もできるので $session->on(handler: function (SessionEvent $event): void { }); // nullを許容する動的なタイプ指定も可能。 $type = null; $session->on(type: $type, handler: function (SessionEvent $event): void { }); ``` 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # SessionConfig Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/session-config Laravel Copilot SDK の SessionConfig で、ツール・MCP・フック・エージェント・ライフサイクルイベントを設定します。 ## SessionConfig `SessionConfig`クラスで様々な設定が可能です。 `Copilot::run(prompt: '...', config: $config)`や`Copilot::start(function (CopilotSession $session) { ... }, config: $config)`のように使用します。 単純にモデルを指定したいだけのような`SessionConfig`クラスを使うまでもない時は配列での指定も可能です。`Copilot::run(prompt: '...', config: ['model' => 'auto'])` ```php theme={null} use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Types\SessionConfig; use Revolution\Copilot\Types\SystemMessageConfig; use Revolution\Copilot\Types\InfiniteSessionConfig; use Revolution\Copilot\Types\UserInputRequest; use Revolution\Copilot\Types\Rpc\ModelCapabilitiesOverride; use Revolution\Copilot\Types\Rpc\ModelCapabilitiesOverrideSupports; use Revolution\Copilot\Types\Rpc\ModelCapabilitiesOverrideLimits; use Revolution\Copilot\Enums\ReasoningEffort; $config = new SessionConfig( // 使用するモデルの指定 model: 'auto', // 新規セッション作成時に固定のセッションIDを指定 sessionId: 'session-123', clientName: 'my-app', // 推論レベル。対応しているモデルでのみ設定可能。 reasoningEffort: ReasoningEffort::HIGH, // モデルのcapabilitiesをオーバーライド。ランタイムのデフォルトにdeep-mergeされる。 // modelCapabilities: new ModelCapabilitiesOverride( // supports: new ModelCapabilitiesOverrideSupports(vision: true, reasoningEffort: true), // limits: new ModelCapabilitiesOverrideLimits(max_prompt_tokens: 200000), // ), // 配列での指定も可能 // modelCapabilities: ['supports' => ['vision' => true]], // configディレクトリを上書き設定 configDirectory: '', // MCP設定やスキルディレクトリの自動検出を有効化 // .mcp.json や .vscode/mcp.json をワーキングディレクトリから自動検出し、 // 明示的に指定した mcpServers や skillDirectories とマージする(名前衝突時は明示指定が優先) enableConfigDiscovery: true, // カスタムツール tools: [...], // システムメッセージ systemMessage: new SystemMessageConfig( content: 'You are a helpful assistant for Laravel developers.', ), // システムメッセージ: replaceモード(システムプロンプトを完全に置き換え) // systemMessage: new SystemMessageConfig( // mode: 'replace', // content: 'カスタムシステムプロンプト全体', // ), // システムメッセージ: customizeモード(セクション単位でオーバーライド) // systemMessage: new SystemMessageConfig( // mode: 'customize', // sections: [ // 'tone' => new SectionOverride(action: SectionOverrideAction::REPLACE, content: 'Always respond in Japanese.'), // 'safety' => new SectionOverride(action: SectionOverrideAction::REMOVE), // 'custom_instructions' => new SectionOverride(action: SectionOverrideAction::APPEND, content: 'Additional instructions here.'), // ], // content: 'appendモード同様の追加コンテンツ(オプション)', // ), // カスタムプロバイダー provider: new ProviderConfig(), // セッションごとのGitHubトークン(マルチテナント対応) // クライアントレベルのgithub_tokenとは別に、セッション単位でトークンを指定できる gitHubToken: $user->github_token, // リモートセッションの動作モード // 'off' - ローカルのみ(デフォルト) // 'export' - セッションイベントをGitHubにエクスポート(リモートステアリングなし) // 'on' - エクスポートとリモートステアリングを両方有効化 remoteSession: \Revolution\Copilot\Enums\RemoteSessionMode::Export, // クラウドセッション(ローカルではなくクラウドでセッションを作成) // オプションでリポジトリ情報を関連付けられる cloud: new \Revolution\Copilot\Types\CloudSessionOptions( repository: new \Revolution\Copilot\Types\CloudSessionRepository( owner: 'myorg', name: 'myrepo', branch: 'main', ), ), onPermissionRequest: function (array $request) { // 権限リクエストを処理 }, onUserInputRequest: function (UserInputRequest $request) { // ユーザーインプットリクエストを処理。ask_user }, // エリシテーションリクエストのハンドラー // 設定するとエージェントからのフォームベースUIダイアログリクエストを受け取れる onElicitationRequest: function (ElicitationContext $context) { // エリシテーションリクエストを処理 return ['action' => 'accept', 'content' => ['field' => 'value']]; }, // プランモード終了リクエストのハンドラー // 設定するとエージェントがプランモードを終了する際のリクエストを受け取れる onExitPlanModeRequest: function (ExitPlanModeRequest $request) { // プラン内容を確認して承認・却下 return new ExitPlanModeResult(approved: true, selectedAction: $request->recommendedAction); }, // 自動モード切替リクエストのハンドラー // レートリミット到達時にautoモードへの切替を許可するか選択できる onAutoModeSwitchRequest: function (AutoModeSwitchRequest $request) { // "yes", "yes_always", "no" のいずれかを返す return 'yes'; }, // セッションフック hooks: [], // ワーキングディレクトリ workingDirectory: '', // ストリーミングを有効化 streaming: true, // サブエージェントのストリーミングイベントをメインストリームに含めるか (デフォルト: true) includeSubAgentStreamingEvents: true, // 使用可能なビルトインツール availableTools: ['read_file', 'write_file'], // もしくは除外するツール excludedTools: ['shell'], // MCPサーバー設定 mcpServers: [ 'github' => [ 'type' => 'http', 'url' => 'https://api.githubcopilot.com/mcp/', ], ], // カスタムエージェント customAgents: [ [ 'name' => 'reviewer', 'displayName' => 'Code Reviewer', 'description' => 'Reviews code for best practices', 'prompt' => 'You are an expert code reviewer.', ], ], // デフォルトエージェントの設定。カスタムエージェントが選択されていない場合に使用されるビルトインエージェントの設定。 // excludedTools: デフォルトエージェントから除外するツールのリスト。 // カスタムサブエージェントには引き続き利用可能。 defaultAgent: ['excludedTools' => ['tool_name']], // セッション開始時にアクティブにするエージェントを指定。customAgentsのnameと一致する必要がある。 agent: 'reviewer', // スキルディレクトリ skillDirectories: [], // 無効なスキル disabledSkills: [], // 無限セッション設定。デフォルトは有効。 infiniteSessions: new InfiniteSessionConfig( enabled: true, backgroundCompactionThreshold: 0.80, // コンテキスト使用率が80%になったら圧縮を開始する bufferExhaustionThreshold: 0.95, // 圧縮が完了するまで95%でブロックする ), // 無限セッションを無効化 // infiniteSessions: new InfiniteSessionConfig(enabled: false), // セッション作成RPCより前にイベントハンドラを登録する。 // session.start などの早期イベントを取りこぼさないようにするために使用する。 // $session->on($handler) を後から呼ぶのと同等だが、ライフサイクルのより早い段階で登録される。 onEvent: function (SessionEvent $event) { // すべてのセッションイベントを受け取る }, ); $response = Copilot::run('...', config: $config); ``` セッション再開時には`ResumeSessionConfig`クラスを使用します。`SessionConfig`とほとんど同じですが少しだけ違います。ResumeSessionConfigは設定を変えたい項目のみ指定します。他は新規セッション開始時の設定が引き継がれます。 クラウド実行は [Cloud Sessions](/jp/packages/laravel-copilot-sdk/cloud-sessions)、Plugin Directory は [Plugin Directories](/jp/packages/laravel-copilot-sdk/plugin-directories) を参照してください。 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # セッションコンテキストとフィルタリング Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/session-context Laravel Copilot SDK の SessionContext と SessionListFilter を使って、作業ディレクトリや Git 情報でセッションを確認・絞り込む方法を説明します。 ## セッションコンテキストとフィルタリング GitHub Copilot SDK v0.1.24 以降では、セッションに作業ディレクトリと Git 情報を含むコンテキストが追加され、セッション一覧でフィルタリングできるようになりました。 ## SessionContext `SessionContext` は、セッション作成時の作業ディレクトリと Git リポジトリ情報を保持します。 ### プロパティ * `cwd` (`string`): 作業ディレクトリの絶対パス * `gitRoot` (`?string`): Git リポジトリのルートディレクトリ(Git リポジトリ外では `null`) * `repository` (`?string`): GitHub リポジトリ(`owner/repo` 形式、例: `invokable/laravel-copilot-sdk`) * `branch` (`?string`): 現在の Git ブランチ名 ### 使用例 ```php theme={null} use Revolution\Copilot\Facades\Copilot; // セッション一覧を取得 $sessions = Copilot::client()->listSessions(); foreach ($sessions as $metadata) { echo "Session: {$metadata->sessionId}\n"; if ($metadata->context !== null) { echo " 作業ディレクトリ: {$metadata->context->cwd}\n"; if ($metadata->context->repository !== null) { echo " リポジトリ: {$metadata->context->repository}\n"; echo " ブランチ: {$metadata->context->branch}\n"; } } } ``` ## SessionListFilter `SessionListFilter` を使うと、特定の作業ディレクトリやリポジトリのセッションだけを取得できます。 ### プロパティ * `cwd` (`?string`): 作業ディレクトリで完全一致フィルタ * `gitRoot` (`?string`): Git ルートディレクトリでフィルタ * `repository` (`?string`): リポジトリ(`owner/repo` 形式)でフィルタ * `branch` (`?string`): ブランチ名でフィルタ ### 使用例 #### 配列でフィルタ指定 ```php theme={null} use Revolution\Copilot\Facades\Copilot; // 特定のリポジトリのセッションのみを取得 $sessions = Copilot::client()->listSessions([ 'repository' => 'invokable/laravel-copilot-sdk', ]); // 特定のブランチで作業中のセッションを取得 $sessions = Copilot::client()->listSessions([ 'repository' => 'owner/repo', 'branch' => 'feature/new-feature', ]); // 特定の作業ディレクトリのセッションを取得 $sessions = Copilot::client()->listSessions([ 'cwd' => '/home/user/projects/my-app', ]); ``` #### SessionListFilter クラスを使用 ```php theme={null} use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Types\SessionListFilter; $filter = new SessionListFilter( repository: 'owner/repo', branch: 'main', ); $sessions = Copilot::client()->listSessions($filter); ``` ## `session.context_changed` イベント セッション中に作業ディレクトリが変更されると、`session.context_changed` イベントが発火します。 ### イベントタイプ ```php theme={null} use Revolution\Copilot\Enums\SessionEventType; SessionEventType::SESSION_CONTEXT_CHANGED; // 'session.context_changed' ``` ### イベントデータ イベントデータには更新後のコンテキスト情報が含まれます。 ```php theme={null} use Revolution\Copilot\Facades\Copilot; Copilot::start(function ($session) { $session->on(function ($event) { if ($event->type === 'session.context_changed') { $data = $event->data; echo "作業ディレクトリが変更されました\n"; echo " cwd: {$data['cwd']}\n"; echo " repository: {$data['repository']}\n"; echo " branch: {$data['branch']}\n"; } }); $response = $session->sendAndWait( prompt: 'Change to a different directory and list files', ); }); ``` ## SessionMetadata `SessionMetadata` に `context` プロパティが追加されました。 ### 新しいプロパティ * `context` (`?SessionContext`): セッションの作業ディレクトリと Git 情報 ### 使用例 ```php theme={null} use Revolution\Copilot\Facades\Copilot; $sessions = Copilot::client()->listSessions(); foreach ($sessions as $metadata) { echo "Session ID: {$metadata->sessionId}\n"; echo "開始時刻: {$metadata->startTime}\n"; // context はオプショナルなので null チェックが必要 if ($metadata->context !== null) { echo "コンテキスト:\n"; echo " 作業ディレクトリ: {$metadata->context->cwd}\n"; if ($metadata->context->repository !== null) { echo " リポジトリ: {$metadata->context->repository}\n"; } if ($metadata->context->branch !== null) { echo " ブランチ: {$metadata->context->branch}\n"; } } echo "\n"; } ``` ## 実用例 ### 特定プロジェクトのセッションを取得 ```php theme={null} use Revolution\Copilot\Facades\Copilot; // Laravel Copilot SDK プロジェクトのセッションのみを取得 $sessions = Copilot::client()->listSessions([ 'repository' => 'invokable/laravel-copilot-sdk', ]); // 作業中セッションを表示 foreach ($sessions as $metadata) { echo "{$metadata->sessionId}: {$metadata->summary}\n"; echo " ブランチ: {$metadata->context->branch}\n"; } ``` ### 機能ブランチのセッションを管理 ```php theme={null} use Revolution\Copilot\Facades\Copilot; $sessions = Copilot::client()->listSessions(); $featureSessions = array_filter($sessions, function ($metadata) { return $metadata->context !== null && str_starts_with($metadata->context->branch ?? '', 'feature/'); }); // 機能ブランチごとにセッションをグループ化 $byBranch = []; foreach ($featureSessions as $metadata) { $branch = $metadata->context->branch; $byBranch[$branch][] = $metadata; } foreach ($byBranch as $branch => $sessions) { echo "{$branch}: " . count($sessions) . " セッション\n"; } ``` ### 作業ディレクトリごとにセッションを整理 ```php theme={null} use Revolution\Copilot\Facades\Copilot; $sessions = Copilot::client()->listSessions(); // 作業ディレクトリでグループ化 $byCwd = []; foreach ($sessions as $metadata) { if ($metadata->context !== null) { $cwd = $metadata->context->cwd; $byCwd[$cwd][] = $metadata; } } // ディレクトリごとのセッション数を表示 foreach ($byCwd as $cwd => $sessions) { echo "{$cwd}: " . count($sessions) . " セッション\n"; } ``` ## 注意事項 * `context` は、Git リポジトリ内で作成したセッションでのみ Git 関連情報(`gitRoot`, `repository`, `branch`)を含みます。 * `context` フィールドは Copilot CLI v0.0.409 以降で利用できます。古いバージョンでは `null` が返ります。 * フィルタリングは完全一致で動作します。部分一致やワイルドカードは未サポートです。 * `SessionListFilter` のすべてのフィールドはオプショナルです。フィルタ未指定時は全セッションが返ります。 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # SessionEvent Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/session-event Laravel Copilot SDK の SessionEvent を、Laravel らしいヘルパー、型判定、ブロードキャストで扱います。 ## SessionEvent Copilot からのメッセージはすべて `Revolution\Copilot\Types\SessionEvent` クラスです。 このクラスは最もよく使うため、Laravel らしい便利機能が追加されています。 ## `content()` AI からの最も重要な応答メッセージを取得します。 ```php theme={null} $response = Copilot::run('1 + 1'); echo $response->content(); // '2' // content() は null になることがあります ``` `__toString()` による暗黙の型変換でもメッセージ内容を返します。 ```php theme={null} echo (string) $response; // '2' // こちらは null になりません ``` ## Event type の判定 `isAssistantMessage()`, `isUserMessage()`, `isIdle()`, `isAssistantMessageDelta()` が使えます。 よく使う Event type があれば、さらにヘルパーが追加される場合があります。 `is()` で任意の `EventType` を判定できます。 ```php theme={null} use Revolution\Copilot\Enums\SessionEventType; if ($response->is(SessionEventType::HOOK_START)) { // フック開始イベント時の処理 } ``` `type()` は `SessionEventType` enum の文字列値を返します。 ```php theme={null} echo $response->type(); // 'assistant.message' ``` ## `failed()` / `successful()` イベントタイプが `SESSION_ERROR` の時は `failed()` が true になります。 `successful()` はその逆です。 Laravel の命名に合わせるため、`isError()` から改名されています。 ## `throw()` Laravel の HTTP API や Process API と同じように、エラーは保持され、`throw()` で例外を送出できます。 エラーがない場合は何もしないため、次のように書けます。 ```php theme={null} $content = $response->throw()->content(); ``` イベントタイプが `SESSION_ERROR` の場合は `Revolution\Copilot\Exceptions\SessionErrorException` を送出します。 タイムアウト時は `Revolution\Copilot\Exceptions\SessionTimeoutException` を送出します。 JSON-RPC のエラーでは `Revolution\Copilot\Exceptions\JsonRpcException` を送出します。 ## Conditionable `when()` と `unless()` が使えます。 ```php theme={null} $response->when($response->isAssistantMessage(), function (SessionEvent $event) { // アシスタントメッセージ時の処理 }); ``` ## Dumpable `dump()` と `dd()` が使えます。 ```php theme={null} $response->dump(); ``` ## Tappable `tap()` が使えます。 ```php theme={null} return $response->tap(function (SessionEvent $event) { // 何か処理 info($event->content()); }); ``` ## InteractsWithData この機能は SessionEvent の `$data` プロパティにのみ適用されます。 `all()`, `has()`, `only()`, `collect()` などの使い慣れたヘルパーを使えます。 詳しくは [InteractsWithData](/jp/advanced/interacts-with-data) を参照してください。 SessionEvent の `$data` は `EventType` ごとに構造が異なるため、イベントごとの値アクセスに便利です。 `content()` も内部的に InteractsWithData を使っています。 ```php theme={null} return $this->data('content', $default); // default を指定すると content() は null になりません echo $response->content(''); ``` ## `toArray()` / `toJson()` SessionEvent 全体を配列または JSON に変換できます。 ```php theme={null} $array = $response->toArray(); $json = $response->toJson(); ``` イベント全体向けの `collect()` メソッドはありません。 `$data` には InteractsWithData の `collect()` があるためです。 イベント全体をコレクション操作したい場合は Laravel の `collect()` ヘルパーを使います。 ```php theme={null} $collect = collect($response->toArray()); ``` ## `broadcast()` / `broadcastNow()` Laravel AI SDK の `StreamEvent` と同じインターフェースでブロードキャストできます。 AI SDK ではストリーミング時に使いますが、SessionEvent では任意の `EventType` で使えます。 ```php theme={null} use Illuminate\Broadcasting\Channel; $event->broadcast(new Channel('channel-name')); ``` `broadcastNow()` はキューを経由せずに即時ブロードキャストします。 ```php theme={null} $event->broadcastNow(new Channel('channel-name')); ``` ストリーミング時の利用例: ```php theme={null} use Illuminate\Broadcasting\Channel; use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Types\SessionConfig; use Revolution\Copilot\Types\SessionEvent; Copilot::start(function (CopilotSession $session) { $session->on(function (SessionEvent $event): void { if ($event->isAssistantMessageDelta()) { $event->broadcastNow(new Channel('copilot')); } }); $session->sendAndWait(prompt: 'Tell me something about Laravel.'); }, config: new SessionConfig(streaming: true)); ``` 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # セッションライフサイクルイベント Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/session-lifecycle-event Laravel Copilot SDK のセッション作成・更新・削除・フォアグラウンド状態変更イベントを購読する方法を説明します。 ## セッションライフサイクルイベント セッションライフサイクルイベントは、セッションの作成、削除、更新、およびフォアグラウンド/バックグラウンドの状態変更(TUI+サーバーモード時)を通知するイベントです。 ## イベントタイプ `SessionLifecycleEventType` enum で定義されている 5 つのイベントタイプがあります。 | イベント | 説明 | | - | - | | `session.created` | 新しいセッションが作成された | | `session.deleted` | セッションが削除された | | `session.updated` | セッションが更新された | | `session.foreground` | セッションがフォアグラウンドに移動した | | `session.background` | セッションがバックグラウンドに移動した | ```php theme={null} use Revolution\Copilot\Enums\SessionLifecycleEventType; // イベントタイプの値を取得 echo SessionLifecycleEventType::SESSION_CREATED->value; // 'session.created' ``` ## イベントの購読 `onLifecycle()` メソッドを使用してセッションライフサイクルイベントを購読できます。 ```php theme={null} use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Types\SessionLifecycleEvent; // Clientを取得してライフサイクルイベントを購読 $client = Copilot::client(); // 全てのライフサイクルイベントを購読 $unsubscribe = $client->onLifecycle(function (SessionLifecycleEvent $event) { match ($event->type) { SessionLifecycleEventType::SESSION_CREATED => info("セッション作成: {$event->sessionId}"), SessionLifecycleEventType::SESSION_DELETED => info("セッション削除: {$event->sessionId}"), SessionLifecycleEventType::SESSION_FOREGROUND => info("フォアグラウンド: {$event->sessionId}"), SessionLifecycleEventType::SESSION_BACKGROUND => info("バックグラウンド: {$event->sessionId}"), default => null, }; }); // 購読を解除 $unsubscribe(); ``` ## SessionLifecycleEvent `SessionLifecycleEvent` はライフサイクルイベントの詳細を含む readonly class です。 ```php theme={null} use Revolution\Copilot\Types\SessionLifecycleEvent; use Revolution\Copilot\Types\SessionLifecycleEventMetadata; readonly class SessionLifecycleEvent implements Arrayable { public function __construct( public SessionLifecycleEventType $type, // イベントタイプ public string $sessionId, // セッションID public ?SessionLifecycleEventMetadata $metadata = null, // メタデータ(削除時は含まれない) ) {} } ``` ### SessionLifecycleEventMetadata セッションのメタデータを含むクラスです(削除イベントには含まれません)。 ```php theme={null} readonly class SessionLifecycleEventMetadata implements Arrayable { public function __construct( public string $startTime, // セッション開始時刻 public string $modifiedTime, // 最終更新時刻 public ?string $summary = null, // セッションの要約 ) {} } ``` ## フォアグラウンドセッション管理 TUI+サーバーモード(`--ui-server`)で動作しているサーバーに接続している場合、フォアグラウンドセッションを管理できます。 ### 現在のフォアグラウンドセッションを取得 ```php theme={null} use Revolution\Copilot\Facades\Copilot; $client = Copilot::client(); // TUIに表示されている現在のセッションIDを取得 $sessionId = $client->getForegroundSessionId(); if ($sessionId !== null) { echo "現在のフォアグラウンドセッション: {$sessionId}"; } ``` ### フォアグラウンドセッションを設定 ```php theme={null} use Revolution\Copilot\Facades\Copilot; $client = Copilot::client(); // TUIに特定のセッションを表示 $client->setForegroundSessionId('session-123'); ``` ## ForegroundSessionInfo フォアグラウンドセッションの情報を含む readonly class です。 ```php theme={null} use Revolution\Copilot\Types\ForegroundSessionInfo; readonly class ForegroundSessionInfo implements Arrayable { public function __construct( public ?string $sessionId = null, // フォアグラウンドセッションID public ?string $workspacePath = null, // ワークスペースパス ) {} } ``` ## TCPモードでの使用 TUI+サーバーモードでCopilot CLIを起動し、SDKから接続することでライフサイクルイベントを活用できます。 ```bash theme={null} # TUI+サーバーモードでCopilot CLIを起動 copilot --ui-server --port 8080 ``` ```php theme={null} use Revolution\Copilot\Facades\Copilot; // TCPモードでサーバーに接続 $client = Copilot::useTcp('tcp://127.0.0.1:8080')->client(); // ライフサイクルイベントを購読 $client->onLifecycle(function (SessionLifecycleEvent $event) { // イベント処理 }); ``` ## 注意事項 * `session.foreground` と `session.background` イベントは、TUI+サーバーモードでのみ発生します * セッション削除時(`session.deleted`)には `metadata` プロパティは `null` になります * `onLifecycle()` は購読解除用のコールバック関数を返します * [Session Hooks](/jp/packages/laravel-copilot-sdk/hooks) の `onSessionStart` や `onSessionEnd` とは別の機能です 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # Skills Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/skills 再利用可能な SKILL.md を読み込んで、ドメイン固有の指示を SessionConfig で管理する方法を説明します。 ## Skills Skills は再利用可能なプロンプトモジュールです。`SKILL.md` を読み込んで、特定ドメイン向けの指示をセッションへ追加できます。 ## 概要 * ドメイン知識をスキルとして再利用できる * プロジェクト間で同じ振る舞いを共有できる * セッション単位で有効/無効を切り替えられる Laravel 版では `SessionConfig` の `skillDirectories` と `disabledSkills` を使って設定します。 ## 基本的な使い方 `skillDirectories` に親ディレクトリを指定すると、配下の各サブディレクトリにある `SKILL.md` が読み込まれます。 ```php theme={null} use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Types\SessionConfig; Copilot::start(function (CopilotSession $session) { $response = $session->sendAndWait(prompt: 'この差分をセキュリティ観点でレビューして'); dump($response->content()); }, config: new SessionConfig( skillDirectories: [ base_path('skills'), ], )); ``` 配列形式でも指定できます。 ```php theme={null} Copilot::run( prompt: 'READMEの改善案を出して', config: [ 'skillDirectories' => [base_path('skills')], ], ); ``` ## スキルを無効化する `disabledSkills` を使うと、ロード済みスキルの一部だけ無効化できます。 ```php theme={null} $config = new SessionConfig( skillDirectories: [base_path('skills')], disabledSkills: ['experimental-feature', 'deprecated-tool'], ); ``` ## ディレクトリ構成 ```text theme={null} skills/ ├── code-review/ │ └── SKILL.md └── documentation/ └── SKILL.md ``` `skillDirectories` には `skills/` のような親ディレクトリを指定します。 ## SKILL.md の形式 `SKILL.md` は Markdown で、YAML frontmatter を任意で付けられます。 ```markdown theme={null} --- name: code-review description: Specialized code review capabilities --- # Code Review Guidelines When reviewing code, always check for: 1. Security vulnerabilities 2. Performance issues 3. Code style 4. Test coverage ``` frontmatter の主な項目: * `name`: スキル識別子(`disabledSkills` で指定する名前) * `description`: スキルの短い説明 `name` を省略した場合はディレクトリ名が使われます。 ## Custom Agents と組み合わせる `customAgents[].skills` にスキル名を指定すると、対象エージェント起動時にそのスキル内容が事前に読み込まれます。 ```php theme={null} use Revolution\Copilot\Types\SessionConfig; $config = new SessionConfig( skillDirectories: [base_path('skills/security')], customAgents: [ [ 'name' => 'security-auditor', 'description' => 'Security-focused code reviewer', 'prompt' => 'Focus on OWASP Top 10 vulnerabilities', 'skills' => ['security-scan', 'dependency-check'], ], ], ); ``` > `skills` を省略した場合、エージェントにスキル内容は注入されません。 ## RPC での確認・操作 スキルの検出や有効化/無効化は RPC からも操作できます。 ```php theme={null} // サーバーレベルでスキルを検出 $result = Copilot::client()->rpc()->skills()->discover(); // セッション内スキル一覧 $session->rpc()->skills()->list(); // セッション内で個別に有効/無効を切り替え $session->rpc()->skills()->enable(['name' => 'security-scan']); $session->rpc()->skills()->disable(['name' => 'security-scan']); ``` ## ベストプラクティス * スキルはドメインごとに分割する(`skills/security`, `skills/testing` など) * frontmatter に `name` と `description` を付ける * 競合する指示がある場合は `disabledSkills` で明示的に除外する * まず単体で動作確認してから複数スキルを組み合わせる ## 参考 * [SessionConfig](/jp/packages/laravel-copilot-sdk/session-config) * [Custom Agents](/jp/packages/laravel-copilot-sdk/custom-agents) * [RPC](/jp/packages/laravel-copilot-sdk/rpc) # ステアリングとキューイング Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/steering Laravel Copilot SDK の steering(immediate)と queueing(enqueue)の違いと、Laravel での実用的な使い分けを説明します。 ## ステアリングとキューイング 通常の Laravel アプリケーションのフローでは、ステアリングを直接使う場面はほとんどありません。Laravel の同期的な実行パターンでは恩恵を受けづらく、基本はデフォルトのキューイング運用が適しています。 エージェントが処理中にメッセージを送る方法として、**ステアリング**と**キューイング**の 2 つのパターンがあります。 ## 概要 `send()` / `sendAndWait()` / `sendAndStream()` / `Copilot::run()` のすべてに `$mode` パラメータがあります。 | モード | 動作 | | - | - | | `"enqueue"`(デフォルト) | 現在のターンが完了した後にキューとして処理される | | `"immediate"`(ステアリング) | 現在処理中のターンに割り込んで即座に注入される | ## ステアリング(`"immediate"`) エージェントが処理中のターンに直接メッセージを注入します。エージェントはリアルタイムでメッセージを受け取り、応答を調整します。進行中の作業を中断せずに方向修正したい場合に使います。 ```php theme={null} use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Contracts\CopilotSession; Copilot::start(function (CopilotSession $session) { // 長時間かかるタスクを開始 $messageId = $session->send(prompt: 'Refactor the authentication module to use sessions'); // エージェントが処理中に方向修正 $session->send( prompt: 'Actually, use JWT tokens instead of sessions', mode: 'immediate', ); // アイドル状態になるまで待機 $response = $session->sendAndWait(prompt: 'Summarize what you did'); }); ``` ステアリングはベストエフォートです。エージェントがすでにツール呼び出しをコミットしていた場合、そのツール呼び出し完了後に適用されますが、同一ターン内で処理されます。ターンが完了した後にステアリングメッセージが届いた場合は自動的にキューに移動されます。 ## キューイング(`"enqueue"`) メッセージをキューに追加し、現在のターンが完了してから順次処理します。各キューされたメッセージは独立したターンとして実行されます。`$mode` を省略した場合のデフォルト動作です。 ```php theme={null} Copilot::start(function (CopilotSession $session) { // 最初のタスクを開始 $session->send(prompt: 'Set up the project structure'); // エージェントが処理中に後続タスクをキューへ追加 $session->send(prompt: 'Add unit tests for the auth module', mode: 'enqueue'); $session->send(prompt: 'Update the README with setup instructions', mode: 'enqueue'); // キューが空になるまで待つ }); ``` ## Laravelでの実用的な考え方 Laravel では `Copilot::run()` や `sendAndWait()` を使う同期的なパターンが中心です。これらはメッセージを送ってアイドル状態になるまで待機するため、**エージェントが処理中に割り込む機会がなく、`$mode` は実質的に意味をもちません**。 ```php theme={null} // Copilot::run() は内部的に sendAndWait() を呼び出すため // mode を指定しても意味がない $response = Copilot::run(prompt: 'Tell me something about Laravel.'); // セッション内でも sendAndWait() を順番に呼ぶだけなら不要 Copilot::start(function (CopilotSession $session) { $r1 = $session->sendAndWait(prompt: 'First task'); $r2 = $session->sendAndWait(prompt: 'Second task'); }); ``` ステアリング(`"immediate"`)が活きるのは、`send()` で非同期的にメッセージを送りつつ、並行して別のメッセージで進行中のターンに介入できる場合です。Laravel の同期的な処理フローでは、その機会を作るのが難しいため、**基本的には `$mode` を指定せずデフォルトのままにしておくのが適切です**。 ## どちらを使うべきか | 場面 | パターン | | - | - | | エージェントが間違った方向に進んでいる | ステアリング(`"immediate"`) | | 次にやることを思いついた | キューイング(`"enqueue"`) | | 複数タスクを順番に実行したい | キューイング(`"enqueue"`) | | 通常の Laravel アプリ | デフォルト(`$mode` 不要) | ## ベストプラクティス * **デフォルトはキューイング** — `$mode` を省略(または `"enqueue"` を指定)するのがほとんどのケースで適切です。予測可能な動作をします。 * **ステアリングは修正用に** — `"immediate"` はエージェントが明らかに間違ったことをしている場合に限定して使います。 * **ステアリングメッセージは簡潔に** — 現在のコンテキストで理解できる短いメッセージにします。長く複雑なステアリングメッセージはかえって混乱を招きます。 * **連続ステアリングは避ける** — 短時間に複数のステアリングを送るとターンの品質が低下する可能性があります。大幅な方向転換が必要な場合はターンを中断して最初からやり直す方が適切なこともあります。 ## 参考 * [基本的な使い方](/jp/packages/laravel-copilot-sdk/bare-usage) * [Streaming](/jp/packages/laravel-copilot-sdk/streaming) * [send-on](/jp/packages/laravel-copilot-sdk/send-on) 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # ストリーミング Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/streaming Laravel Copilot SDK のストリーミング応答を CLI、SSE、WebSocket、Livewire で扱う方法を説明します。 ## ストリーミング SessionConfigで `streaming: true`を指定すると、Copilotからの応答をストリーミングで受け取れます。 元からstdioモードでもTCPモードでもストリーミングのような動作です。違いは`ASSISTANT_MESSAGE_DELTA`を受信するようになることです。 ```php theme={null} use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Types\SessionEvent; $config = new SessionConfig( streaming: true, ); Copilot::start(function (CopilotSession $session) { $session->on(function (SessionEvent $event): void { if ($event->isAssistantMessageDelta()) { // deltaではメッセージが小分けで届く。途中経過の表示などに使う。もしくはこれだけで完結させることも可能。流暢な表示を実現できる。 echo $event->deltaContent(); } elseif ($event->isAssistantMessage()) { // 小分けにされてたdeltaの送信が終わったらASSISTANT_MESSAGEとして全文を含む一つ分のメッセージが送信される。 // 次のメッセージがあれば再度deltaで届く。 // 一つのプロンプトに対して複数のメッセージが届くこともあるのでstreamingの場合は複数のdeltaと全文1回が繰り返される。 } elseif ($event->isAssistantReasoningDelta()) { // reasoning deltaも同様に小分けで届く。 echo $event->deltaContent(); } }); $session->sendAndWait(prompt: 'Tell me something about Laravel.'); }, config: $config); ``` * 流暢な表示のためには改行を追加せずそのまま表示します。 * Laravel Promptsの`spin()`は表示が崩れるので一緒に使いません。 ## 具体的な使用パターン ### Artisanコマンド 上記のように`echo`もしくは`$this->output->write()`で直接出力します。Copilot CLIを直接使った場合と同じ表示なので理解しやすいです。Laravel Promptsの`stream()`を使うとフェードイン効果付きでの表示もできます。 ```php theme={null} use function Laravel\Prompts\stream; Artisan::command('copilot:streaming {--resume=}', function () { $config = new SessionConfig( streaming: true, ); Copilot::start(function (CopilotSession $session) { info('Starting Copilot with streaming: '.$session->id()); $stream = stream(); $session->on(function (SessionEvent $event) use ($stream) { if ($event->isAssistantMessageDelta()) { // deltaで小分けにされたメッセージが届く。Laravel Promptsのstream()を使ってフェードインの効果付きで流暢に表示。 $stream->append($event->deltaContent()); } elseif ($event->isIdle()) { // idleまで完了したらclose。同じセッション中に複数回プロンプトを送信しても正常に動作するので再度の`$stream = stream()`は不要。 $stream->close(); } }); while (true) { $prompt = text( label: 'Enter your prompt', placeholder: 'Ask me anything...', required: true, hint: 'Ctrl+C to exit', ); $session->sendAndWait($prompt); } }, config: $config, resume: $this->option('resume')); })->purpose('Copilot streaming'); ``` ### WebページでServer-Sent Events (SSE)として配信 #### `response()->eventStream()`を使う書き方 `response()->eventStream()`が追加されたのはLaravel12リリース直前くらいの時期です。このパッケージはLaravel12以上のみ対応なので使用可能です。 ```php theme={null} Route::get('/copilot/sse', function () { return response()->eventStream(function () { yield from Copilot::stream(function (CopilotSession $session) { foreach ($session->sendAndStream('Tell me something about Laravel.') as $event) { if ($event->isAssistantMessageDelta()) { yield $event->deltaContent(); } } }, config: new SessionConfig(streaming: true)); }); }); Route::get('/copilot', function () { return view('copilot'); }); ``` #### `response()->stream()`を使う書き方 ```php theme={null} Route::get('/copilot/sse', function () { return response()->stream(function () { Copilot::start(function (CopilotSession $session) { $session->on(function (SessionEvent $event) { if ($event->isAssistantMessageDelta()) { echo "event: update\n"; echo 'data: '.$event->deltaContent()."\n\n"; ob_flush(); flush(); } }); $session->sendAndWait('Tell me something about Laravel.'); }, config: new SessionConfig(streaming: true)); echo "event: update\n"; echo "data: \n\n"; ob_flush(); flush(); }, 200, [ 'Content-Type' => 'text/event-stream', 'Cache-Control' => 'no-cache', 'Connection' => 'keep-alive', 'X-Accel-Buffering' => 'no', ]); }); Route::get('/copilot', function () { return view('copilot'); }); ``` #### フロントエンド側のコード例 `copilot.blade.php`は簡易的な表示確認用です。本番用にはReactかVueを使っているならLaravel公式のnpmパッケージを使うのが推奨です。`@laravel/stream-react`や`@laravel/stream-vue` ```html theme={null}

Copilot SSE Test

``` ## WebSocketでの配信 Laravelの通知機能、ブロードキャスト機能、Reverbを組み合わせて`delta`をWebSocketで配信することも可能です。 ## Livewireの`wire:stream` Livewireでも`wire:stream`ディレクティブを使ってストリーミング表示が可能です。 ```php theme={null} question = $this->prompt; $this->prompt = ''; $this->answer = ''; $this->js('$wire.copilot()'); } public function copilot(): void { Copilot::start(function (CopilotSession $session) { $this->sessionId = $session->id(); $session->on(SessionEventType::ASSISTANT_MESSAGE_DELTA, function (SessionEvent $event): void { $this->stream( content: $event->deltaContent(), to: 'answer', ); $this->answer = $event->deltaContent(); }); $response = $session->sendAndWait(prompt: $this->question); $this->answer = $response->content(); }, config: ['streaming' => true], resume: $this->sessionId); } }; ?>
@if ($question)

{{ $question }}

{{ $answer }}
@endif
``` 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # ストリーミングイベント Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/streaming-events SessionConfig の streaming を有効化したときに受け取れる Session Event の種類と payload を整理します。 ## ストリーミングイベント `SessionConfig` で `streaming: true` を有効にすると、セッション中の動作がイベントとして順次配信されます。 このページは、公式の `streaming-events.md` を Laravel 向けに読みやすくした日本語版です。 > [SessionEvent](/jp/packages/laravel-copilot-sdk/session-event) ページ は Laravel 向けに拡張した `SessionEvent` クラス自体の説明です。 > このページは「どのイベントタイプで、どのデータが届くか」のリファレンスです。 ## 概要 Copilot エージェントの処理(推論、メッセージ生成、ツール実行、権限確認など)はすべてセッションイベントとして流れます。 * **Ephemeral event**: リアルタイム配信のみ。セッションログには永続化されない(再開時に再生されない) * **Persisted event**: セッションログに保存される(再開時に再生される) * **Delta event**: 断片的に届く差分イベント(`deltaContent` など)。連結して全文を構成する * **`parentId` chain**: 各イベントが直前イベントIDを参照する連鎖 ## Event envelope(共通フィールド) すべてのイベントは以下の共通構造を持ちます。 | Field | Type | 説明 | | - | - | - | | `id` | `string` | イベント固有ID(UUID v4) | | `timestamp` | `string` | 作成時刻(ISO 8601) | | `parentId` | `string \| null` | 直前イベントID(先頭イベントは `null`) | | `ephemeral` | `boolean?` | 一時イベントの場合 `true` | | `type` | `string` | イベントタイプ | | `data` | `object` | イベント固有のペイロード | ## Laravel での購読例 ```php theme={null} use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Enums\SessionEventType; use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Types\SessionConfig; use Revolution\Copilot\Types\SessionEvent; Copilot::start(function (CopilotSession $session): void { // 全イベント $session->on(function (SessionEvent $event): void { info($event->type(), $event->toArray()); }); // 特定イベント $session->on(SessionEventType::ASSISTANT_MESSAGE_DELTA, function (SessionEvent $event): void { echo $event->deltaContent(); }); $session->sendAndWait(prompt: 'Laravelの特徴を教えて'); }, config: new SessionConfig(streaming: true)); ``` ## 主なイベントカテゴリ ## Assistant events ### `assistant.turn_start` ターン開始。 * `turnId`(必須) * `interactionId`(任意) ### `assistant.intent`(ephemeral) 現在の実行意図(例: `Exploring codebase`)。 * `intent`(必須) ### `assistant.reasoning` 推論ブロックの完成版。 * `reasoningId`(必須) * `content`(必須) ### `assistant.reasoning_delta`(ephemeral) 推論テキストの差分。 * `reasoningId`(必須) * `deltaContent`(必須) ### `assistant.message` アシスタントの完成メッセージ。 主なフィールド: * `messageId`(必須) * `content`(必須) * `toolRequests`(任意) * `reasoningOpaque` / `reasoningText` / `encryptedContent`(任意) * `phase` / `outputTokens` / `interactionId`(任意) * `parentToolCallId`(任意、サブエージェント由来時) ### `assistant.message_delta`(ephemeral) メッセージ本文の差分。 * `messageId`(必須) * `deltaContent`(必須) * `parentToolCallId`(任意) ### `assistant.turn_end` ターン終了。 * `turnId`(必須) ### `assistant.usage`(ephemeral) API 呼び出し単位の使用量情報。 主なフィールド: * `model`(必須) * `inputTokens` / `outputTokens` / `cost` / `duration`(任意) * `apiCallId` / `providerCallId`(任意) * `quotaSnapshots` / `copilotUsage`(任意) ### `assistant.streaming_delta`(ephemeral) 低レベルな受信進捗。 * `totalResponseSizeBytes`(必須) ## Tool execution events ### `tool.execution_start` ツール実行開始。 * `toolCallId`(必須) * `toolName`(必須) * `arguments` / `mcpServerName` / `mcpToolName` / `parentToolCallId`(任意) ### `tool.execution_partial_result`(ephemeral) ツール実行中の部分出力。 * `toolCallId`(必須) * `partialOutput`(必須) ### `tool.execution_progress`(ephemeral) 進捗メッセージ。 * `toolCallId`(必須) * `progressMessage`(必須) ### `tool.execution_complete` ツール実行完了(成功/失敗)。 * `toolCallId`(必須) * `success`(必須) * `result`(成功時) * `error`(失敗時) * `toolTelemetry` / `parentToolCallId`(任意) ### `tool.user_requested` ユーザー明示要求でのツール呼び出し。 * `toolCallId`(必須) * `toolName`(必須) * `arguments`(任意) ## Session lifecycle events ### `session.start` セッション開始。Cloud Sessions では `producer` が `copilot-agent` の `session.start` を確認してから最初のプロンプトを送ると安全です。 * `producer`(任意) ### `session.idle`(ephemeral) 現在の処理が完了し、次の入力待ち。 * `backgroundTasks`(任意) ### `session.error` セッション処理中のエラー。 * `errorType`(必須) * `message`(必須) * `stack` / `statusCode` / `providerCallId`(任意) ### `session.compaction_start` コンテキスト圧縮開始(`data` は空オブジェクト)。 ### `session.compaction_complete` コンテキスト圧縮完了。 主なフィールド: * `success`(必須) * `error`(任意) * `preCompactionTokens` / `postCompactionTokens`(任意) * `summaryContent` / `checkpointPath`(任意) ### `session.title_changed`(ephemeral) 自動タイトル更新。 * `title`(必須) ### `session.context_changed` 作業コンテキスト変更。 * `cwd`(必須) * `gitRoot` / `repository` / `branch`(任意) ### `session.info` リモート URL などのセッション情報。 * `infoType`(必須) * `url`(任意、`infoType` が `remote` の場合など) ### `session.remote_steerable_changed` Mission Control からのリモート操作可否が変わったことを示すイベント。 * `remoteSteerable`(任意) ### `session.usage_info`(ephemeral) コンテキストウィンドウ利用状況。 * `tokenLimit`(必須) * `currentTokens`(必須) * `messagesLength`(必須) ### `session.task_complete` タスク完了通知。 * `summary`(任意) ### `session.shutdown` セッション終了。 主なフィールド: * `shutdownType`(必須) * `errorReason`(任意) * `totalPremiumRequests` / `totalApiDurationMs`(必須) * `codeChanges` / `modelMetrics`(必須) ## Permission / user input events ### `permission.requested`(ephemeral) 権限確認要求。 * `requestId`(必須) * `permissionRequest`(必須) `permissionRequest.kind`: * `shell` * `write` * `read` * `mcp` * `url` * `memory` * `custom-tool` ### `permission.completed`(ephemeral) 権限確認の解決結果。 * `requestId`(必須) * `result.kind`(必須) ### `user_input.requested`(ephemeral) ユーザーへの質問。 * `requestId`(必須) * `question`(必須) * `choices` / `allowFreeform`(任意) ### `user_input.completed`(ephemeral) ユーザー入力完了。 * `requestId`(必須) ### `elicitation.requested`(ephemeral) 構造化入力(フォーム)要求。 * `requestId`(必須) * `message`(必須) * `requestedSchema`(必須) ### `elicitation.completed`(ephemeral) 構造化入力の完了。 * `requestId`(必須) ## Sub-agent / skill events ### `subagent.started` * `toolCallId`(必須) * `agentName` / `agentDisplayName` / `agentDescription`(必須) ### `subagent.completed` * `toolCallId`(必須) * `agentName` / `agentDisplayName`(必須) ### `subagent.failed` * `toolCallId`(必須) * `agentName` / `agentDisplayName`(必須) * `error`(必須) ### `subagent.selected` * `agentName`(必須) * `agentDisplayName`(必須) * `tools`(必須、`null` 許容) ### `subagent.deselected` デフォルトエージェントへ復帰(`data` は空オブジェクト)。 ### `skill.invoked` * `name` / `path` / `content`(必須) * `allowedTools` / `pluginName` / `pluginVersion`(任意) ## Other events ### `abort` * `reason`(必須) ### `user.message` * `content`(必須) * `transformedContent` / `attachments` / `source` / `agentMode` / `interactionId`(任意) ### `system.message` * `content`(必須) * `role`(必須) * `name` / `metadata`(任意) ### `external_tool.requested`(ephemeral) * `requestId` / `sessionId` / `toolCallId` / `toolName`(必須) * `arguments`(任意) ### `external_tool.completed`(ephemeral) * `requestId`(必須) ### `exit_plan_mode.requested`(ephemeral) * `requestId` / `summary` / `planContent` / `actions` / `recommendedAction`(必須) ### `exit_plan_mode.completed`(ephemeral) * `requestId`(必須) ### `command.queued`(ephemeral) * `requestId`(必須) * `command`(必須) ### `command.completed`(ephemeral) * `requestId`(必須) ## 典型的なイベント順序 ```text theme={null} assistant.turn_start ├── assistant.intent (ephemeral) ├── assistant.reasoning_delta (ephemeral, 複数回) ├── assistant.reasoning ├── assistant.message_delta (ephemeral, 複数回) ├── assistant.message ├── assistant.usage (ephemeral) ├── [必要に応じて permission / tool.* がループ] assistant.turn_end session.idle (ephemeral) ``` ## 全イベント一覧(クイックリファレンス) | Event Type | Ephemeral | Category | | - | - | - | | `session.start` | | Session | | `assistant.turn_start` | | Assistant | | `assistant.intent` | ✅ | Assistant | | `assistant.reasoning` | | Assistant | | `assistant.reasoning_delta` | ✅ | Assistant | | `assistant.streaming_delta` | ✅ | Assistant | | `assistant.message` | | Assistant | | `assistant.message_delta` | ✅ | Assistant | | `assistant.turn_end` | | Assistant | | `assistant.usage` | ✅ | Assistant | | `tool.user_requested` | | Tool | | `tool.execution_start` | | Tool | | `tool.execution_partial_result` | ✅ | Tool | | `tool.execution_progress` | ✅ | Tool | | `tool.execution_complete` | | Tool | | `session.idle` | ✅ | Session | | `session.error` | | Session | | `session.compaction_start` | | Session | | `session.compaction_complete` | | Session | | `session.title_changed` | ✅ | Session | | `session.context_changed` | | Session | | `session.info` | | Session | | `session.remote_steerable_changed` | | Session | | `session.usage_info` | ✅ | Session | | `session.task_complete` | | Session | | `session.shutdown` | | Session | | `permission.requested` | ✅ | Permission | | `permission.completed` | ✅ | Permission | | `user_input.requested` | ✅ | User Input | | `user_input.completed` | ✅ | User Input | | `elicitation.requested` | ✅ | User Input | | `elicitation.completed` | ✅ | User Input | | `subagent.started` | | Sub-Agent | | `subagent.completed` | | Sub-Agent | | `subagent.failed` | | Sub-Agent | | `subagent.selected` | | Sub-Agent | | `subagent.deselected` | | Sub-Agent | | `skill.invoked` | | Skill | | `abort` | | Control | | `user.message` | | User | | `system.message` | | System | | `external_tool.requested` | ✅ | External Tool | | `external_tool.completed` | ✅ | External Tool | | `command.queued` | ✅ | Command | | `command.completed` | ✅ | Command | | `exit_plan_mode.requested` | ✅ | Plan Mode | | `exit_plan_mode.completed` | ✅ | Plan Mode | ## 関連ドキュメント * [公式の Streaming Events リファレンス](https://github.com/github/copilot-sdk/blob/main/docs/features/streaming-events.md) * Laravel版のイベントクラス解説: [SessionEvent](/jp/packages/laravel-copilot-sdk/session-event) * ストリーミングの実装パターン: [Streaming](/jp/packages/laravel-copilot-sdk/streaming) # TCPモード Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/tcp-mode Laravel Copilot SDK で事前起動した Copilot CLI サーバーへ接続し、低オーバーヘッドで共有運用する方法を説明します。 ## TCPモード 通常、SDK は各リクエストごとに新しい Copilot CLI プロセスを起動します(stdio モード)。 TCP モードを使うと、事前に起動した Copilot CLI サーバーへ接続できます。 ## TCPモードのメリット * **パフォーマンス向上**: プロセス起動オーバーヘッドがない * **リソース共有**: 複数の Laravel プロセスで同一 CLI サーバーを共有できる * **プロセス管理**: Laravel Forge / Laravel Cloud でバックグラウンドプロセスとして管理できる * **デプロイ対応**: デプロイ時の自動再起動に対応できる ## 使い方 ### 1. Copilot CLIサーバーを起動 ```shell theme={null} copilot --headless --port 12345 ``` ### 2. 環境変数を設定 ```dotenv theme={null} COPILOT_URL=tcp://127.0.0.1:12345 # COPILOT_URL=http://127.0.0.1:12345 # COPILOT_URL=127.0.0.1:12345 # COPILOT_URL=12345 # COPILOT_URL=127.0.0.1 # COPILOT_URL=localhost ``` これだけで、SDK は stdio モードから TCP モードに自動で切り替わります。 * `tcp://` は任意です。`http://` やスキームなしでも動作します。 * ポートだけの指定も可能です。その場合ホストは自動で `127.0.0.1` になります。 * ホストだけの指定は `127.0.0.1` と `localhost` のみ有効です。その場合ポートはデフォルト `12345` になります。 ## 設定ファイル `config/copilot.php` で TCP 接続を設定できます。 ```php theme={null} return [ // TCP接続先URL(設定するとTCPモードになる) 'url' => env('COPILOT_URL'), // 以下はstdioモード時のみ使用される 'cli_path' => env('COPILOT_CLI_PATH', 'copilot'), 'cli_args' => [], 'cwd' => null, 'log_level' => env('COPILOT_LOG_LEVEL', 'info'), // 両モード共通 'timeout' => env('COPILOT_TIMEOUT', 60), 'model' => env('COPILOT_MODEL'), ]; ``` `COPILOT_URL`(`cli_url`)と `COPILOT_CLI_PATH` を両方設定した場合は TCP モードが優先されます。 ## 実行時のモード切り替え 通常は設定ファイルに従って TCP モードか stdio モードかが自動で選ばれます。 コード内で明示的に切り替えることも可能です。 ```php theme={null} use Revolution\Copilot\Facades\Copilot; // TCPモードに切り替える $response = Copilot::useTcp(url: 'tcp://127.0.0.1:12345')->run(prompt: 'Hello, TCP mode!'); // 省略時は設定ファイルの値を使う $response = Copilot::useTcp()->run(prompt: 'Hello, TCP mode!'); // stdioモードに切り替える $stdioConfig = [ 'cli_path' => 'copilot', 'cli_args' => [], 'cwd' => base_path(), 'log_level' => 'info', ]; $response = Copilot::useStdio($stdioConfig)->run(prompt: 'Hello, stdio mode!'); // 省略時は設定ファイルの値を使う // 両モードを設定している場合はTCPが優先なので、一時的な上書きに使える $response = Copilot::useStdio()->run(prompt: 'Hello, stdio mode!'); ``` サーバーによっては TCP モードで正常に動作しないことがあります。 キュー処理は TCP モード、HTTP リクエスト内の処理は stdio モードのように使い分けることも可能です。 ## Laravel Forge / Laravel Cloudでの運用 ### Laravel Forge 1. **Daemon を作成**: Forge 管理画面で Daemon を作成します。 ``` Command: copilot --headless --port 12345 User: forge Directory: /home/forge/your-app ``` 2. **環境変数を設定**: `.env` に `COPILOT_URL` を追加します。 3. **デプロイ時に再起動**: デプロイスクリプトで Daemon を再起動します。 現在の Forge 環境では不要な場合があります。 ```shell theme={null} sudo supervisorctl restart daemon-123456:* ``` ### Laravel Cloud Laravel Cloud のワーカー機能を使い、バックグラウンドプロセスとして実行できます。 詳しくは [Laravel Cloudでの使い方](/jp/packages/laravel-copilot-sdk/laravel-cloud) を参照してください。 ## 注意事項 ### セキュリティ TCPサーバーは可能な限りローカルバインド(`127.0.0.1`)を使ってください。 外部公開する場合はファイアウォール設定を適切に行ってください。 ### 再接続 現在のバージョンには自動再接続機能がありません。接続が切れると例外がスローされます。 ### 現在モードの確認 プログラム内でクライアントがどちらのモードか確認できます。 ```php theme={null} use Revolution\Copilot\Facades\Copilot; $client = Copilot::client(); if ($client->isTcpMode()) { // TCPモード } else { // stdioモード } ``` ## トラブルシューティング ### 接続できない 1. Copilot CLI サーバーが起動しているか確認 ```shell theme={null} ps aux | grep copilot ``` 2. ポートが正しいか確認 ```shell theme={null} netstat -an | grep 12345 ``` 3. ファイアウォール設定を確認 ### タイムアウトエラー `config/copilot.php` の `timeout` 値を増やしてください。 ```php theme={null} 'timeout' => 120, // 2分 ``` 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # Telemetry Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/telemetry Laravel Copilot SDK で OpenTelemetry を有効化し、アプリと Copilot CLI の Trace Context を連携する方法を説明します。 ## OpenTelemetry Copilot CLI には OpenTelemetry によるトレース機能が組み込まれています。SDK 側で設定するだけで、CLI プロセスのトレースデータを収集できます。 ## 基本的な使い方 ### CLIプロセスのトレース有効化 `config/copilot.php` に telemetry 設定を追加するか、直接オプションで指定します。 ```php theme={null} // config/copilot.php 'telemetry' => [ 'otlpEndpoint' => 'http://localhost:4318', ], ``` または直接指定します。 ```php theme={null} use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Types\TelemetryConfig; Copilot::useStdio([ 'telemetry' => new TelemetryConfig( otlpEndpoint: 'http://localhost:4318', ) ]); $response = Copilot::run(prompt: 'Hello'); ``` クライアントを直接使う場合: ```php theme={null} use Revolution\Copilot\Client; use Revolution\Copilot\Types\TelemetryConfig; $client = new Client([ 'telemetry' => new TelemetryConfig( otlpEndpoint: 'http://localhost:4318', ), ]); ``` ### TelemetryConfigオプション | オプション | 説明 | | - | - | | `otlpEndpoint` | OTLP HTTPエンドポイントURL | | `filePath` | JSON-linesトレース出力のファイルパス | | `exporterType` | `"otlp-http"` または `"file"` | | `sourceName` | インストルメンテーションスコープ名 | | `captureContent` | メッセージ内容(プロンプト、レスポンス)をキャプチャするか | ## W3C Trace Context プロパゲーション ほとんどのユーザーにはこの機能は不要です。上記の `TelemetryConfig` だけで CLI のトレースを収集できます。以下はアプリケーション側で独自の OpenTelemetry スパンを作成し、CLI のスパンと同じ分散トレースに表示させたい場合の高度な機能です。 SDK は `session.create`、`session.resume`、`session.send` の JSON-RPC リクエストに W3C Trace Context(`traceparent`/`tracestate`)を自動的に注入します。 ### 自動プロパゲーション(推奨) `open-telemetry/api` パッケージをインストールするだけで、トレースコンテキストが自動的に伝搬されます。 ```shell theme={null} composer require open-telemetry/api open-telemetry/sdk ``` インストール後は特別な設定不要で、アプリケーションのスパンと CLI のスパンが同一の分散トレースにリンクされます。 ### カスタムプロバイダー 独自のトレースコンテキスト取得ロジックを使いたい場合: ```php theme={null} use Revolution\Copilot\Support\TraceContext; TraceContext::useProvider(function (): array { return [ 'traceparent' => '00-traceid-spanid-01', 'tracestate' => 'vendor=value', ]; }); ``` ### SDK → CLI(アウトバウンド) 以下の RPC 呼び出しに自動的に `traceparent`/`tracestate` が注入されます。 * `session.create` — セッション作成時 * `session.resume` — セッション再開時 * `session.send` — メッセージ送信時 ### CLI → SDK(インバウンド) CLI がツールを呼び出す際、ツールハンドラの `$invocation` 配列にトレースコンテキストが含まれます。 ```php theme={null} $session->registerTools([ [ 'name' => 'my-tool', 'description' => 'My custom tool', 'handler' => function (array $args, array $invocation) { // CLIのスパンからのトレースコンテキスト $traceparent = $invocation['traceparent'] ?? null; $tracestate = $invocation['tracestate'] ?? null; // open-telemetry/apiがインストールされていれば // コンテキストは自動的に復元され、 // ここで作成するスパンはCLIのスパンの子になります return 'result'; }, ], ]); ``` `open-telemetry/api` がインストールされている場合、ツールハンドラ実行中の OpenTelemetry コンテキストは CLI のスパンに自動的にリンクされます。明示的に `traceparent` を使う必要はありません。 ## 依存関係 | パッケージ | 用途 | | - | - | | `open-telemetry/api` | 自動 Trace Context 伝搬(suggest、オプション) | | `open-telemetry/sdk` | トレースデータのエクスポート | `open-telemetry/api` は `composer.json` の `suggest` に含まれています。インストールされていなくても、SDK 自体は正常に動作します。 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # ツール Source: https://kawax.biz/jp/packages/laravel-copilot-sdk/tools Laravel Copilot SDK でカスタムツールを定義・処理し、権限設定、呼び出しメタデータ、MCP結果変換を扱います。 ## ツール Copilot CLIのビルトインツールはデフォルトで有効です。ここで指定できるのはカスタムツールです。 ## 基本的な使い方 SessionConfigの `tools` にツールの定義を指定します。 `Tool::define()`は他言語版の`defineTool`と同様のヘルパーです。 parametersにはLaravel自身がLaravel MCPで使っているJsonSchemaを使用可能です。JsonSchemaを使わず直接配列を指定も可能です。 ```php theme={null} use Illuminate\Support\Facades\Artisan; use Illuminate\JsonSchema\JsonSchema; use Revolution\Copilot\Contracts\CopilotSession; use Revolution\Copilot\Facades\Copilot; use Revolution\Copilot\Types\SessionConfig; use Revolution\Copilot\Types\Tool; use Revolution\Copilot\Types\ToolResultObject; use function Laravel\Prompts\{info, note, spin, warning}; Artisan::command('copilot:tools', function () { $facts = [ 'PHP' => 'A popular general-purpose scripting language that is especially suited to web development.', 'Laravel' => 'A web application framework with expressive, elegant syntax.', ]; $parameters = JsonSchema::object( properties: [ 'topic' => JsonSchema::string() ->description('Topic to look up (e.g., "PHP", "Laravel")') ->required(), ], )->toArray(); $config = new SessionConfig( tools: [ Tool::define( name: 'lookup_fact', description: 'Returns a fun fact about a given topic.', parameters: $parameters, handler: function (array $params, array $invocation) use ($facts): array { $topic = $params['topic'] ?? ''; $fact = $facts[$topic] ?? null; if (! $fact) { return new ToolResultObject( textResultForLlm: "No fact stored for {$topic}.", resultType: 'failure', sessionLog: "lookup_fact: missing topic {$topic}", toolTelemetry: [], ); } return new ToolResultObject( textResultForLlm: $fact, resultType: 'success', sessionLog: "lookup_fact: served {$topic}", toolTelemetry: [], ); }, overridesBuiltInTool: false, skipPermission: false, // trueにするとパーミッションプロンプトなしで実行 defer: 'auto', // ツールを常にプリロードするのではなく、遅延ロード(ツール検索による遅延ロード)するかどうかを制御します。`"auto"` の場合、ツールは遅延ロードされ、ツール検索によって表示されます。`"never"` の場合、ツールは常にプリロードされます。オプション。デフォルトは `"auto" です。 ), ], ); Copilot::start(function (CopilotSession $session) { info('Starting Copilot with tools: '.$session->id()); $prompt = 'You can call the lookup_fact tool. Use lookup_fact to tell me something about Laravel.'; warning($prompt); $response = spin( callback: fn () => $session->sendAndWait($prompt), message: 'Copilot thinking...', ); note($response->content()); }, config: $config); }); ``` ## skipPermission `skipPermission: true` を指定すると、そのツールはパーミッションプロンプトなしで実行できます。 ```php theme={null} Tool::define( name: 'read_only_lookup', description: 'Read-only data lookup.', parameters: $parameters, handler: fn ($params) => ['result' => 'data'], skipPermission: true, ), ``` ## \$invocation ツールハンドラの第2引数 `$invocation` には以下のフィールドが渡されます。 ```php theme={null} [ 'sessionId' => '...', 'toolCallId' => '...', 'toolName' => 'lookup_fact', 'arguments' => [...], // ツール引数($params と同じ内容) // OpenTelemetryが有効な場合のみ含まれる 'traceparent' => '...', // W3C Trace Context traceparent 'tracestate' => '...', // W3C Trace Context tracestate ] ``` ## プロトコルの詳細 Protocol v3(現在のデフォルト)では、ツール呼び出しはJSON-RPCリクエストではなくセッションイベント(`external_tool.requested`)としてブロードキャストされます。SDKはこのイベントを内部的に処理して `session.tools.handlePendingToolCall` RPCで応答します。 **`SessionConfig` の使い方は変わりません。** `tools` に定義を渡すだけで、プロトコルの違いはSDKが吸収します。 ## MCP CallToolResult の変換 MCPサーバーのツール結果(`CallToolResult`)をCopilot SDKの`ToolResultObject`に変換するには`McpCallToolResult::convert()`を使います。 MCPツールの結果はtext・image・resourceなどの`content`ブロック配列ですが、Copilot SDKは`ToolResultObject`形式を期待するため変換が必要です。 ```php theme={null} use Revolution\Copilot\Support\McpCallToolResult; // MCP CallToolResult形式 $mcpResult = [ 'content' => [ ['type' => 'text', 'text' => 'File contents here'], ['type' => 'image', 'data' => 'base64...', 'mimeType' => 'image/png'], ], 'isError' => false, ]; // Copilot SDK ToolResultObject形式に変換 $toolResult = McpCallToolResult::convert($mcpResult); // $toolResult->textResultForLlm => 'File contents here' // $toolResult->resultType => 'success' // $toolResult->binaryResultsForLlm => [['data' => 'base64...', 'mimeType' => 'image/png', 'type' => 'image']] ``` 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-copilot-sdk) を参照してください。 # Google Sheets API for Laravel Source: https://kawax.biz/jp/packages/laravel-google-sheets/index Laravel で Google Sheets API v4 を使う。流暢で直感的なインターフェース。 Google Sheets API v4 の Laravel 向けパッケージです。Google PHP クライアントライブラリの複雑性を隠蔽し、直感的で表現力豊かなメソッドチェーン API を提供します。 ## 特徴 * **複数の認証方式対応** — OAuth 2.0(ユーザー個別アクセス)、Service Account(サーバー間通信)、API キー(公開データ) * **流暢な API** — メソッドチェーンで直感的にデータ操作 * **Laravel Collection 統合** — Google Sheets のデータを Laravel Collection に変換。配列処理が簡単 * **拡張性** — マクロシステムでファサードにカスタムメソッド追加可能 * **Google Drive 統合** — スプレッドシート管理や一覧表示 ## 一般的な使用場面 * **ユーザーダッシュボード** — Google Sheets データを表示・操作 * **データインポート/エクスポート** — Laravel アプリと Google Sheets 間の大量データ移行 * **自動レポート生成** — レポートをプログラムで生成・更新 * **マルチユーザーアプリケーション** — ユーザーが個別の Google Sheets を管理 ## 設計思想 このパッケージの主な目的は **Google Sheets からの読み込み** です。読み込み前に細かい条件を指定するのではなく、まず Laravel Collection として全データを取得し、その後 Laravel 側でデータ処理する設計になっています。 ## システム要件 * PHP >= 8.3 * Laravel >= 12.0 ## インストール ```bash theme={null} composer require revolution/laravel-google-sheets ``` ```bash theme={null} php artisan vendor:publish --tag="google-config" ``` [Google Cloud Console](https://console.cloud.google.com/) で以下の API を有効にします: * **Google Sheets API** * **Google Drive API** 3 つの方式から選んで設定します: * [Service Account](/jp/packages/laravel-google-sheets/service-account) — サーバー間通信・自動化タスク向け * [OAuth 2.0](/jp/packages/laravel-google-sheets/oauth) — ユーザー個別アクセス向け * API キー — 公開データのみ ## 認証方式の選択 | 方式 | 用途 | ユーザー操作 | アクセス範囲 | 難易度 | | - | - | - | - | - | | **Service Account** | サーバー間通信・自動化 | 不要 | 共有されたスプレッドシート | 中 | | **OAuth 2.0** | ユーザー向けアプリ | 同意が必要 | ユーザー個別シート | 高 | | **API キー** | 公開データのみ | 不要 | 公開スプレッドシートのみ | 低 | ## 基本的な使い方 次のスプレッドシート構造の例を考えてみましょう。 | id | name | mail | | - | - | - | | 1 | name1 | mail1 | | 2 | name2 | mail2 | Spreadsheet URL: `https://docs.google.com/spreadsheets/d/{spreadsheetID}/...` `spreadsheetId`はURLから取得してください。 ### サービスアカウント サービスアカウント認証を使用する場合、トークンの設定は不要です。 ```php theme={null} use Revolution\Google\Sheets\Facades\Sheets; // サービスアカウント認証は設定時に自動的に行われます $values = Sheets::spreadsheet('spreadsheetId')->sheet('Sheet 1')->all(); // [ // ['id', 'name', 'mail'], // ['1', 'name1', 'mail1'], // ['2', 'name2', 'mail2'] // ] ``` ### OAuth OAuth認証を使用する場合、ユーザーのアクセストークンを設定する必要があります。 ```php theme={null} use Revolution\Google\Sheets\Facades\Sheets; $user = $request->user(); $token = [ 'access_token' => $user->access_token, 'refresh_token' => $user->refresh_token, 'expires_in' => $user->expires_in, 'created' => $user->updated_at->getTimestamp(), ]; // all() は配列を返します $values = Sheets::setAccessToken($token)->spreadsheet('spreadsheetId')->sheet('Sheet 1')->all(); // [ // ['id', 'name', 'mail'], // ['1', 'name1', 'mail1'], // ['2', 'name1', 'mail2'] // ] ``` ### ヘッダーをキーとしてシートの値を取得する(推奨) コレクションの変換は簡単で、その後の処理も柔軟に対応できるため、この方法をお勧めします。 ```php theme={null} use Revolution\Google\Sheets\Facades\Sheets; // get() は Laravel コレクションを返します $rows = Sheets::sheet('Sheet 1')->get(); $header = $rows->pull(0); $values = Sheets::collection(header: $header, rows: $rows); $values->toArray() // [ // ['id' => '1', 'name' => 'name1', 'mail' => 'mail1'], // ['id' => '2', 'name' => 'name2', 'mail' => 'mail2'] // ] ``` Blade ```php theme={null} @foreach($values as $value) {{ data_get($value, 'name') }} @endforeach ``` ### A1表記法の使用 ```php theme={null} use Revolution\Google\Sheets\Facades\Sheets; $values = Sheets::sheet('Sheet 1')->range('A1:B2')->all(); // [ // ['id', 'name'], // ['1', 'name1'], // ] ``` ### A1表記について A1表記は、Googleスプレッドシートでセルまたは範囲を指定する標準的な方法です(例:「A1」、「A1:B2」)。 * 「A1」は、A列1行目のセルを指します。 * 「A1:B2」は、セルA1からB2までの範囲(四角形)を指します。 * 「A:B」は、A列とB列のすべての行を指します。 A1表記法に慣れていない場合や、範囲が動的または複雑な場合は、すべてのデータを取得してから、Laravelコレクションを使用して取得後に処理/フィルタリングする方が簡単な場合が多いです。 ## 次のステップ * [Service Account 認証](/jp/packages/laravel-google-sheets/service-account) — 最も一般的な方式 * [OAuth 2.0 認証](/jp/packages/laravel-google-sheets/oauth) — ユーザー個別アクセス * [laravel-google-sheets](https://github.com/invokable/laravel-google-sheets) * [デモプロジェクト](https://github.com/invokable/google-sheets-project) — Laravel 5.5 から 13 まで対応例 * [関連パッケージ: Google Search Console API for Laravel](https://github.com/invokable/laravel-google-searchconsole) ## ライブデモ 実際の動作は [Laravel Google Sheets デモ](https://sheets.kawax.biz/) で確認できます。 # OAuth 2.0 認証 - Google Sheets API for Laravel Source: https://kawax.biz/jp/packages/laravel-google-sheets/oauth Google Sheets API の OAuth 2.0 認証。ユーザー個別アクセス向け。 OAuth 2.0 認証ではユーザーが個別の Google Sheets にアクセス権を付与します。ユーザー個別のデータを扱うアプリケーション向けです。 ## 適用場面 * **ユーザー中心のアプリ** — ユーザーが自分の Google Sheets にアクセス * **マルチテナントアプリ** — 異なるユーザーが異なるスプレッドシートを管理 * **個人データアクセス** — ユーザー個別アカウントのシートを読み書き * **デスクトップ・Web アプリ** — ユーザーインタラクションが可能 ## 前提条件 * Google Cloud Console プロジェクト * Google Sheets API と Google Drive API を有効化 * Laravel Socialite(推奨) ## セットアップ 1. [Google Cloud Console](https://console.cloud.google.com/) にアクセス 2. プロジェクトを選択または新規作成 3. **APIs & Services** > **Library** に移動 4. 以下の API を有効化: * **Google Sheets API** * **Google Drive API** 1. **APIs & Services** > **Credentials** に移動 2. **Create Credentials** > **OAuth client ID** をクリック 3. OAuth 同意画面を設定(初回のみ): * ユーザータイプを **External** に選択 * 必須項目を入力(アプリ名、サポートメール、開発者連絡先) * スコープを追加:`https://www.googleapis.com/auth/spreadsheets` と `https://www.googleapis.com/auth/drive` 4. アプリケーションタイプを **Web application** に選択 5. 認可リダイレクト URI を追加: * 開発環境: `http://localhost:8000/auth/google/callback` * 本番環境: `https://yourdomain.com/auth/google/callback` 6. **Create** をクリック 7. **Client ID** と **Client Secret** をコピー `.env` ファイルに追加: ```env theme={null} GOOGLE_CLIENT_ID=your-client-id-here GOOGLE_CLIENT_SECRET=your-client-secret-here GOOGLE_REDIRECT=http://localhost:8000/auth/google/callback ``` `config/google.php` を更新: ```php theme={null} 'client_id' => env('GOOGLE_CLIENT_ID', ''), 'client_secret' => env('GOOGLE_CLIENT_SECRET', ''), 'redirect_uri' => env('GOOGLE_REDIRECT', ''), 'scopes' => [ \Google\Service\Sheets::SPREADSHEETS, \Google\Service\Drive::DRIVE, ], 'access_type' => 'offline', // リフレッシュトークンに必須 'prompt' => 'consent select_account', ``` ```bash theme={null} composer require laravel/socialite ``` `config/services.php` に追加: ```php theme={null} 'google' => [ 'client_id' => env('GOOGLE_CLIENT_ID'), 'client_secret' => env('GOOGLE_CLIENT_SECRET'), 'redirect' => env('GOOGLE_REDIRECT'), ], ``` ```php theme={null} // app/Http/Controllers/AuthController.php scopes(config('google.scopes')) ->with([ 'access_type' => config('google.access_type'), 'prompt' => config('google.prompt'), ]) ->redirect(); } public function handleGoogleCallback() { try { $googleUser = Socialite::driver('google')->user(); $user = User::updateOrCreate( ['email' => $googleUser->email], [ 'name' => $googleUser->name, 'email' => $googleUser->email, 'google_access_token' => $googleUser->token, 'google_refresh_token' => $googleUser->refreshToken, 'google_expires_in' => $googleUser->expiresIn, 'google_token_created' => now()->timestamp, ] ); auth()->login($user); return redirect('/dashboard')->with('success', 'Google 認証に成功しました'); } catch (\Exception $e) { return redirect('/login') ->with('error', '認証失敗: ' . $e->getMessage()); } } public function logout(Request $request) { auth()->logout(); $request->session()->invalidate(); $request->session()->regenerateToken(); return redirect('/'); } } ``` ```php theme={null} // routes/web.php Route::get('/auth/google', [AuthController::class, 'redirectToGoogle']) ->name('google.redirect'); Route::get('/auth/google/callback', [AuthController::class, 'handleGoogleCallback']) ->name('google.callback'); Route::post('/logout', [AuthController::class, 'logout']) ->name('logout'); ``` マイグレーション: ```php theme={null} // database/migrations/add_google_tokens_to_users_table.php Schema::table('users', function (Blueprint $table) { $table->text('google_access_token')->nullable(); $table->text('google_refresh_token')->nullable(); $table->integer('google_expires_in')->nullable(); $table->integer('google_token_created')->nullable(); }); ``` User モデル: ```php theme={null} // app/Models/User.php protected $fillable = [ 'name', 'email', 'password', 'google_access_token', 'google_refresh_token', 'google_expires_in', 'google_token_created', ]; protected $hidden = [ 'password', 'remember_token', 'google_access_token', 'google_refresh_token', ]; public function getGoogleTokenArray(): array { return [ 'access_token' => $this->google_access_token, 'refresh_token' => $this->google_refresh_token, 'expires_in' => $this->google_expires_in, 'created' => $this->google_token_created, ]; } public function hasValidGoogleToken(): bool { return !empty($this->google_access_token) && !empty($this->google_refresh_token); } ``` ```php theme={null} use Revolution\Google\Sheets\Facades\Sheets; public function getSheetData(Request $request) { $user = $request->user(); if (!$user->hasValidGoogleToken()) { return redirect()->route('google.redirect'); } try { $token = $user->getGoogleTokenArray(); $values = Sheets::setAccessToken($token) ->spreadsheet('user-spreadsheet-id') ->sheet('Sheet1') ->all(); return view('sheets.data', compact('values')); } catch (\Exception $e) { // トークン期限切れなら再認証に if (str_contains($e->getMessage(), 'invalid_grant') || str_contains($e->getMessage(), 'unauthorized')) { return redirect()->route('google.redirect'); } throw $e; } } ``` ## トークンリフレッシュ パッケージはトークン有効期限切れを自動的に処理します: ```php theme={null} $token = [ 'access_token' => $user->google_access_token, 'refresh_token' => $user->google_refresh_token, 'expires_in' => $user->google_expires_in, 'created' => $user->google_token_created, ]; // 期限切れなら自動的にリフレッシュ Sheets::setAccessToken($token) ->spreadsheet('id') ->sheet('Sheet1') ->all(); // リフレッシュ後の更新トークンを取得 $updatedToken = Sheets::getAccessToken(); if ($updatedToken) { $user->update([ 'google_access_token' => $updatedToken['access_token'], 'google_token_created' => time(), ]); } ``` ## ミドルウェア Google 認証を必須にするミドルウェア: ```php theme={null} // app/Http/Middleware/RequireGoogleAuth.php user(); if (!$user || !$user->hasValidGoogleToken()) { if ($request->expectsJson()) { return response()->json( ['error' => 'Google 認証が必須です'], 401 ); } return redirect()->route('google.redirect'); } return $next($request); } } ``` ## セキュリティ ### 1. トークン保管 * トークンはデータベースに安全に保存 * Laravel の組み込み暗号化を使用 * クライアント側にトークンを公開しない ### 2. スコープ管理 * 必要最小限のスコープのみ要求 * 最小権限の原則を適用 * ユーザーに必要な権限を明確に説明 ### 3. エラーハンドリング * 期限切れトークンを適切に処理 * ユーザーフレンドリーな再認証フローを提供 * エラーをログ記録・監視 ## トラブルシューティング ### よくあるエラー **「redirect\_uri\_mismatch」エラー** * Google Console のリダイレクト URI とアプリケーションが完全に一致しているか確認 * http vs https の違いを確認 * 末尾のスラッシュを確認 **「invalid\_grant」または「unauthorized」エラー** * トークンが期限切れでリフレッシュに失敗 * ユーザーに再認証を求める * リフレッシュトークンが存在するか確認 **「access\_denied」エラー** * ユーザーが権限許可を拒否 * 適切なメッセージで対応 * 再認証を試みるオプションを提供 ### テストルート ```php theme={null} Route::get('/test-oauth', function (Request $request) { $user = $request->user(); if (!$user->hasValidGoogleToken()) { return 'Google トークンがありません。' . '' . '認証'; } try { $token = $user->getGoogleTokenArray(); $sheets = Sheets::setAccessToken($token)->spreadsheetList(); return 'OAuth が機能しています!スプレッドシート数: ' . count($sheets); } catch (\Exception $e) { return 'OAuth エラー: ' . $e->getMessage(); } })->middleware('auth'); ``` # Service Account 認証 - Google Sheets API for Laravel Source: https://kawax.biz/jp/packages/laravel-google-sheets/service-account Google Sheets API の Service Account 認証を設定。サーバー間通信向け。 Service Account 認証はサーバー間で Google Sheets にアクセスする場合に最適です。ユーザー操作は不要で、アプリケーション自身がスプレッドシートを操作します。自動化タスクやバックグラウンドジョブに向いています。 ## 適用場面 * **サーバー間アクセス** — ユーザーの操作なしでアプリが Sheets にアクセス * **自動化システム** — バックグラウンドジョブ、cron タスク、自動レポート * **固定スプレッドシート** — 特定のスプレッドシートを制御 * **本番環境** — ユーザー同意フローなしで確実に動作 ## 前提条件 * Google Cloud Console プロジェクト * Google Sheets API と Google Drive API を有効化 * プロジェクト管理者権限 ## セットアップ 1. [Google Cloud Console](https://console.cloud.google.com/) にアクセス 2. 新規プロジェクトを作成または既存を選択 3. プロジェクト ID をメモ 1. **APIs & Services** > **Library** に移動 2. 以下の API を検索・有効化: * **Google Sheets API** * **Google Drive API** 1. **APIs & Services** > **Credentials** に移動 2. **Create Credentials** > **Service Account** をクリック 3. Service Account の詳細を入力: * **Service account name**: 説明的な名前(例:「Laravel Sheets App」) * **Service account ID**: 自動生成 * **Description**: オプション 4. **Create and Continue** をクリック 5. オプションの両セクションはスキップして **Done** をクリック 1. **Credentials** ページで作成した Service Account をクリック 2. **Keys** タブに移動 3. **Add Key** > **Create new key** をクリック 4. キー形式は **JSON** を選択 5. **Create** をクリック 6. JSON ファイルが自動ダウンロードされます 1. ダウンロードした JSON ファイルを `storage/app/` に移動 2. `google-service-account.json` に名前を変更 3. **重要**: `.gitignore` に追加してバージョン管理から除外 ```bash theme={null} # .gitignore に追加 storage/app/google-service-account.json ``` `.env` に以下を追加: ```env theme={null} GOOGLE_SERVICE_ENABLED=true GOOGLE_SERVICE_ACCOUNT_JSON_LOCATION=storage/app/google-service-account.json ``` ```php theme={null} 'scopes' => [ \Google\Service\Sheets::SPREADSHEETS, \Google\Service\Drive::DRIVE, ], ``` 各 Google Sheets スプレッドシートについて: 1. Google Sheets で開く 2. **共有** ボタンをクリック 3. JSON ファイルの `client_email` フィールドをコピー 4. このメールアドレスに共有(例:`your-service-account@your-project-id.iam.gserviceaccount.com`) 5. 権限を選択: * **閲覧者** — 読み取り専用 * **編集者** — 読み書き * **所有者** — フルアクセス(非推奨) ## 使用方法 設定完了後、Service Account が自動的に使用されます。アクセストークンの設定は不要です。 ```php theme={null} use Revolution\Google\Sheets\Facades\Sheets; // Service Account が自動的に利用される $values = Sheets::spreadsheet('your-spreadsheet-id') ->sheet('Sheet1') ->all(); // スプレッドシート名で参照(Drive API へのアクセスが必要) $values = Sheets::spreadsheetByTitle('My Spreadsheet') ->sheet('Sheet1') ->all(); ``` ## セキュリティベストプラクティス ### 1. Service Account 権限を制限 * 必要なスプレッドシートのみ共有 * **所有者** ではなく **編集者** を使用 * 定期的にアクセス権限を監査 ### 2. キーの安全な保管 * Service Account キーはバージョン管理に含めない * `.gitignore` で確実に除外 * 本番環境では web root 外の安全な場所に保存 * 定期的にキーをローテーション ### 3. 本番環境への展開 追加のセキュリティ対策: * JSON キーを web root 外の安全な場所に保存 * 環境ごとに異なる Service Account を使用(開発・ステージング・本番) * Google Cloud Console でこれらのアカウントの利用状況を監視 * 疑わしいアクティビティをログ記録・アラート ## 環境変数リファレンス | 変数 | 説明 | 例 | | - | - | - | | `GOOGLE_SERVICE_ENABLED` | Service Account 認証を有効化 | `true` | | `GOOGLE_SERVICE_ACCOUNT_JSON_LOCATION` | JSON ファイルのパス | `storage/app/google-service-account.json` | ## 高度な設定 ### JSON 文字列を環境変数に保存 JSON ファイルを個別ファイルとして保存する代わりに、JSON 文字列を環境変数として保存できます。GitHub Actions などの CI/CD 環境に適しています。 **Step 1: `.env` に JSON 文字列を追加** ```env theme={null} GOOGLE_SERVICE_ENABLED=true GOOGLE_SERVICE_ACCOUNT_JSON_LOCATION='{"type": "service_account", "project_id": "your-project-id", ...}' ``` **Step 2: `config/google.php` で JSON 文字列をデコード** ```php theme={null} // config/google.php 'service' => [ 'enable' => env('GOOGLE_SERVICE_ENABLED', false), 'file' => json_decode(env('GOOGLE_SERVICE_ACCOUNT_JSON_LOCATION', ''), true), ], ``` この方法でファイルを個別に保存する必要がなく、CI/CD パイプラインの環境変数で完全に管理できます。 ## トラブルシューティング ### よくあるエラー **「caller does not have permission」エラー** * スプレッドシートが Service Account メールアドレスで共有されているか確認 * 書き込み操作の場合、権限が **編集者** 以上か確認 **「File not found」エラー** * JSON ファイルのパスが正しいか確認 * ファイルが存在し、web サーバーが読み取れるか確認 **「API not enabled」エラー** * Google Cloud Console で Google Sheets API と Google Drive API が有効か確認 * API 有効化後、数分待つ **「Invalid credentials」エラー** * JSON キーファイルが有効で破損していないか確認 * Service Account が削除または無効化されていないか確認 * プロジェクト ID が Google Cloud プロジェクトと一致しているか確認 ### セットアップテスト Service Account セットアップを確認するテストルート: ```php theme={null} // routes/web.php Route::get('/test-sheets', function () { try { $sheets = Sheets::spreadsheetList(); return response()->json([ 'status' => 'success', 'message' => 'Service Account 認証が機能しています', 'spreadsheet_count' => count($sheets) ]); } catch (\Exception $e) { return response()->json([ 'status' => 'error', 'message' => $e->getMessage() ]); } }); ``` このテストルートにアクセスして、Service Account 認証が正しく設定されているか確認します。 # Webhook / Bot - LINE SDK for Laravel Source: https://kawax.biz/jp/packages/laravel-line-sdk/bot WebhookコントローラーとBot FacadeによるLINE Messaging APIのメッセージ送受信。 ## Webhook パッケージはWebhookルーティングとコントローラーを提供しています。 ```mermaid theme={null} flowchart TD A["ユーザーがメッセージを送信"] --> B["LINEがWebhookで送信"] B --> C["line.webhook ルートで受信"] C --> D["ValidateSignature ミドルウェアで検証"] D --> E["WebhookController"] E --> F["WebhookEventDispatcher から
Laravelイベントをディスパッチ"] F --> G["イベントリスナーで返信"] ``` ### Webhook URL デフォルトのWebhook URLは次のとおりです。 ``` https://example.com/line/webhook ``` `.env` でパスを変更できます。 ```dotenv theme={null} LINE_BOT_WEBHOOK_PATH=webhook ``` ### Laravelイベントシステムとの連携 Webhookイベントを受信するとLaravelイベントがディスパッチされます。イベントディスカバリーはデフォルトで有効です。 本番環境では `php artisan event:cache` を実行してください。 ### デフォルト Listener の公開 ```shell theme={null} php artisan vendor:publish --tag=line-listeners ``` `app/Listeners/Line/` に `MessageListener` が生成されます。 ```php theme={null} namespace App\Listeners\Line; use LINE\Clients\MessagingApi\ApiException; use LINE\Webhook\Model\MessageEvent; use LINE\Webhook\Model\StickerMessageContent; use LINE\Webhook\Model\TextMessageContent; use Revolution\Line\Facades\Bot; class MessageListener { protected string $token; public function handle(MessageEvent $event): void { $message = $event->getMessage(); $this->token = $event->getReplyToken(); match ($message::class) { TextMessageContent::class => $this->text($message), StickerMessageContent::class => $this->sticker($message), }; } protected function text(TextMessageContent $message): void { Bot::reply($this->token)->text($message->getText()); } protected function sticker(StickerMessageContent $message): void { Bot::reply($this->token)->sticker( $message->getPackageId(), $message->getStickerId() ); } } ``` ## Bot Facade `Revolution\Line\Facades\Bot` は公式SDK `MessagingApiApi` クラスのすべてのメソッドに委譲します。 ```php theme={null} use Revolution\Line\Facades\Bot; Bot::replyMessage(); Bot::pushMessage(); ``` ### reply メソッド `Bot::reply()` を使うと、リプライトークンを渡してメッセージを返信できます。 `Bot::pushMessage()` によるプッシュ配信は料金プランごとに通数制限があります。一方、ユーザーからのメッセージに対するリプライ(`Bot::reply()`)は通数制限がなく無料で利用できます。 ```php theme={null} use Revolution\Line\Facades\Bot; // テキストを返信する Bot::reply($token)->text('text'); // 送信者名を変えて複数のテキストを返信する Bot::reply($token)->withSender('alt-name')->text('text1', 'text2'); // スタンプを返信する Bot::reply($token)->sticker(package: 1, sticker: 1); ``` ## カスタマイズ ### Bot マクロ `Bot` は `Macroable` を実装しているため、任意のメソッドを追加できます。 `AppServiceProvider@boot` で登録します。 ```php theme={null} use Revolution\Line\Facades\Bot; public function boot(): void { Bot::macro('foo', function () { return $this->bot()->...; }); } ``` ```php theme={null} $foo = Bot::foo(); ``` ### MessagingApiApi インスタンスの差し替え `Bot::bot()` は `MessagingApiApi` インスタンスを返します。`Bot::botUsing()` でインスタンスを差し替えられます。 ```php theme={null} $bot = new MyBot(); Bot::botUsing($bot); ``` Callableも受け付けます。 ```php theme={null} Bot::botUsing(function () { return new MyBot(); }); ``` ### WebhookHandler の差し替え Laravelイベントシステムを使わずに独自のWebhook処理を書きたい場合は、`WebhookHandler` interfaceを実装して差し替えます。 `app/Actions/LineWebhook.php` を作成します。 ```php theme={null} each(function ($event) { if ($event instanceof MessageEvent) { // } }); return response('OK'); } } ``` `AppServiceProvider@register` で登録します。 ```php theme={null} use App\Actions\LineWebhook; use Revolution\Line\Contracts\WebhookHandler; public function register(): void { $this->app->scoped(WebhookHandler::class, LineWebhook::class); } ``` ### デフォルトルートのミドルウェア デフォルトで `throttle` ミドルウェアが有効です。`.env` で変更できます。 ```dotenv theme={null} # 無効にする LINE_BOT_WEBHOOK_MIDDLEWARE=null # throttle 設定を変更する LINE_BOT_WEBHOOK_MIDDLEWARE=throttle:120,1 ``` ### Http::line() パッケージは `Http` クラスを拡張しているため、`Bot` Facade を使わず直接APIリクエストを送ることもできます。 ```php theme={null} use Illuminate\Support\Facades\Http; $response = Http::line()->post('/v2/bot/channel/webhook/test', [ 'endpoint' => '', ]); ``` 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-line-sdk) を参照してください。 # LINE SDK for Laravel Source: https://kawax.biz/jp/packages/laravel-line-sdk/index LINE公式SDKをLaravel向けに使いやすくしたパッケージ。Webhook・Bot・通知・Socialiteの4機能を提供。 ## 概要 [revolution/laravel-line-sdk](https://github.com/invokable/laravel-line-sdk) は、LINE公式SDKをLaravelから使いやすくするパッケージです。 Webhook受信、Bot Facade、Laravel Notifications統合、LINE Login(Socialite)の4機能を提供します。 このパッケージは PHP 8.3 以上、Laravel 12.0 以上が必要です。 ## インストール ```shell theme={null} composer require revolution/laravel-line-sdk ``` ## 設定 ### LINE Developers コンソール [LINE Developers](https://developers.line.biz/) でチャンネルを作成します。 Messaging API チャンネルは LINE Developers Console から直接作成できなくなりました。先に [LINE Official Account Manager](https://manager.line.biz/) で公式アカウントを作成し、Messaging API の利用を有効化してください。 2つのチャンネルを作成します。 | チャンネル | 取得するもの | | - | - | | **Messaging API** | Channel access token (long-lived)、Channel secret | | **LINE Login** | Channel ID、Channel secret | ### .env ```dotenv theme={null} LINE_BOT_CHANNEL_TOKEN= LINE_BOT_CHANNEL_SECRET= LINE_LOGIN_CLIENT_ID= LINE_LOGIN_CLIENT_SECRET= LINE_LOGIN_REDIRECT= ``` ### 設定ファイルの公開(任意) ```shell theme={null} php artisan vendor:publish --tag=line-config ``` ## クイックスタート 最短でBotを動作させる手順です。 ```shell theme={null} laravel new line-bot cd ./line-bot composer require revolution/laravel-line-sdk ``` ```dotenv theme={null} LINE_BOT_CHANNEL_TOKEN= LINE_BOT_CHANNEL_SECRET= ``` ```shell theme={null} php artisan vendor:publish --tag=line-listeners ``` `app/Listeners/Line/` に `MessageListener` が生成されます。 LINEからWebhookを受信するには、公開されたURLが必要です。ローカルサーバーでは受信できません。 LINE Developers コンソールで Webhook URL を設定します。 ``` https://example.com/line/webhook ``` Messaging API チャンネルの QR コードを使って友だち追加します。メッセージを送ると、`MessageListener` がそのままオウム返しします。 ## ドキュメント WebhookコントローラーとBot Facadeによるメッセージ送受信。 Laravel Notification SystemとのLINE Messaging API統合。 LINE LoginによるOAuth2認証をSocialiteで実装する。 ## 参考リンク * GitHub: [invokable/laravel-line-sdk](https://github.com/invokable/laravel-line-sdk) * デモ: [invokable/laravel-line-project](https://github.com/invokable/laravel-line-project) * DeepWiki: [invokable/laravel-line-sdk](https://deepwiki.com/invokable/laravel-line-sdk) # 通知チャンネル - LINE SDK for Laravel Source: https://kawax.biz/jp/packages/laravel-line-sdk/notification Laravel Notification SystemとLINE Messaging APIを統合して通知を送信する。 ## 概要 `LineChannel` を使うと、Laravel Notifications から LINE にメッセージを送信できます。 LINE Notifyのサービス終了に伴いLINE Notify対応は終了しています。現在はMessaging APIによる通知なので料金プランごとの通数制限があります。[Messaging APIの料金](https://developers.line.biz/ja/docs/messaging-api/pricing/) を参照してください。 ## Notification クラス `via()` で `LineChannel::class` を返し、`toLine()` でメッセージを定義します。 ```php theme={null} use Illuminate\Notifications\Notification; use Revolution\Line\Notifications\LineChannel; use Revolution\Line\Notifications\LineMessage; class TestNotification extends Notification { public function via(object $notifiable): array { return [ LineChannel::class, ]; } public function toLine(object $notifiable): LineMessage { return LineMessage::create(text: 'test'); } } ``` ## 送信パターン ### On-Demand 通知 ユーザーモデルに紐づけずに通知を送る場合は `Notification::route()` を使います。送信先は `userId` または `groupId` です。 ```php theme={null} use Illuminate\Support\Facades\Notification; Notification::route('line', 'to') ->notify(new TestNotification()); ``` ### User モデルからの通知 User ごとに送信先を管理する場合は `routeNotificationForLine()` を定義します。 ```php theme={null} use Illuminate\Notifications\Notifiable; class User extends Model { use Notifiable; public function routeNotificationForLine($notification): string { return $this->line_id; } } ``` ```php theme={null} $user->notify(new TestNotification()); ``` `userId` や `groupId` は `FollowEvent` や `JoinEvent` などのWebhookイベントから取得できます。 ## メッセージタイプ ### テキスト 最大5件まで送信できます。 ```php theme={null} use Revolution\Line\Notifications\LineMessage; public function toLine(object $notifiable): LineMessage { return LineMessage::create() ->text('text 1') ->text('text 2'); } ``` ### 送信者名とアイコンのカスタマイズ `withSender()` はメッセージ追加メソッドより前に呼ぶ必要があります。 ```php theme={null} use Revolution\Line\Notifications\LineMessage; public function toLine(object $notifiable): LineMessage { return LineMessage::create() ->withSender(name: 'alt-name', icon: 'https://...png') ->text('text 1') ->text('text 2'); } ``` `create()` のパラメータで名前とアイコンを指定することもできます。 ```php theme={null} use Revolution\Line\Notifications\LineMessage; public function toLine(object $notifiable): LineMessage { return LineMessage::create(text: 'test', name: 'alt-name', icon: 'https://...png'); } ``` ### スタンプ 使用できるスタンプは [Sticker list](https://developers.line.biz/ja/docs/messaging-api/sticker-list/) を参照してください。 ```php theme={null} use Revolution\Line\Notifications\LineMessage; public function toLine(object $notifiable): LineMessage { return LineMessage::create() ->text('test') ->sticker(package: 446, sticker: 1988); } ``` ### 画像 公開URLを指定します。 ```php theme={null} use Revolution\Line\Notifications\LineMessage; public function toLine(object $notifiable): LineMessage { return LineMessage::create() ->image(original: 'https://.../test.png', preview: 'https://.../preview.png'); } ``` ### 動画 公開URLを指定します。 ```php theme={null} use Revolution\Line\Notifications\LineMessage; public function toLine(object $notifiable): LineMessage { return LineMessage::create() ->video(original: 'https://.../test.mp4', preview: 'https://.../preview.png'); } ``` ### その他のメッセージタイプ `message()` を使うと任意のメッセージタイプを追加できます。`setType()` でタイプを指定してください。 ```php theme={null} use LINE\Clients\MessagingApi\Model\LocationMessage; use LINE\Constants\MessageType; use Revolution\Line\Notifications\LineMessage; public function toLine(object $notifiable): LineMessage { $location = (new LocationMessage()) ->setType(MessageType::LOCATION) ->setTitle('title') ->setAddress('address') ->setLatitude(0.0) ->setLongitude(0.0); return LineMessage::create() ->message($location); } ``` 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-line-sdk) を参照してください。 # Socialite(LINE Login)- LINE SDK for Laravel Source: https://kawax.biz/jp/packages/laravel-line-sdk/socialite LINE LoginによるOAuth2認証をLaravel Socialiteで実装する。 ## 概要 このパッケージは `Socialite::extend()` を使った正規の方法でLINE Loginドライバーを提供します。組み込みプロバイダーとまったく同じAPIで使えます。 利用可能なドライバー: | ドライバー | 説明 | | - | - | | `line-login` | [LINE Login](https://developers.line.biz/ja/docs/line-login/) によるOAuth2認証 | Laravel Socialite 自体の使い方については [Socialite ガイド](/jp/socialite) を参照してください。 ## 設定 ### .env ```dotenv theme={null} LINE_LOGIN_CLIENT_ID= LINE_LOGIN_CLIENT_SECRET= LINE_LOGIN_REDIRECT= ``` ## 基本的な使い方 `routes/web.php` にリダイレクト用とコールバック用の2つのルートを追加します。 ```php theme={null} use App\Http\Controllers\SocialiteController; Route::get('login', [SocialiteController::class, 'login']); Route::get('callback', [SocialiteController::class, 'callback']); ``` ```php theme={null} redirect(); } public function callback(Request $request) { if ($request->missing('code')) { // デバッグ用。本番環境ではリダイレクトやエラー処理に変更してください return redirect('/login')->withErrors('認証がキャンセルされました。'); } /** @var \Laravel\Socialite\Two\User */ $user = Socialite::driver('line-login')->user(); $loginUser = User::updateOrCreate([ 'line_id' => $user->id, ], [ 'name' => $user->nickname, 'avatar' => $user->avatar, 'access_token' => $user->token, 'refresh_token' => $user->refreshToken, ]); auth()->login($loginUser, true); return redirect()->route('home'); } } ``` ## オプションパラメータ `with()` で追加パラメータを渡せます。 ```php theme={null} public function login() { return Socialite::driver('line-login')->with([ 'prompt' => 'consent', 'bot_prompt' => 'normal', ])->redirect(); } ``` ## スコープの設定 取得する権限をスコープで指定します。 ```php theme={null} public function login() { return Socialite::driver('line-login') ->setScopes(['profile', 'openid']) ->redirect(); } ``` 主なスコープの一覧については [LINE Login スコープ](https://developers.line.biz/ja/docs/line-login/integrate-line-login/#scopes) を参照してください。 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-line-sdk) を参照してください。 # Laravel Nostr Source: https://kawax.biz/jp/packages/laravel-nostr Nostr プロトコル用 Laravel パッケージ。Key 管理、イベント操作、pool 対応、NIP-05 / NIP-17、Laravel Notifications 統合。 ## 概要 [revolution/laravel-nostr](https://github.com/invokable/laravel-nostr) は、Nostr プロトコルを Laravel から利用するためのパッケージです。Key の生成・変換、イベントの取得・発行、pool(複数リレー)対応、NIP-05 プロフィール、NIP-17 Private Direct Messages、そして Laravel Notifications との統合を提供します。 Nostr の仕様はまだ進化中のため、このパッケージも継続的に開発されています。通知機能はすでに実用的に使えます。 ## ドライバー このパッケージには 2 つのドライバーがあります。 | ドライバー | 説明 | | - | - | | `native` | PHP のみで動作する実装。[nostr-php](https://github.com/nostrver-se/nostr-php) を使用。現在はこちらで十分。 | | `node` | 外部 [WebAPI](https://github.com/kawax/nostr-vercel-api)(Node.js)に依存する実装。 | `native` ドライバーのユニークな点は `WebSocketHttpMixin` の実装です。Laravel の HTTP クライアントで WebSocket に接続し、データを送受信したらすぐに切断します。WebSocket サーバーを起動し続ける必要がなく、Laravel ユーザーなら誰でも使える設計になっています。 `native` ドライバーは NIP-04 には対応していません。 ### デフォルトドライバーの設定 `config/nostr.php` または `.env` で設定します。 ```php theme={null} // config/nostr.php 'driver' => env('NOSTR_DRIVER', 'node'), ``` ```dotenv theme={null} NOSTR_DRIVER=native ``` ドライバーを指定しない場合はデフォルトのドライバーが使われます。 ```php theme={null} use Revolution\Nostr\Facades\Nostr; Nostr::event()->list(); ``` ドライバーを明示的に指定することもできます。 ```php theme={null} use Revolution\Nostr\Facades\Nostr; Nostr::driver('node')->event()->list(); Nostr::node()->event()->list(); Nostr::driver('native')->event()->list(); Nostr::native()->event()->list(); ``` ## インストール ```bash theme={null} composer require revolution/laravel-nostr ``` ```bash theme={null} php artisan vendor:publish --tag=nostr-config ``` ## Key 管理 ### Key を生成する ```php theme={null} use Revolution\Nostr\Facades\Nostr; use Illuminate\Http\Client\Response; /** @var Response $response */ $response = Nostr::key()->generate(); $keys = $response->json(); // [ // 'sk' => 'sk...', // 'nsec' => 'nsec...', // 'pk' => 'pk...', // 'npub' => 'npub...', // ] ``` ### Key を変換する nsec から変換します。 ```php theme={null} use Revolution\Nostr\Facades\Nostr; $response = Nostr::key()->fromNsec(nsec: 'nsec'); $keys = $response->json(); // ['sk' => '...', 'nsec' => '...', 'pk' => '...', 'npub' => '...'] ``` Secret Key から変換します。 ```php theme={null} $response = Nostr::key()->fromSecretKey(sk: 'sk'); ``` npub から変換します(公開鍵のみ)。 ```php theme={null} $response = Nostr::key()->fromNpub(npub: 'npub'); $keys = $response->json(); // ['pk' => '...', 'npub' => '...'] ``` Public Key から変換します。 ```php theme={null} $response = Nostr::key()->fromPublicKey(pk: 'pk'); ``` ## イベントの取得 ### 複数イベントを取得する ```php theme={null} use Illuminate\Http\Client\Response; use Revolution\Nostr\Facades\Nostr; use Revolution\Nostr\Filter; use Revolution\Nostr\Kind; $filter = Filter::make( authors: ['my pk'], kinds: [Kind::Text], limit: 10, ); /** @var Response $response */ $response = Nostr::event()->list(filter: $filter); $events = $response->json('events'); // [ // ['id' => '...1', 'kind' => 1, 'content' => '...'], // ['id' => '...2', 'kind' => 1, 'content' => '...'], // ] ``` ### 1 件のイベントを取得する ```php theme={null} use Revolution\Nostr\Facades\Nostr; use Revolution\Nostr\Filter; use Revolution\Nostr\Kind; $filter = Filter::make( authors: ['my pk'], kinds: [Kind::Metadata], ); $response = Nostr::event()->get(filter: $filter); $event = $response->json('event'); // ['id' => '...', 'kind' => 0, 'content' => '{name: ""}'] ``` ## イベントの発行 ### 単一リレーに発行する ```php theme={null} use Revolution\Nostr\Facades\Nostr; use Revolution\Nostr\Event; use Revolution\Nostr\Kind; $event = Event::make( kind: Kind::Text, content: 'hello', created_at: now()->timestamp, tags: [], ); $sk = 'my sk'; $response = Nostr::event()->publish(event: $event, sk: $sk); if ($response->successful()) { $event = $response->json('event'); } ``` ### 複数リレーに発行する(pool) ```php theme={null} use Revolution\Nostr\Facades\Nostr; use Revolution\Nostr\Event; use Revolution\Nostr\Kind; $event = Event::make( kind: Kind::Text, content: 'test', created_at: now()->timestamp, tags: [], ); $responses = Nostr::pool()->publish(event: $event, sk: 'my sk'); // $responses は array // ['wss://relay1' => $response, 'wss://relay2' => $response] foreach ($responses as $relay => $response) { if ($response->failed()) { dump($relay . ' : ' . $response->body()); } } ``` ## リレーサーバーの設定 ### 使用されるリレーサーバー `Nostr::event()` のみを使う場合は `config/nostr.php` の最初のリレーが使われます。`Nostr::pool()` を使う場合は設定内のすべてのリレーが対象になります。 ### 実行時にリレーを変更する ```php theme={null} use Revolution\Nostr\Facades\Nostr; $response = Nostr::event()->withRelay('wss://')->...; ``` ```php theme={null} use Revolution\Nostr\Facades\Nostr; $response = Nostr::pool()->withRelays(['wss://', 'wss://'])->...; ``` ## NIP-05 プロフィール ```php theme={null} use Revolution\Nostr\Facades\Nostr; $profile = Nostr::nip05()->profile('user@localhost'); // [ // 'user' => 'user@localhost', // 'pubkey' => 'pk', // 'relays' => [], // ] ``` ## NIP-17 Private Direct Messages NIP-17 は `native` ドライバーのみ対応しています。 ### プライベートメッセージを送信する ```php theme={null} use Revolution\Nostr\Facades\Nostr; $response = Nostr::driver('native') ->nip17() ->sendDirectMessage( sk: 'sender-secret-key', pk: 'receiver-public-key', message: 'Hello, this is a private message!' ); ``` ### プライベートメッセージを復号する ```php theme={null} use Revolution\Nostr\Facades\Nostr; $response = Nostr::driver('native') ->nip17() ->decryptDirectMessage( giftWrap: $receivedGiftWrap, sk: 'receiver-secret-key' ); $decryptedMessage = $response->json(); ``` ## Laravel Notifications `NostrChannel` を使うと、Laravel Notifications から Nostr にメッセージを送信できます。 ### Notification クラス ```php theme={null} use Illuminate\Notifications\Notification; use Revolution\Nostr\Notifications\NostrChannel; use Revolution\Nostr\Notifications\NostrMessage; use Revolution\Nostr\Tags\HashTag; class TestNotification extends Notification { public function via(object $notifiable): array { return [ 'mail', NostrChannel::class, ]; } public function toNostr(object $notifiable): NostrMessage { return new NostrMessage( // content 内の #laravel は表示用、tags の HashTag はプロトコルレベルの分類用 content: 'hello #laravel', tags: [ HashTag::make(t: 'laravel'), ], ); } } ``` ### オンデマンド通知 ```php theme={null} use Illuminate\Support\Facades\Notification; use Revolution\Nostr\Notifications\NostrRoute; Notification::route('nostr', NostrRoute::to(sk: 'sk')) ->notify(new TestNotification()); ``` ### User モデルへの統合 ```php theme={null} use Illuminate\Notifications\Notifiable; use Revolution\Nostr\Notifications\NostrRoute; class User { use Notifiable; public function routeNotificationForNostr($notification): NostrRoute { return NostrRoute::to(sk: $this->sk, relays: ['wss://']); } } ``` ```php theme={null} $user->notify(new TestNotification()); ``` ### 通知で使うリレーサーバー デフォルトでは `config/nostr.php` のすべてのリレーが使われます。`NostrRoute` でリレーを指定すると実行時に変更できます。 ```php theme={null} use Revolution\Nostr\Notifications\NostrRoute; return NostrRoute::to(sk: 'sk', relays: ['wss://', 'wss://']); ``` 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-nostr) を参照してください。 # Laravel Notification for Discord(Webhook) Source: https://kawax.biz/jp/packages/laravel-notification-discord-webhook Discord Webhook を使って Laravel から簡単に通知を送れるパッケージ。Bot なし、WebSocket なし、Webhook URL を設定するだけで即使える。 ## 概要 [revolution/laravel-notification-discord-webhook](https://github.com/invokable/laravel-notification-discord-webhook) は、Discord Webhook を使って Laravel Notifications から Discord に通知を送るパッケージです。 Discord の正式な Bot API は WebSocket 接続を維持する必要があり、セットアップが複雑です。Webhook なら URL を取得するだけで通知を送れます。チームの Discord チャンネルにアプリのイベントを通知したいだけであれば、Webhook が最適な選択です。 このパッケージは PHP 8.3 以上、Laravel 12.0 以上が必要です。 ## インストール ```shell theme={null} composer require revolution/laravel-notification-discord-webhook ``` ## 設定 ### Discord の Webhook URL を取得する Discord サーバーの **設定 → 連携サービス → ウェブフック** から Webhook URL を作成できます。詳細は [Discord の公式ガイド](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) を参照してください。 ### config/services.php ```php theme={null} 'discord' => [ 'webhook' => env('DISCORD_WEBHOOK'), ], ``` ### .env ```dotenv theme={null} DISCORD_WEBHOOK=https://discord.com/api/webhooks/... ``` ## 基本的な使い方 ### Notification クラスを作成する `toDiscordWebhook()` メソッドを実装し、`via()` で `DiscordChannel::class` を返します。 ```php theme={null} content); } } ``` ## 送信パターン ### On-Demand 通知 特定のユーザーモデルに紐づけずに通知を送る場合は、`Notification::route()` を使います。 ```php theme={null} use Illuminate\Support\Facades\Notification; Notification::route('discord-webhook', config('services.discord.webhook')) ->notify(new DiscordNotification('デプロイが完了しました。')); ``` ### User モデルからの通知 User ごとに異なる Webhook URL を使う場合は、`routeNotificationForDiscordWebhook()` を定義します。 ```php theme={null} use Illuminate\Notifications\Notifiable; class User extends Authenticatable { use Notifiable; public function routeNotificationForDiscordWebhook($notification): string { return $this->discord_webhook; } } ``` ```php theme={null} $user->notify(new DiscordNotification('テスト通知')); ``` ## メッセージタイプ ### テキストメッセージ 最もシンプルな送信方法です。 ```php theme={null} public function toDiscordWebhook(object $notifiable): DiscordMessage { return DiscordMessage::create(content: $this->content); } ``` ### Embed メッセージ `DiscordEmbed` を使うとタイトルや URL を含むリッチな表示ができます。 ```php theme={null} use Revolution\Laravel\Notification\DiscordWebhook\DiscordMessage; use Revolution\Laravel\Notification\DiscordWebhook\DiscordEmbed; public function toDiscordWebhook(object $notifiable): DiscordMessage { return DiscordMessage::create() ->embed( DiscordEmbed::make( title: 'INFO', description: $this->content, url: route('home'), ) ); } ``` ### ファイル添付 `DiscordAttachment` を使うとファイルを添付して送信できます。`content`(ファイルの中身)と `filename` が必須です。 ```php theme={null} use Revolution\Laravel\Notification\DiscordWebhook\DiscordMessage; use Revolution\Laravel\Notification\DiscordWebhook\DiscordAttachment; use Illuminate\Support\Facades\Storage; public function toDiscordWebhook(object $notifiable): DiscordMessage { return DiscordMessage::create() ->file( DiscordAttachment::make( content: Storage::get('test.png'), filename: 'test.png', description: 'test', filetype: 'image/png' ) ); } ``` ### Embed 内でのファイル参照 Embed の `image` や `thumbnail` に添付ファイルを `attachment://ファイル名` で参照できます。 ```php theme={null} use Revolution\Laravel\Notification\DiscordWebhook\DiscordMessage; use Revolution\Laravel\Notification\DiscordWebhook\DiscordAttachment; use Revolution\Laravel\Notification\DiscordWebhook\DiscordEmbed; use Illuminate\Support\Facades\Storage; public function toDiscordWebhook(object $notifiable): DiscordMessage { return DiscordMessage::create() ->embed( DiscordEmbed::make( title: 'test', description: $this->content, image: 'attachment://test.jpg', thumbnail: 'attachment://test2.jpg', ) ) ->file(DiscordAttachment::make( content: Storage::get('test.jpg'), filename: 'test.jpg', description: 'test', filetype: 'image/jpg' )) ->file(new DiscordAttachment( content: Storage::get('test2.jpg'), filename: 'test2.jpg', description: 'test2', filetype: 'image/jpg' )); } ``` ### 任意のペイロードを送信する `->with()` を使うと、Discord Webhook の任意のパラメータを直接指定できます。 ```php theme={null} public function toDiscordWebhook(object $notifiable): DiscordMessage { return DiscordMessage::create() ->with([ 'content' => $this->content, 'embeds' => [[]], ]); } ``` `->with()` で指定できるパラメータの詳細は [Discord の公式 API ドキュメント](https://discord.com/developers/docs/resources/webhook#execute-webhook) を参照してください。 ## まとめ Discord Bot は強力ですが、単に通知を送るだけであれば Webhook がずっとシンプルです。このパッケージを使えば、Laravel の通知システムとシームレスに統合して、チームの Discord チャンネルにアプリのイベントをすばやく通知できます。 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-notification-discord-webhook) を参照してください。 # Laravel AI SDK連携 - VOICEVOX for Laravel Source: https://kawax.biz/jp/packages/laravel-voicevox/ai-sdk VOICEVOX for Laravel を Laravel AI SDK の Audio ファサードから使うための設定と使い方を説明します。 ## Laravel AI SDK 連携 VOICEVOX for Laravel は [Laravel AI SDK](https://github.com/laravel/ai) と統合されており、`Audio` ファサードから音声生成できます。 利用できるドライバーは次の2つです。 * `voicevox-client`(クライアントモード) * `voicevox`(ネイティブモード) ## クライアントドライバー(`voicevox-client`) 公式 VOICEVOX エンジンに HTTP リクエストを送るモードです。FFI は不要です。 ### 事前準備 ```shell theme={null} docker pull voicevox/voicevox_engine:cpu-latest docker run --rm -p '127.0.0.1:50021:50021' voicevox/voicevox_engine:cpu-latest ``` ### 設定 `config/ai.php` に `voicevox-client` プロバイダーを追加します。 ```php theme={null} 'providers' => [ 'voicevox-client' => [ 'driver' => 'voicevox-client', 'key' => env('VOICEVOX_URL', 'http://127.0.0.1:50021'), ], ], ``` ### 使い方 ```php theme={null} use Laravel\Ai\Audio; $response = Audio::of('Laravelが好きなのだ') ->voice('ずんだもん') ->generate('voicevox-client'); Storage::put('talk.wav', $response->content()); ``` ## ネイティブドライバー(`voicevox`) VOICEVOX CORE を直接呼び出すモードです。FFI が必要です。 `config/voicevox.php` の `core.vvms` で読み込んだモデルのみ利用できます。 ### 設定 ```php theme={null} 'providers' => [ 'voicevox' => [ 'driver' => 'voicevox', ], ], ``` ### 使い方 ```php theme={null} use Laravel\Ai\Audio; $response = Audio::of('ネイティブで話すのだ') ->voice('ずんだもん') ->generate('voicevox'); Storage::put('talk.wav', $response->content()); ``` ## `voice()` に指定できる値 `voice()` には次を指定できます。 * VOICEVOX のスタイル ID(数値文字列) * キャラクター名エイリアス(例: `ずんだもん`、`四国めたん/ノーマル`) ### エイリアス一覧 | エイリアス | スタイルID | キャラクター | | - | - | - | | `ずんだもん` | 1 | ずんだもん(あまあま) | | `ずんだもん/あまあま` | 1 | ずんだもん(あまあま) | | `ずんだもん/ノーマル` | 3 | ずんだもん(ノーマル) | | `ずんだもん/セクシー` | 5 | ずんだもん(セクシー) | | `ずんだもん/ツンツン` | 7 | ずんだもん(ツンツン) | | `ずんだもん/ささやき` | 22 | ずんだもん(ささやき) | | `ずんだもん/ヒソヒソ` | 38 | ずんだもん(ヒソヒソ) | | `四国めたん/あまあま` | 0 | 四国めたん(あまあま) | | `四国めたん` | 2 | 四国めたん(ノーマル) | | `四国めたん/ノーマル` | 2 | 四国めたん(ノーマル) | | `四国めたん/セクシー` | 4 | 四国めたん(セクシー) | | `四国めたん/ツンツン` | 6 | 四国めたん(ツンツン) | | `四国めたん/ヒソヒソ` | 37 | 四国めたん(ヒソヒソ) | | `春日部つむぎ` | 8 | 春日部つむぎ(ノーマル) | | `波音リツ` | 9 | 波音リツ(ノーマル) | | `雨晴はう` | 10 | 雨晴はう(ノーマル) | | `玄野武宏` | 11 | 玄野武宏(ノーマル) | | `白上虎太郎` | 12 | 白上虎太郎(ノーマル) | | `青山龍星` | 13 | 青山龍星(ノーマル) | | `冥鳴ひまり` | 14 | 冥鳴ひまり(ノーマル) | | `九州そら` | 16 | 九州そら(ノーマル) | | `default-female` | 10 | 雨晴はう(ノーマル) | | `default-male` | 12 | 白上虎太郎(ノーマル) | `voice()` を省略した場合は `default-female`(ID: 10)が使われます。 ```php theme={null} use Laravel\Ai\Audio; Audio::of('テスト')->voice('3')->generate('voicevox-client'); ``` 利用可能なスピーカー一覧は `Voicevox::speakers()` でも確認できます。 ```php theme={null} use Revolution\Voicevox\Voicevox; $speakers = Voicevox::speakers(); ``` ## エージェント(KanalizerAgent) ネイティブモードでは `enable_katakana_english` がないため、英語を事前にカタカナ化する `KanalizerAgent` を使えます。 ```php theme={null} use Revolution\Voicevox\Ai\Agents\KanalizerAgent; use function Revolution\Voicevox\talk; $kana = KanalizerAgent::make()->prompt('KanalizerAgentで英語をカタカナに変換するのだ'); $response = talk($kana['kana'] ?? $kana->text, id: 1)->generate(id: 1); $response->storeAs('native', 'kanalizer.wav'); ``` AI によるカタカナ変換は常に正確とは限りません。実運用では生成テキストを人間が確認してから合成してください。 ## 次に読むページ クライアントモードの通常 API の流れを確認します。 FFI で直接合成するモードの使い方を確認します。 # エンジンAPI連携アプリ開発ガイド - VOICEVOX for Laravel Source: https://kawax.biz/jp/packages/laravel-voicevox/app-guide VOICEVOXエンジンAPIを呼び出すクライアントアプリを開発するために、URL登録、ソング合成パイプライン、キャッシュ、マルチエンジン設計までを実装観点で整理します。 # VOICEVOXエンジンAPI連携アプリ開発ガイド VOICEVOX Song VOICEVOXエンジンAPIを呼び出すクライアントアプリを作るときの実装メモ。\ macOS用ソングアプリの実装記録をもとに、公式VOICEVOXエディターの内部構成をそのまま移植するのではなく、外部アプリとして安定してエンジンAPIを使うための項目を整理する。 Xcode/Swift以外の環境でも使えるように汎用方針を先に書き、各項目の末尾にXcode向けの補足を置く。 このページは [原文ガイド](https://github.com/invokable/laravel-voicevox/blob/main/docs/develop/voicevox-engine-api-app-guide.md) をもとに、内容を省略せずにドキュメント化しています。 ## 1. 最初にエンジンをURLで登録する VOICEVOX Song 公式VOICEVOXエディターは、同梱または指定されたエンジン実行ファイルをアプリ側から起動し、エンジン設定や `engine_manifest.json` を前提に管理する。一方、外部クライアントアプリでは、まず「起動済みのHTTP APIサーバーのURL」を登録する方式にした方が扱いやすい。 代表的なURLは次の通り。 | エンジン | 例 | | - | - | | 公式VOICEVOXエンジン | `http://127.0.0.1:50021` | | Laravel版エンジン | `http://127.0.0.1:50513` | 公式エンジンはポートの自動調整機能があり、50021が使われていたら50022以降の空いてるポートで起動する。\ Laravel版も`--port=50513`を指定せずにデフォルトの8000で起動すると8001以降で起動するが、ポートを指定していると調整機能は無効となっている。 登録時はURL文字列だけを保存するのではなく、接続確認として最低限次のAPIを呼び、取得したメタ情報と一緒に保存する。 | API | 用途 | | - | - | | `GET /engine_manifest` | `uuid`, エンジン名, `frame_rate`, 既定サンプリングレートなどを取得する | | `GET /version` | 表示・互換性確認用のバージョンを取得する | | `GET /singers` | 歌唱スタイル一覧を取得する | | `GET /singer_info?speaker_uuid=...&resource_format=...` | アイコンや追加情報を取得する | 重要なのは、プロジェクトやトラックにはURLではなく `engineId` と `styleId` を保存すること。URLはユーザー環境ごとに変わるが、`engineId` は `/engine_manifest` の `uuid` と対応し、`.vvproj` の `singer.engineId` にも合わせやすい。 **Xcode向け補足:** ソングアプリでは `RegisteredEngine` に `baseURL`, `engineId`, `name`, `frameRate`, `defaultSamplingRate`, `version`, `singers` を保持し、`UserDefaults` に保存している。URLは末尾スラッシュ、クエリ、フラグメントを正規化してから使うと、`http://127.0.0.1:50021/` と `http://127.0.0.1:50021` を同一扱いにできる。macOSアプリでHTTPのローカル接続を使う場合は、サンドボックスやApp Transport Securityの設定も確認する。 ## 2. 公式アプリと同じ「エンジン起動」まで抱え込まない 外部アプリで最初から公式エディターと同じエンジン起動管理を実装しようとすると、OS別のプロセス起動、同梱バイナリ、ポート競合、アップデート、ログ管理まで扱う必要がある。特に公式エンジン以外の実装を使う場合、エンジン実行ファイルの場所を指定して起動する方式は噛み合いにくい。 まずは次の運用に固定するのが安全。 1. ユーザーがエンジンを先に起動する。 2. アプリでURLを入力する。 3. アプリがメタ情報を取得して登録する。 4. レンダリング時は登録済み `engineId` からURLへ解決する。 この方式なら、公式エンジン、Laravel版エンジン、Docker上のエンジン、別ホストのエンジンを同じ抽象で扱える。接続できない場合は「エンジンを起動してから再接続してください」のように、ユーザーが次に取る操作を明示する。 **Xcode向け補足:** SwiftUIでは「エンジン接続」画面を独立させ、`TextField` にURL、`登録 / 再接続` ボタン、公式/Laravel版のプリセットボタン、登録済みエンジン一覧を置くと運用しやすい。非同期接続中は `ProgressView` を表示し、接続エラーは `LocalizedError` の文言をそのまま出すより、ユーザー向けの説明を前置きするとよい。 ## 3. 歌手・スタイルはエンジン登録後に選ばせる VOICEVOXのAPIでは、最終的に音声生成に使う声は `speaker` クエリパラメータに渡すstyle IDで決まる。ソングではトラックごとに歌手スタイルを持つため、UI上では登録済みエンジンから `singers` を展開し、歌手名・スタイル名・style IDを選択肢にする。 保存する値は次の2つで十分。 ```jsonc theme={null} { "singer": { "engineId": "engine-uuid", "styleId": 6000 } } ``` この構造にしておくと、`.vvproj` 互換のソングデータと合わせやすく、マルチエンジンにも対応しやすい。プロジェクトを開いたときに該当 `engineId` が未登録なら、「未登録エンジン」として表示し、URL登録を促す。 **Xcode向け補足:** Swiftでは `Singer(engineId: String, styleId: Int)` をトラックに持たせ、表示用には `RegisteredEngine.singers` から `SingerStyleOption` のようなViewModel用配列へ平坦化するとよい。歌手割り当てを変えたら、レンダリング済み音声は無効化する。 ## 4. `.vvproj`互換を考えるならtick基準のソングモデルにする VOICEVOXエディターのソングデータは、音符やテンポをtick基準で持つ。エンジンAPI自体は最終的にフレーム長を必要とするが、アプリ内の編集モデルはtick基準にしておく方が、ピアノロール、テンポ変更、拍子、Undo/Redo、`.vvproj` 読み書きに向いている。 最低限の構造は次の通り。 | 要素 | 主なフィールド | | - | - | | Song | `tpqn`, `tempos`, `timeSignatures`, `tracks`, `trackOrder` | | Track | `name`, `singer`, `notes`, `gain`, `pan`, `solo`, `mute` | | Note | `id`, `position`, `duration`, `noteNumber`, `lyric` | | Tempo | `position`, `bpm` | | TimeSignature | `measureNumber`, `beats`, `beatType` | レンダリング時だけ、テンポマップを使ってtickを秒へ変換し、エンジンの `frame_rate` に合わせて `frame_length` を計算する。 **Xcode向け補足:** `Codable` で `.vvproj` 風のJSONを読み書きする場合、`tracks` はIDをキーにしたDictionary、`trackOrder` は表示順配列として分ける。保存前に `trackOrder` の重複、存在しないID、未並び替えのトラックを検証すると、読み込み後のUI崩れを防げる。 ## 5. ソング合成は4段階APIとして実装する ソングのレンダリングは、トークの `/audio_query` → `/synthesis` より手順が多い。基本パイプラインは次の4段階。 ```mermaid theme={null} sequenceDiagram participant App as クライアント
アプリ participant Engine as VOICEVOX
エンジン App->>Engine: POST /sing_frame_audio_query?speaker=6000 Engine-->>App: FrameAudioQuery App->>Engine: POST /sing_frame_f0?speaker=6000 Engine-->>App: f0[] App->>Engine: POST /sing_frame_volume?speaker=6000 Engine-->>App: volume[] App->>Engine: POST /frame_synthesis?speaker=track.styleId Engine-->>App: WAV ``` `/sing_frame_audio_query`, `/sing_frame_f0`, `/sing_frame_volume` では singing teacher のstyle IDである `6000` を使い、最後の `/frame_synthesis` だけ実際に選択された歌手の `styleId` を使う。この値は公式エンジンでもLaravel版エンジンでも同じ前提で扱える。 各APIのリクエストでは、`Score` と `FrameAudioQuery` の対応を崩さないことが重要。`Score.notes[].frame_length` はエンジンの `frame_rate` から算出し、無音は `key: null`, `lyric: ""` として表現する。 **Xcode向け補足:** Swiftでは `VoicevoxEngineSongAPI` のようなprotocolを作り、実装を `URLSession` クライアントにするとテストしやすい。JSONを返すエンドポイントでは `Content-Type` / `Accept` を `application/json` にし、`/frame_synthesis` のレスポンスはWAVのバイナリ `Data` として扱う。HTTP 2xx以外はステータスコードとレスポンス本文を含むエラーにする。 ## 6. フレーズ単位で分割してキャッシュする ソング全体を毎回1回で合成すると、少し編集しただけでも全トラック再生成になり、UIの応答が悪くなる。公式エディターと同様に、連続するノート群をフレーズとして分け、休符で分割する設計が扱いやすい。 フレーズごとに次の段階をキャッシュする。 | キャッシュ | 入力に含めるもの | | - | - | | AudioQuery | エンジンID、frame rate、テンポ、Score、音域調整 | | F0 | AudioQuery、ピッチ編集、フレーズ位置 | | Volume | AudioQuery、F0、音量生成に影響する値 | | Voice/WAV | 合成用Query、最終style ID | 初期実装では、編集が入ったら対象トラックのレンダリング済み音声を丸ごと無効化してもよい。後からフレーズ単位の差分無効化を足せるように、入力ハッシュでキャッシュキーを作る。 **Xcode向け補足:** `actor` にレンダリングキャッシュを持たせると、Swift Concurrency上で状態管理しやすい。新しいレンダリングが始まったら世代番号を進め、古いAPIレスポンスが返ってきても採用しない。`Task.checkCancellation()` と世代チェックを組み合わせると、キャンセルと再レンダリングの両方を扱える。 ## 7. フレームレートと休符をエンジンメタ情報から決める `ScoreNote.frame_length` は秒数と `frame_rate` から計算する。`frame_rate` を固定値として埋め込むのではなく、登録時に `/engine_manifest` から取得して保存する。 ソング合成では、フレーズ先頭と末尾に休符を入れる。先頭休符が短すぎると音素生成が不安定になることがあるため、実装では「実際の休符」「4分音符」「最低秒数から逆算したtick」のバランスを取る。末尾にも短い無音を追加し、必要なら最後の無音区間をフェードアウトする。 また、フレーム長は丸めの結果0以下にならないよう補正する。短いノートやテンポ変更直後では、丸め誤差で0フレームが発生しやすい。 **Xcode向け補足:** `tickToSecond` / `secondToTick` を純粋関数にしてテストしておくと、ピアノロール、再生ヘッド、レンダリング、動画出力で同じ変換を使える。`frameLength = Int(round(seconds * frameRate))` の結果は、隣接ノートへ差分を逃がすなどして最低1フレームを保証する。 ## 8. パラメータ編集は「生成前」と「合成前」のどこで効くかを分ける ソングでは、音域・声量・ピッチ・ボリューム・音素タイミング編集がそれぞれ別の段階に影響する。 | 編集 | 反映タイミング | | - | - | | `keyRangeAdjustment` | Score生成時のキー、F0生成後のピッチシフト | | `volumeRangeAdjustment` | `/frame_synthesis` 前の音量配列 | | ピッチ編集 | `/sing_frame_f0` 後のF0配列 | | ボリューム編集 | `/sing_frame_volume` 後のvolume配列 | | 音素タイミング編集 | `FrameAudioQuery.phonemes` | どの編集がどのキャッシュを無効化するかを早めに決めておくと、後からUI編集機能を追加してもレンダリング結果が破綻しにくい。 **Xcode向け補足:** パラメータ編集の適用はViewModelに散らさず、`SongParameterEditApplicator` のような純粋ロジックに寄せるとテストしやすい。配列の範囲外アクセスや負の音量は明示的に補正・エラー化する。 ## 9. 再生・ミックス・書き出しは同じトラック判定を使う レンダリング後は、フレーズごとのWAVをタイムライン上に配置して再生する。マルチトラック対応では、通常再生、全体WAV書き出し、ステム書き出しでsolo/mute/gain/panの判定がずれると、ユーザーにとって予測しづらい挙動になる。 共通ルールとして、soloトラックが1つでもある場合はsoloトラックだけを出し、soloがない場合はmuteされていないトラックを出す。ステム書き出しでは対象トラックを単独で出力し、通常のsolo/mute状態とは切り離して考える。 **Xcode向け補足:** macOSでは `AVAudioEngine`, `AVAudioPlayerNode`, `AVAudioMixerNode` で再生グラフを作れる。書き出しは通常再生とは別にオフラインレンダリング用の経路を用意し、WAVエンコードを共通化するとよい。再生中に編集が入ってレンダリング済み音声が無効になったら、再生を停止して状態を明示する。 ## 10. UIは「未登録・未割り当て・未レンダリング」を区別する VOICEVOXエンジンAPI連携アプリでは、失敗理由がいくつかに分かれる。 | 状態 | 表示すべき案内 | | - | - | | エンジン未登録 | エンジン接続画面でURLを登録する | | エンジン未起動 | エンジンを起動して再接続する | | プロジェクトの `engineId` が未登録 | 対応するエンジンURLを追加登録する | | トラックに歌手未割り当て | 登録済み歌手スタイルを選ぶ | | 編集後で音声が古い | 再レンダリングが必要 | | レンダリング中 | キャンセル可能にする | これらを全部「再生できません」にまとめると原因が分からない。状態を分け、次の操作をUIに出す。 **Xcode向け補足:** SwiftUIのViewModelでは `renderingState` を `idle`, `rendering`, `rendered`, `stale`, `failed` のようなenumにすると、ボタンのdisabled制御とステータス表示を一貫させやすい。エラーメッセージはステータスバーだけでなくアラートにも出せるようにしておく。 ## 11. マルチエンジンでは「URL」と「engineId」の寿命を分ける マルチエンジン対応で混乱しやすいのは、URLと `engineId` の役割が違うこと。 | 値 | 寿命 | 使い方 | | - | - | - | | URL | ユーザー環境ごとに変わる | API呼び出し先として使う | | `engineId` | エンジン実装・モデル側の識別子 | プロジェクト内の参照として使う | | `styleId` | エンジン内のスタイル識別子 | `speaker` パラメータとして使う | プロジェクトファイルにはURLを埋め込まず、`engineId` と `styleId` を保存する。アプリ設定側に `engineId -> URL` の対応を持たせる。これにより、他人から受け取ったプロジェクトでも、自分の環境で同じエンジンをURL登録すればレンダリングできる。 **Xcode向け補足:** `Dictionary(uniqueKeysWithValues: registeredEngines.map { ($0.engineID, $0) })` のようにレンダリング前に `engineId` から `RegisteredEngine` を解決する。見つからない場合はHTTPリクエストを投げず、事前に「歌手のエンジンが登録されていません」と失敗させる。 ## 12. テストはHTTPクライアント・モデル変換・レンダリング計画を分ける VOICEVOXエンジン本体を常に起動してテストするのは重いので、通常の自動テストではHTTPをモックし、アプリ側のリクエスト生成と状態遷移を確認する。 優先してテストしたい項目は次の通り。 | 対象 | 確認すること | | - | - | | URL正規化 | 末尾スラッシュやパス付きURLが期待通り扱われる | | エンジン登録 | `/engine_manifest`, `/version`, `/singers`, `/singer_info` を呼び、保存用モデルを作れる | | `.vvproj` I/O | trackOrderとtracksの整合性、古い/未来バージョンの扱い | | tick/second変換 | テンポ変更をまたぐ変換が往復できる | | Score生成 | 休符、frame\_length、デフォルト歌詞、音域調整 | | レンダリング計画 | 未登録エンジン、歌手未設定、engineId不一致をAPI呼び出し前に検出する | | キャッシュ | 同じ入力で再利用し、編集後に無効化される | **Xcode向け補足:** `URLProtocol` を差し替えた `URLSessionConfiguration.ephemeral` を使うと、実HTTPサーバーなしで `URLSession` クライアントをテストできる。レンダラーはprotocol越しにAPIを注入し、`actor` のキャッシュ状態はスナップショットを返すメソッドを用意すると検証しやすい。 ## 実装順のおすすめ 最小構成から始めるなら、次の順がよい。 URLを登録し、`/engine_manifest` や `/version` から取得したメタ情報を一緒に保存する。 歌手スタイル一覧を表示し、トラックへ `engineId + styleId` を割り当てる。 tick基準のSong/Track/Noteモデルを先に固める。 `Score` 生成と4段階ソングAPIクライアントを実装する。 フレーズ単位レンダリングとキャッシュを導入する。 レンダリング済みWAVをタイムライン上で再生できるようにする。 マルチトラックのsolo/mute/gain/pan判定を統一する。 WAV書き出しとステム書き出しを追加する。 ピッチ・ボリューム・音素タイミング編集を追加する。 `.vvproj` 互換読み書きや動画出力などの発展機能へ進む。 最初から公式エディターの全機能を追うより、URL登録・歌手割り当て・4段階API・再レンダリング状態を先に固めると、公式エンジンにもLaravel版エンジンにも寄せやすい。 ## 関連リンク * [VOICEVOX Core for PHP パッケージページ](/jp/packages/voicevox-core-php) * [エンジン API モード:トーク](/jp/packages/laravel-voicevox/engine-talk) * [エンジン API モード:ソング](/jp/packages/laravel-voicevox/engine-song) * [Score と Note 詳細解説](/jp/packages/laravel-voicevox/song-score-note) * [.vvproj ファイル仕様](/jp/packages/laravel-voicevox/vvproj) # クライアントモード - プリセット - VOICEVOX for Laravel Source: https://kawax.biz/jp/packages/laravel-voicevox/client-presets VOICEVOX for Laravel のクライアントモードで公式エンジンのプリセットを操作し、音声合成パラメータを保存・再利用する方法を説明します。 プリセット機能を使うと、よく使う音声合成パラメータの組み合わせを公式 VOICEVOX エンジンに保存して再利用できます。クライアントモードでは Laravel 側にプリセットを保存せず、接続先の公式エンジンが管理するプリセット API を利用します。 ## 概要 クライアントモードのプリセットは `Voicevox` Facade から操作します。内部では公式 VOICEVOX エンジンの `/presets`、`/add_preset`、`/update_preset`、`/delete_preset`、`/audio_query_from_preset` に HTTP リクエストを送信します。 公式エンジンのプリセットは OS ごとのユーザー領域に `presets.yaml` として保存されます。VOICEVOXアプリ(製品版)macOS のビルド版では通常、次の場所に保存されます。 ```text theme={null} ~/Library/Application Support/voicevox-engine/presets.yaml ``` Docker で公式エンジンを起動している場合も、コンテナ内のユーザー領域に保存されます。コンテナを再起動しても同じコンテナを使い続けている間はデータが維持されます。ただし `docker run --rm` のように一時コンテナを都度作成している場合は、コンテナ終了時にプリセットが失われます。プリセットを確実に永続化したい場合は Docker volume でコンテナ内のユーザー領域をマウントしてください。 ネイティブモードの `storage/voicevox/presets.json` とは別管理です。Laravel 側の `config('voicevox.core.presets')` はクライアントモードには使われません。 ## 事前準備 公式 VOICEVOX エンジンを起動します。 ```shell theme={null} docker pull voicevox/voicevox_engine:cpu-latest docker run --rm -p '127.0.0.1:50021:50021' voicevox/voicevox_engine:cpu-latest ``` 接続先は `VOICEVOX_URL` で変更できます。 ```env theme={null} VOICEVOX_URL=http://127.0.0.1:50021 ``` ## プリセットの構造 クライアントモードで送受信するプリセットは、公式エンジン API のスキーマに合わせた配列です。 ```php theme={null} [ 'id' => 1, // プリセット ID(整数) 'name' => 'ゆっくり', // プリセット名 'speaker_uuid' => 'uuid', // スピーカー UUID(文字列) 'style_id' => 1, // スタイル ID(整数) 'speedScale' => 0.8, // 話速 'pitchScale' => 0.0, // 音高 'intonationScale' => 1.0, // 抑揚 'volumeScale' => 1.0, // 音量 'prePhonemeLength' => 0.1, // 開始無音 'postPhonemeLength' => 0.1, // 終了無音 ] ``` ## 基本的な使い方 ### プリセットの作成 `Voicevox::addPreset()` でプリセットを作成できます。公式エンジンでは `id` に `0` を指定すると未使用の ID が割り当てられます。 ```php theme={null} use Revolution\Voicevox\Voicevox; $id = Voicevox::addPreset([ 'id' => 0, 'name' => 'ゆっくり丁寧', 'speaker_uuid' => '7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff', 'style_id' => 1, 'speedScale' => 0.8, 'pitchScale' => 0.0, 'intonationScale' => 1.2, 'volumeScale' => 1.0, 'prePhonemeLength' => 0.1, 'postPhonemeLength' => 0.1, ]); echo $id; // 例: 1 ``` ### 全プリセットの取得 ```php theme={null} use Revolution\Voicevox\Voicevox; $presets = Voicevox::presets(); /* [ [ "id" => 1, "name" => "ゆっくり丁寧", "speaker_uuid" => "7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff", "style_id" => 1, "speedScale" => 0.8, ... ], ... ] */ ``` ### プリセットの更新 ```php theme={null} use Revolution\Voicevox\Voicevox; $id = Voicevox::updatePreset([ 'id' => 1, 'name' => 'ゆっくりはっきり', 'speaker_uuid' => '7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff', 'style_id' => 1, 'speedScale' => 0.7, 'pitchScale' => 0.0, 'intonationScale' => 1.5, 'volumeScale' => 1.0, 'prePhonemeLength' => 0.1, 'postPhonemeLength' => 0.1, ]); ``` ### プリセットの削除 ```php theme={null} use Revolution\Voicevox\Voicevox; Voicevox::deletePreset(1); ``` ## プリセットを使った音声合成 ### talkFromPreset() `Voicevox::talkFromPreset()` は公式エンジンの `/audio_query_from_preset` を使って、プリセット適用済みの `TalkAudioQuery` を作成します。 ```php theme={null} use Revolution\Voicevox\Voicevox; $response = Voicevox::talkFromPreset('プリセットを使うのだ', presetId: 1) ->generate(id: 1); $response->storeAs('client', 'preset-talk.wav'); ``` ### audioQueryFromPreset() 音声合成前のオーディオクエリ配列だけを取得したい場合は `audioQueryFromPreset()` を使います。 ```php theme={null} use Revolution\Voicevox\Voicevox; $audioQuery = Voicevox::audioQueryFromPreset( text: 'プリセットのクエリだけ作るのだ', presetId: 1, ); $audioQuery['speedScale'] = 1.1; ``` ### tap() で追加調整 プリセットから作成した `TalkAudioQuery` も、通常の `Voicevox::talk()` と同じように `tap()` で調整できます。 ```php theme={null} use Revolution\Voicevox\Client\TalkAudioQuery; use Revolution\Voicevox\Voicevox; $response = Voicevox::talkFromPreset('少し速く読むのだ', presetId: 1) ->tap(function (TalkAudioQuery $talk) { $talk->audioQuery['speedScale'] = 1.2; }) ->generate(id: 1); ``` ## Engine API との対応 `Voicevox` Facade の各メソッドは、公式エンジンの API に対応しています。 | Laravel | 公式エンジン API | | - | - | | `Voicevox::presets()` | `GET /presets` | | `Voicevox::addPreset()` | `POST /add_preset` | | `Voicevox::updatePreset()` | `POST /update_preset` | | `Voicevox::deletePreset()` | `POST /delete_preset` | | `Voicevox::audioQueryFromPreset()` | `POST /audio_query_from_preset` | | `Voicevox::talkFromPreset()` | `POST /audio_query_from_preset` + `POST /synthesis` | ## ネイティブモードとの違い | 項目 | クライアントモード | ネイティブモード | | - | - | - | | 利用方法 | `Voicevox` Facade | `preset()` / `talk()` ヘルパー | | 保存形式 | 公式エンジンの `presets.yaml` | Laravel の `presets.json` | | macOS の例 | `~/Library/Application Support/voicevox-engine/presets.yaml` | `storage/voicevox/presets.json` | | 音声クエリ生成 | 公式エンジンの `/audio_query_from_preset` | Laravel 側のネイティブ実装 | | エンジンプロセス | 必要 | 不要(FFI が必要) | ## 実用例 ### ナレーション用 ```php theme={null} use Revolution\Voicevox\Voicevox; Voicevox::addPreset([ 'id' => 0, 'name' => 'ナレーション', 'speaker_uuid' => '7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff', 'style_id' => 1, 'speedScale' => 1.0, 'pitchScale' => -0.05, 'intonationScale' => 0.8, 'volumeScale' => 1.0, 'prePhonemeLength' => 0.15, 'postPhonemeLength' => 0.15, ]); ``` ### 早口トーク用 ```php theme={null} Voicevox::addPreset([ 'id' => 0, 'name' => '早口', 'speaker_uuid' => '388f246b-8c41-4ac1-8e2d-5d79f3ff56d9', 'style_id' => 3, 'speedScale' => 1.5, 'pitchScale' => 0.05, 'intonationScale' => 1.3, 'volumeScale' => 1.0, 'prePhonemeLength' => 0.05, 'postPhonemeLength' => 0.05, ]); ``` ## パラメータ詳細 * `speedScale`: 0.5 〜 2.0(話速) * `pitchScale`: -0.15 〜 0.15(音高) * `intonationScale`: 0.0 〜 2.0(抑揚) * `volumeScale`: 0.0 〜 2.0(音量) * `prePhonemeLength`: 0.0 〜 1.5(開始無音・秒) * `postPhonemeLength`: 0.0 〜 1.5(終了無音・秒) ## 注意事項 クライアントモードでは公式エンジン側の `presets.yaml` が更新されます。Laravel プロジェクトの `storage` には保存されません。 * `docker run --rm` など一時コンテナを都度作成している場合、コンテナ終了時にプリセットが失われます。 * `speaker_uuid` と `style_id` の両方を含めてください。 * `talkFromPreset()` で作ったクエリを `generate(id:)` する場合、通常はプリセットの `style_id` と同じ ID を指定します。 ## トラブルシューティング ### プリセットが取得できない 1. 公式 VOICEVOX エンジンが起動しているか確認してください。 2. `VOICEVOX_URL` が正しい接続先を指しているか確認してください。 3. Docker で別コンテナを起動し直していないか確認してください。 ### プリセットファイルの場所がわからない macOS のビルド版では次の場所を確認してください。 ```text theme={null} ~/Library/Application Support/voicevox-engine/presets.yaml ``` Docker では公式エンジンのユーザー領域(コンテナ内)に保存されます。永続化が必要な場合は、コンテナ内のユーザー領域を Docker volume にマウントしてください。 ### speaker\_uuid がわからない スピーカー一覧から UUID を取得できます。 ```php theme={null} use Revolution\Voicevox\Voicevox; $speakers = Voicevox::speakers(); foreach ($speakers as $speaker) { echo $speaker['name'].': '.$speaker['speaker_uuid'].PHP_EOL; } ``` # クライアントモード:ソング(歌声音声合成) - VOICEVOX for Laravel Source: https://kawax.biz/jp/packages/laravel-voicevox/client-song VOICEVOX for Laravel のクライアントモードで、Score と Note から歌声音声合成する方法を説明します。 ## クライアントモードで歌わせる クライアントモードの歌声合成では、公式 VOICEVOX エンジンの `sing_frame_audio_query` と `frame_synthesis` API を使います。 Talk と違って、先に `Score` と `Note` を組み立てる必要があります。 ## 事前準備 ```shell theme={null} docker pull voicevox/voicevox_engine:cpu-latest docker run --rm -p '127.0.0.1:50021:50021' voicevox/voicevox_engine:cpu-latest ``` ## `Score` と `Note` を作る ```php theme={null} use Revolution\Voicevox\Song\Note; use Revolution\Voicevox\Song\Score; $score = Score::make([ Note::make(length: 15), Note::make(length: Note::len(ticks: 480, bpm: 120), lyric: 'ド', key: 60), Note::make(length: Note::len(480, 120), lyric: 'レ', key: 62), Note::make(length: Note::len(960, 120), lyric: 'ミ', key: 64), Note::make(length: 2), ]); ``` | パラメーター | 型 | 説明 | | - | - | - | | `length` | `int` | フレーム長 | | `lyric` | `string` | 歌詞。空文字または省略で休符 | | `key` | `int\|null` | MIDI ノート番号 | `Note::len($ticks, $bpm)` を使うと、MIDI の ticks と BPM からフレーム長を計算できます。四分音符を 480 ticks として扱うと分かりやすいです。 ## 基本的な使い方 ```php theme={null} use Revolution\Voicevox\Song\Note; use Revolution\Voicevox\Song\Score; use Revolution\Voicevox\Voicevox; $score = Score::make([ Note::make(length: 15), Note::make(length: Note::len(480, 120), lyric: 'ド', key: 60), Note::make(length: Note::len(480, 120), lyric: 'レ', key: 62), Note::make(length: Note::len(960, 120), lyric: 'ミ', key: 64), Note::make(length: 2), ]); $response = Voicevox::song($score, teacher: 6000) ->generate(id: 3001); $response->storeAs('client', 'song.wav'); ``` ### `teacher` と `id` * `teacher`: `sing_frame_audio_query` に渡すスタイル ID。現状は6000ひとつしか存在しないのでデフォルトで6000。 * `generate(id:)`: `frame_synthesis` に渡すスタイル ID 利用可能な歌声スピーカー一覧は `singers()` で取得できます。 ```php theme={null} use Revolution\Voicevox\Voicevox; $singers = Voicevox::singers(); ``` ## `tap()` で F0 とボリュームを調整する Song では `tap()` の使いどころが特に分かりやすく、`SongAudioQuery` を調整してから `generate()` へ流せます。 ```php theme={null} use Revolution\Voicevox\Client\SongAudioQuery; use Revolution\Voicevox\Voicevox; $response = Voicevox::song($score, teacher: 6000) ->tap(function (SongAudioQuery $song) { $song->sync(); }) ->generate(id: 3001); ``` F0 とボリュームを個別に更新する場合は **F0 → ボリューム** の順で実行します。 ```php theme={null} ->tap(function (SongAudioQuery $song) { $song->updateF0(); $song->updateVolume(); }) ``` Score を変更してから再同期したいときも `tap()` が便利です。 ```php theme={null} ->tap(function (SongAudioQuery $song) { $song->score->notes[2] = Note::make(length: Note::len(480, 120), lyric: 'ファ', key: 65); $song->sync(); }) ``` `tap()` の発想は [tap() ヘルパーと Tappable トレイト](/jp/advanced/tap) にまとめています。VOICEVOX for Laravel は「戻り値を変えずに中間オブジェクトだけ調整したい」典型例です。 ## 次に読むページ フレーム長計算と JSON 管理パターンを確認します。 同じ `Score` を FFI 経由のネイティブモードで使います。 コアライブラリ単体での音声合成フローを確認します。 # クライアントモード:トーク(テキスト音声合成) - VOICEVOX for Laravel Source: https://kawax.biz/jp/packages/laravel-voicevox/client-talk VOICEVOX for Laravel のクライアントモードで、公式 VOICEVOX エンジンに接続してテキスト音声合成する手順を説明します。 ## クライアントモードとは クライアントモードは、公式 VOICEVOX エンジンに HTTP リクエストを送って音声を生成します。 FFI は不要なので、まず試す入口として使いやすいモードです。 導入がまだなら [インストールと設定](/jp/packages/laravel-voicevox/installation) を先に確認してください。 ## 事前準備 ```shell theme={null} docker pull voicevox/voicevox_engine:cpu-latest docker run --rm -p '127.0.0.1:50021:50021' voicevox/voicevox_engine:cpu-latest ``` ```dotenv theme={null} VOICEVOX_URL=http://127.0.0.1:50021 ``` GPU が使える環境なら GPU イメージも利用できます。 ## 基本的な使い方 ```php theme={null} use Revolution\Voicevox\Voicevox; $response = Voicevox::talk('Laravelが好きなのだ', id: 1) ->generate(id: 1); $response->storeAs('client', 'talk.wav'); ``` ## スタイル ID の指定 `talk()` と `generate()` の両方にスタイル ID を渡せます。 * `talk(id:)`: `audio_query` を作るときのスタイル * `generate(id:)`: 音声合成するときのスタイル 通常は同じ値を指定します。 ```php theme={null} use Revolution\Voicevox\Voicevox; $response = Voicevox::talk('ずんだもんなのだ', id: 1) ->generate(id: 1); ``` 利用可能なスピーカー一覧は `speakers()` で取得できます。 ```php theme={null} use Revolution\Voicevox\Voicevox; $speakers = Voicevox::speakers(); ``` ## `tap()` で Audio Query を調整する このパッケージでは `generate()` の前に `TalkAudioQuery` を受け取れるので、`tap()` で副作用だけ差し込む形がきれいにハマります。 ```php theme={null} use Revolution\Voicevox\Client\TalkAudioQuery; use Revolution\Voicevox\Voicevox; $response = Voicevox::talk('速めに話すのだ', id: 1) ->tap(function (TalkAudioQuery $talk) { $talk->audioQuery['speedScale'] = 1.2; $talk->audioQuery['pitchScale'] = 0.05; $talk->audioQuery['intonationScale'] = 1.5; $talk->audioQuery['volumeScale'] = 1.0; }) ->generate(id: 1); ``` `tap()` はクエリーの微調整だけを行い、戻り値はそのまま次の `generate()` に渡せます。考え方は [tap() ヘルパーと Tappable トレイト](/jp/advanced/tap) で詳しく整理しています。 ## 英語の自動カタカナ変換 クライアントモードでは `enable_katakana_english` が有効なので、英語を含むテキストを自動でカタカナ寄りに補正できます。 ```php theme={null} $response = Voicevox::talk('I love Laravel', id: 1) ->generate(id: 1); ``` 無効にしたい場合は第3引数に `false` を渡します。 ```php theme={null} $response = Voicevox::talk('I love Laravel', id: 1, enableKatakanaEnglish: false) ->generate(id: 1); ``` ## レスポンスを保存する ```php theme={null} $response = Voicevox::talk('テスト', id: 1)->generate(id: 1); $wav = $response->content(); $path = $response->storeAs('client', 'talk.wav'); $path = $response->storeAs('client', 'talk.wav', disk: 's3'); ``` ## 次に読むページ 歌声合成で `Score` と `Note` を組み立てます。 FFI を使う前提になったらコアライブラリ側の使い方も確認します。 # クライアントモード - ユーザー辞書 - VOICEVOX for Laravel Source: https://kawax.biz/jp/packages/laravel-voicevox/client-user-dict VOICEVOX for Laravel のクライアントモードで公式エンジンのユーザー辞書を操作し、固有名詞や専門用語の読み方を登録する方法を説明します。 ユーザー辞書を使うと、固有名詞や専門用語の読み方を公式 VOICEVOX エンジンに登録できます。クライアントモードでは Laravel 側に辞書を保存せず、接続先の公式エンジンが管理するユーザー辞書 API をそのまま利用します。 ## 概要 クライアントモードのユーザー辞書は `Voicevox` Facade から操作します。内部では公式 VOICEVOX エンジンの `/user_dict`、`/user_dict_word`、`/import_user_dict` に HTTP リクエストを送信します。 公式エンジンの辞書データは OS ごとのユーザー領域に保存されます。VOICEVOXアプリ(製品版)macOS のビルド版では通常、次の場所に保存されます。 ```text theme={null} ~/Library/Application Support/voicevox-engine/user_dict.json ``` Docker で公式エンジンを起動している場合も、コンテナ内のユーザー領域に保存されます。コンテナを再起動しても同じコンテナを使い続けている間はデータが維持されます。ただし `docker run --rm` のように一時コンテナを都度作成している場合は、コンテナ終了時に辞書が失われます。辞書を確実に永続化したい場合は Docker volume でコンテナ内のユーザー領域をマウントしてください。 ネイティブモードの `storage/voicevox/user_dict.json` とは別管理です。Laravel 側の `config('voicevox.core.user_dict')` はクライアントモードには使われません。 ## 事前準備 公式 VOICEVOX エンジンを起動します。 ```shell theme={null} docker pull voicevox/voicevox_engine:cpu-latest docker run --rm -p '127.0.0.1:50021:50021' voicevox/voicevox_engine:cpu-latest ``` 接続先は `VOICEVOX_URL` で変更できます。 ```env theme={null} VOICEVOX_URL=http://127.0.0.1:50021 ``` ## 基本的な使い方 ### 単語の追加 `Voicevox::addWord()` で単語を登録できます。 ```php theme={null} use Revolution\Voicevox\Voicevox; $uuid = Voicevox::addWord( surface: 'Laravel', pronunciation: 'ララベル', accentType: 0, ); // クライアントモードでは "-" ありの UUID が返る // 例: "a1b2c3d4-0000-0000-0000-000000000000" ``` #### パラメータ | パラメータ | 型 | 説明 | | - | - | - | | `surface` | string | 表記(実際の単語の文字列) | | `pronunciation` | string | 読み方(カタカナのみ) | | `accentType` | int | アクセント型(0 = 平板、1 以上 = アクセント位置) | | `wordType` | string\|null | 品詞(`COMMON_NOUN`、`PROPER_NOUN`、`VERB`、`ADJECTIVE`、`SUFFIX`) | | `priority` | int\|null | 優先度 | ### 全単語の取得 登録されているすべての単語を取得できます。 ```php theme={null} use Revolution\Voicevox\Voicevox; $words = Voicevox::userDict(); /* [ "a1b2c3d4-0000-0000-0000-000000000000" => [ "surface" => "Laravel", "pronunciation" => "ララベル", "accent_type" => 0, "word_type" => "COMMON_NOUN", "priority" => 5, ], ... ] */ ``` ### 単語の更新 追加時に返ってきた UUID を指定して更新します。 ```php theme={null} use Revolution\Voicevox\Voicevox; Voicevox::updateWord( wordUuid: 'a1b2c3d4-0000-0000-0000-000000000000', surface: 'Laravel', pronunciation: 'ラレベル', accentType: 1, ); ``` ### 単語の削除 ```php theme={null} use Revolution\Voicevox\Voicevox; Voicevox::deleteWord('a1b2c3d4-0000-0000-0000-000000000000'); ``` ### 辞書のインポート 公式エンジンのユーザー辞書形式の配列をインポートできます。`override: false`(デフォルト)なら既存の単語は保持され、インポートされた単語が追加されます。 ```php theme={null} use Revolution\Voicevox\Voicevox; $words = json_decode(file_get_contents('other_user_dict.json'), true); Voicevox::importUserDict($words, override: false); // override: true なら既存辞書を置き換える Voicevox::importUserDict($words, override: true); ``` 辞書をエクスポートしたい場合は `Voicevox::userDict()` の結果を JSON に保存します。 ```php theme={null} $json = json_encode(Voicevox::userDict(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); file_put_contents('exported_user_dict.json', $json); ``` ## 辞書登録後の音声合成 ユーザー辞書に登録した単語は、公式エンジンで音声合成するときに自動的に参照されます。 ```php theme={null} use Revolution\Voicevox\Voicevox; Voicevox::addWord( surface: 'Laravel', pronunciation: 'ララベル', accentType: 0, ); $response = Voicevox::talk('Laravelで開発するのは楽しいです', id: 1)->generate(id: 1); $response->storeAs('client', 'laravel.wav'); ``` ## Engine API との対応 `Voicevox` Facade の各メソッドは、公式エンジンの API に対応しています。 | Laravel | 公式エンジン API | | - | - | | `Voicevox::userDict()` | `GET /user_dict` | | `Voicevox::addWord()` | `POST /user_dict_word` | | `Voicevox::updateWord()` | `PUT /user_dict_word/{word_uuid}` | | `Voicevox::deleteWord()` | `DELETE /user_dict_word/{word_uuid}` | | `Voicevox::importUserDict()` | `POST /import_user_dict` | ## ネイティブモードとの違い | 項目 | クライアントモード | ネイティブモード | | - | - | - | | 利用方法 | `Voicevox` Facade | `dict()` ヘルパー | | 保存先 | 公式エンジンのユーザー領域 | Laravel の `storage/voicevox/user_dict.json` | | macOS の例 | `~/Library/Application Support/voicevox-engine/user_dict.json` | `storage/voicevox/user_dict.json` | | 追加時の UUID | `-` あり | Core からは `-` なし | | エンジンプロセス | 必要 | 不要(FFI が必要) | ## 注意事項 `pronunciation` はカタカナで指定してください。ひらがなや漢字は使用できません。 * クライアントモードでは公式エンジン側の辞書が更新されます。Laravel プロジェクトの `storage` には保存されません。 * `docker run --rm` など一時コンテナを都度作成している場合、コンテナ終了時に辞書が失われます。 * 追加時に返る UUID は `a1b2c3d4-0000-0000-0000-000000000000` のようなハイフンあり形式です。 ## トラブルシューティング ### 単語が反映されない 1. 公式 VOICEVOX エンジンが起動しているか確認してください。 2. `VOICEVOX_URL` が正しい接続先を指しているか確認してください。 3. 読み方がカタカナになっているか確認してください。 4. Docker で別コンテナを起動し直していないか確認してください。 ### 辞書ファイルの場所がわからない macOS のビルド版では次の場所を確認してください。 ```text theme={null} ~/Library/Application Support/voicevox-engine/user_dict.json ``` Docker では公式エンジンのユーザー領域(コンテナ内)に保存されます。永続化が必要な場合は、コンテナ内のユーザー領域を Docker volume にマウントしてください。 # 設定リファレンス - VOICEVOX for Laravel Source: https://kawax.biz/jp/packages/laravel-voicevox/configuration config/voicevox.php の全設定項目と環境変数、デフォルト値、設定例を説明します。 このページでは `config/voicevox.php` の全設定項目をまとめます。 ## 設定ファイルを公開する ```shell theme={null} php artisan vendor:publish --tag="voicevox-config" ``` ## `client` 設定 ### `client.url` * Type: `string` * Default: `'http://127.0.0.1:50021'` * Env: `VOICEVOX_URL` ```php theme={null} 'client' => [ 'url' => env('VOICEVOX_URL', 'http://127.0.0.1:50021'), ], ``` ```dotenv theme={null} VOICEVOX_URL=http://127.0.0.1:50021 ``` ### `client.core_version` * Type: `string|null` * Default: `null` * Env: `VOICEVOX_CLIENT_CORE_VERSION` ```php theme={null} 'client' => [ 'core_version' => env('VOICEVOX_CLIENT_CORE_VERSION'), ], ``` ```dotenv theme={null} VOICEVOX_CLIENT_CORE_VERSION=0.15.0 ``` ## `core` 設定 ### `core.path` * Type: `string|null` * Default: `null` * Env: `VOICEVOX_CORE_PATH` * Required: ネイティブモード / エンジン API モード ```php theme={null} 'core' => [ 'path' => env('VOICEVOX_CORE_PATH'), ], ``` ```dotenv theme={null} # macOS VOICEVOX_CORE_PATH=/Users/username/.local/voicevox_core/ # Linux VOICEVOX_CORE_PATH=/home/username/.local/voicevox_core/ # Windows VOICEVOX_CORE_PATH=C:\Users\username\.local\voicevox_core\ ``` ### `core.dict` * Type: `string` * Default: `'dict/open_jtalk_dic_utf_8-1.11'` * Env: `VOICEVOX_CORE_DICT_PATH` ```php theme={null} 'core' => [ 'dict' => env('VOICEVOX_CORE_DICT_PATH', 'dict/open_jtalk_dic_utf_8-1.11'), ], ``` ### `core.models` * Type: `string` * Default: `'models/vvms'` * Env: `VOICEVOX_CORE_MODELS_PATH` ```php theme={null} 'core' => [ 'models' => env('VOICEVOX_CORE_MODELS_PATH', 'models/vvms'), ], ``` ### `core.user_dict` * Type: `string` * Default: `storage_path('voicevox/user_dict.json')` * Env: `VOICEVOX_CORE_USER_DICT_PATH` ```php theme={null} 'core' => [ 'user_dict' => env('VOICEVOX_CORE_USER_DICT_PATH', storage_path('voicevox/user_dict.json')), ], ``` ```dotenv theme={null} VOICEVOX_CORE_USER_DICT_PATH=/path/to/custom/user_dict.json ``` 関連ページ: [ユーザー辞書](/jp/packages/laravel-voicevox/native-user-dict) ### `core.presets` * Type: `string` * Default: `storage_path('voicevox/presets.json')` * Env: `VOICEVOX_CORE_PRESETS_PATH` ```php theme={null} 'core' => [ 'presets' => env('VOICEVOX_CORE_PRESETS_PATH', storage_path('voicevox/presets.json')), ], ``` ```dotenv theme={null} VOICEVOX_CORE_PRESETS_PATH=/path/to/custom/presets.json ``` 関連ページ: [プリセット](/jp/packages/laravel-voicevox/native-presets) ### `core.vvms` * Type: `array` * Default: `['0.vvm', '9.vvm', 's0.vvm']` * Env: なし ```php theme={null} # デフォルト(高速起動) 'core' => [ 'vvms' => ['0.vvm', '9.vvm', 's0.vvm'], ], # 全モデルを読み込む(起動は遅くなる) 'core' => [ 'vvms' => [], ], ``` ## `engine` 設定 ### `engine.disabled` * Type: `bool` * Default: `false` * Env: `VOICEVOX_ENGINE_DISABLED` ```php theme={null} 'engine' => [ 'disabled' => env('VOICEVOX_ENGINE_DISABLED', false), ], ``` ```dotenv theme={null} VOICEVOX_ENGINE_DISABLED=true ``` ### `engine.fallback_url` * Type: `string` * Default: `'http://127.0.0.1:50021'` * Env: `VOICEVOX_ENGINE_FALLBACK_URL` ```php theme={null} 'engine' => [ 'fallback_url' => env('VOICEVOX_ENGINE_FALLBACK_URL', 'http://127.0.0.1:50021'), ], ``` ```dotenv theme={null} VOICEVOX_ENGINE_FALLBACK_URL=http://127.0.0.1:50021 ``` ### `engine.fallback_error` * Type: `string` * Default: `'The Laravel version of the engine does not support this endpoint. Please use the official engine instead.'` * Env: なし ```php theme={null} 'engine' => [ 'fallback_error' => 'The Laravel version of the engine does not support this endpoint. Please use the official engine instead.', ], ``` まずは `client.url` と `core.path` だけを設定し、必要になったら `core.user_dict` や `core.presets` を調整する構成が運用しやすいです。 # 歌唱音声合成用のクエリを作成する Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/クエリ作成/歌唱音声合成用のクエリを作成する https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json post /sing_frame_audio_query 歌唱音声合成用のクエリの初期値を得ます。ここで得られたクエリはそのまま歌唱音声合成に利用できます。各値の意味は`Schemas`を参照してください。 **Laravel実装状況**: ✅ コアで完全対応(フォールバック実装済み) # 音声合成用のクエリをプリセットを用いて作成する Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/クエリ作成/音声合成用のクエリをプリセットを用いて作成する https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json post /audio_query_from_preset 音声合成用のクエリの初期値を得ます。ここで得られたクエリはそのまま音声合成に利用できます。各値の意味は`Schemas`を参照してください。 **Laravel実装状況**: ⚠️ NativePresetStoreとコアのcreateAudioQueryで対応。enable_katakana_english非対応(プリセットデータを共有してないのでフォールバックなし) # 音声合成用のクエリを作成する Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/クエリ作成/音声合成用のクエリを作成する https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json post /audio_query 音声合成用のクエリの初期値を得ます。ここで得られたクエリはそのまま音声合成に利用できます。各値の意味は`Schemas`を参照してください。 **Laravel実装状況**: ⚠️ Laravelではenable_katakana_english非対応(フォールバック実装済み) # アクセント句から音素の長さと音高を得る Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/クエリ編集/アクセント句から音素の長さと音高を得る https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json post /mora_data **Laravel実装状況**: ✅ コアで完全対応(フォールバック実装済み) # アクセント句から音素の長さを得る Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/クエリ編集/アクセント句から音素の長さを得る https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json post /mora_length **Laravel実装状況**: ✅ コアで完全対応(フォールバック実装済み) # アクセント句から音高を得る Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/クエリ編集/アクセント句から音高を得る https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json post /mora_pitch **Laravel実装状況**: ✅ コアで完全対応(フォールバック実装済み) # テキストからアクセント句を得る Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/クエリ編集/テキストからアクセント句を得る https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json post /accent_phrases テキストからアクセント句を得ます。 is_kanaが`true`のとき、テキストは次のAquesTalk 風記法で解釈されます。デフォルトは`false`です。 * 全てのカナはカタカナで記述される * アクセント句は`/`または`、`で区切る。`、`で区切った場合に限り無音区間が挿入される。 * カナの手前に`_`を入れるとそのカナは無声化される * アクセント位置を`'`で指定する。全てのアクセント句にはアクセント位置を1つ指定する必要がある。 * アクセント句末に`?`(全角)を入れることにより疑問文の発音ができる。 enable_katakana_englishが`true`のとき、テキスト中の読みが不明な英単語をカタカナ読みにします。デフォルトは`true`です。 is_kanaが`true`のとき、enable_katakana_englishの値は無視されます。 **Laravel実装状況**: ⚠️ is_kanaのtrue/false両対応、enable_katakana_english非対応(フォールバック実装済み) # 楽譜・歌唱音声合成用のクエリからフレームごとの基本周波数を得る Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/クエリ編集/楽譜・歌唱音声合成用のクエリからフレームごとの基本周波数を得る https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json post /sing_frame_f0 **Laravel実装状況**: ✅ コアのcreateSingFrameF0で直接対応(フォールバック実装済み) # 楽譜・歌唱音声合成用のクエリからフレームごとの音量を得る Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/クエリ編集/楽譜・歌唱音声合成用のクエリからフレームごとの音量を得る https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json post /sing_frame_volume **Laravel実装状況**: ✅ コアのcreateSingFrameVolumeで直接対応(フォールバック実装済み) # エンジン API モード:ソング(歌声音声合成) - VOICEVOX for Laravel Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-song Laravel アプリ内の VOICEVOX エンジン互換 API で、歌声合成エンドポイントを使う手順を説明します。 ## エンジン API で歌わせる エンジン API モードは、公式 VOICEVOX エンジン互換のソングエンドポイントも提供します。 エンジン API の基本セットアップは [エンジン API: トーク](/jp/packages/laravel-voicevox/engine-talk) を先に確認してください。 ## 事前準備 歌声モデルを使うため、`config/voicevox.php` の `core.vvms` に `s0.vvm` が含まれていることを確認します(デフォルトで含まれます)。 ```dotenv theme={null} VOICEVOX_CORE_PATH=/path/to/voicevox_core/ ``` ## Laravel 版エンジンを起動する ```shell theme={null} php artisan serve --port=50513 ``` ## 歌声音声を生成する ### 1. `sing_frame_audio_query` を作成する ```shell theme={null} curl -s -X POST "http://127.0.0.1:50513/sing_frame_audio_query?speaker=6000" \ -H "Content-Type: application/json" \ -d '{ "notes": [ {"id": "a", "key": null, "frame_length": 15, "lyric": ""}, {"id": "b", "key": 60, "frame_length": 94, "lyric": "ド"}, {"id": "c", "key": 62, "frame_length": 94, "lyric": "レ"}, {"id": "d", "key": 64, "frame_length": 187, "lyric": "ミ"}, {"id": "e", "key": null, "frame_length": 2, "lyric": ""} ] }' \ > frame_audio_query.json ``` `speaker` には `sing` または `singing_teacher` 種別のスタイル ID(例: 6000)を指定します。 ### 2. `frame_synthesis` で音声合成する ```shell theme={null} curl -s -X POST "http://127.0.0.1:50513/frame_synthesis?speaker=3001" \ -H "Content-Type: application/json" \ -d @frame_audio_query.json \ > song.wav ``` `speaker` には `sing` または `frame_decode` 種別のスタイル ID(例: 3001)を指定します。 ## Laravel クライアントから使う Laravel 版エンジンに向けて `Voicevox::song()` を利用できます。 ```php theme={null} // config/voicevox.php 'client' => [ 'url' => env('VOICEVOX_URL', 'http://127.0.0.1:50513'), ], ``` ```php theme={null} use Revolution\Voicevox\Song\Note; use Revolution\Voicevox\Song\Score; use Revolution\Voicevox\Voicevox; $score = Score::make([ Note::make(length: 15), Note::make(length: Note::len(480, 120), lyric: 'ド', key: 60), Note::make(length: Note::len(480, 120), lyric: 'レ', key: 62), Note::make(length: Note::len(960, 120), lyric: 'ミ', key: 64), Note::make(length: 2), ]); $response = Voicevox::song($score, teacher: 6000)->generate(id: 3001); $response->storeAs('engine', 'song.wav'); ``` ## F0・ボリューム調整エンドポイント ```shell theme={null} curl -s -X POST "http://127.0.0.1:50513/sing_frame_f0?speaker=6000" \ -H "Content-Type: application/json" \ -d '{"score": {...}, "frame_audio_query": {...}}' \ > f0.json curl -s -X POST "http://127.0.0.1:50513/sing_frame_volume?speaker=6000" \ -H "Content-Type: application/json" \ -d '{"score": {...}, "frame_audio_query": {...}}' \ > volume.json ``` F0 とボリュームを個別に更新する場合は、必ず **F0 → ボリューム** の順で実行してください。 ## ソング関連の対応状況 | エンドポイント | Laravel 版 | フォールバック | 備考 | | - | - | - | - | | `POST /sing_frame_audio_query` | ✅ | ✅ | | | `POST /frame_synthesis` | ✅ | ✅ | | | `POST /sing_frame_f0` | ✅ | ✅ | | | `POST /sing_frame_volume` | ✅ | ✅ | f0 → volume の順で更新 | | `GET /singers` | ✅ | ✅ | | | `GET /singer_info` | ✅ | ✅ | リソースインストールが必要 | ## 次に読むページ Song API で使う `Score` / `Note` の詳細を確認します。 テキスト音声合成エンドポイントの使い方を確認します。 同じ譜面を公式エンジンへ直接送るモードを確認します。 # エンジン API モード:トーク(テキスト音声合成) - VOICEVOX for Laravel Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-talk Laravel アプリに VOICEVOX エンジン互換 HTTP API を組み込み、公式エンジンと同じトークエンドポイントを使う方法を説明します。 ## エンジン API モードとは エンジン API モードは、公式 VOICEVOX エンジン互換の HTTP API を Laravel アプリ内で提供します。 `php artisan serve --port=50513` でアプリを起動するだけで、`/audio_query` や `/synthesis` をそのまま利用できます。 ## 事前準備 [インストールと設定](/jp/packages/laravel-voicevox/installation) の手順で `voicevox-core-php` とコアライブラリを用意します。 ```dotenv theme={null} VOICEVOX_CORE_PATH=/path/to/voicevox_core/ ``` ```ini theme={null} ffi.enable=true ``` ```shell theme={null} php artisan voicevox:install ``` ## Laravel 版エンジンを起動する ```shell theme={null} php artisan serve --port=50513 ``` デフォルトでは `http://127.0.0.1:50513` で起動します。 ### ポート 50513 について 公式 VOICEVOX エンジンのデフォルトポートは 50021 です。Laravel 版エンジンと公式エンジンを同時に起動できるよう、デフォルトポートを 50513 と定めています。 `--port=50513` を付けて起動しているだけなので、`--port=8000` など他のポートを使っても構いません。 公式エンジンはポートが使用中の場合、50022 以降の空きポートに自動調整して起動します。`php artisan serve` でも `--port` を指定しない場合は 8001 以降に自動調整しますが、`--port` を指定した状態でポートが使用中だと起動に失敗します。他のサービスと衝突しにくいポートを固定で指定することを推奨します。50513 はその一例です。 ## トーク音声を生成する ### 1. `audio_query` を作成する ```shell theme={null} curl -s -X POST "http://127.0.0.1:50513/audio_query?speaker=1&text=ララベルが好きなのだ" \ -H "Content-Type: application/json" \ > audio_query.json ``` ### 2. `synthesis` で音声合成する ```shell theme={null} curl -s -X POST "http://127.0.0.1:50513/synthesis?speaker=1" \ -H "Content-Type: application/json" \ -d @audio_query.json \ > talk.wav ``` ## Laravel クライアントから使う Laravel 版エンジンをクライアントモードの接続先として使えます。 ```php theme={null} // config/voicevox.php 'client' => [ 'url' => env('VOICEVOX_URL', 'http://127.0.0.1:50513'), ], ``` ```php theme={null} use Revolution\Voicevox\Voicevox; $response = Voicevox::talk('ララベルが好きなのだ', id: 1) ->generate(id: 1); $response->storeAs('engine', 'talk.wav'); ``` ## エンジン API の有効・無効とフォールバック ```php theme={null} 'engine' => [ 'disabled' => env('VOICEVOX_ENGINE_DISABLED', false), 'fallback_url' => env('VOICEVOX_ENGINE_FALLBACK_URL', 'http://127.0.0.1:50021'), ], ``` `cancellable_synthesis` や `multi_synthesis` のような未実装エンドポイントは、公式エンジンへフォールバックできます。 フォールバックを使う場合は、`VOICEVOX_ENGINE_FALLBACK_URL` 側で公式エンジンを起動してください。 ## トーク関連の対応状況 | エンドポイント | Laravel 版 | フォールバック | 備考 | | - | - | - | - | | `POST /audio_query` | ✅ | ✅ | `enable_katakana_english` 非対応 | | `POST /accent_phrases` | ✅ | ✅ | `enable_katakana_english` 非対応 | | `POST /synthesis` | ✅ | ✅ | | | `POST /mora_data` | ✅ | ✅ | | | `POST /mora_length` | ✅ | ✅ | | | `POST /mora_pitch` | ✅ | ✅ | | | `GET /speakers` | ✅ | ✅ | | | `GET /speaker_info` | ✅ | ✅ | リソースインストールが必要 | | `POST /cancellable_synthesis` | ❌ | ✅ | フォールバックのみ | | `POST /multi_synthesis` | ❌ | ✅ | フォールバックのみ | ## OpenAI 互換 TTS エンドポイント `POST /v1/audio/speech` で OpenAI の音声合成 API と同じリクエスト形式を受け付けます。Base URL を変更するだけで、Laravel AI SDK をはじめ OpenAI API 対応の多くのツールから利用できます。 ```shell theme={null} curl -s -X POST "http://127.0.0.1:50513/v1/audio/speech" \ -H "Content-Type: application/json" \ -d '{ "model": "voicevox", "input": "ララベルが好きなのだ", "voice": "ずんだもん" }' \ > talk.wav ``` ### パラメータ | パラメータ | 説明 | | - | - | | `input` | 読み上げるテキスト(最大 4096 文字) | | `voice` | スタイル ID(数値)、エイリアス(`ずんだもん`、`四国めたん` など)、または OpenAI ボイス名(`alloy`、`shimmer` など) | | `speed` | 再生速度。`speedScale` として適用(0.25〜4.0、省略時は 1.0) | | `response_format` | 常に wav で返します。指定値は無視されます | | その他のパラメータ | 無視されます | OpenAI ボイス名(`alloy`、`shimmer` など)は仮の割り当てです。OpenAI の各ボイスがどのような声質かを VOICEVOX キャラクターに対応づける定義はないため、互換性のために便宜的にマッピングしています。VOICEVOX キャラクターのエイリアス(`ずんだもん`、`四国めたん/ノーマル` など)の利用を推奨します。 ## 次に読むページ `sing_frame_audio_query` と `frame_synthesis` の使い方を確認します。 `Audio` ファサードから VOICEVOX を使う設定を確認します。 # はじめに - VOICEVOX for Laravel Source: https://kawax.biz/jp/packages/laravel-voicevox/getting-started VOICEVOX for Laravel の基本概念、3つのモード比較、クイックスタート(クライアントモード)、主要機能をまとめます。 VOICEVOX for Laravel は、Laravel で VOICEVOX の音声合成機能を使うためのパッケージです。テキストから日本語音声を生成できます。 まずはクライアントモードから始めるとセットアップが最短です。FFI が使える環境なら後からネイティブモードやエンジン API モードへ段階的に移行できます。 ## 3つの利用モード | モード | 実行方式 | FFI | 向いている用途 | ガイド | | - | - | - | - | - | | クライアントモード | 公式 VOICEVOX エンジンへ HTTP 接続 | 不要 | まず試す、Docker 環境、FFI が使えない環境 | [Client Talk](/jp/packages/laravel-voicevox/client-talk) / [Client Song](/jp/packages/laravel-voicevox/client-song) | | ネイティブモード | VOICEVOX CORE を PHP FFI で直接呼び出し | 必要 | 高速化、オフライン実行、CLI ツール統合 | [Native Talk](/jp/packages/laravel-voicevox/native-talk) / [Native Song](/jp/packages/laravel-voicevox/native-song) | | エンジン API モード | Laravel が VOICEVOX 互換 API を提供 | 必要 | API サーバー公開、複数クライアント連携 | [Engine Talk](/jp/packages/laravel-voicevox/engine-talk) / [Engine Song](/jp/packages/laravel-voicevox/engine-song) | ## クイックスタート クライアントモードの手順です。 ```shell theme={null} composer require revolution/laravel-voicevox ``` 詳細は [インストールと設定](/jp/packages/laravel-voicevox/installation) を確認してください。 ```shell theme={null} docker pull voicevox/voicevox_engine:cpu-latest docker run --rm -p '127.0.0.1:50021:50021' voicevox/voicevox_engine:cpu-latest ``` ```php theme={null} use Revolution\Voicevox\Voicevox; $response = Voicevox::talk('こんにちは、Laravel です。', id: 1)->generate(id: 1); $response->storeAs('output.wav'); ``` ## 主要な機能 ### 音声合成(Talk) ```php theme={null} use Revolution\Voicevox\Client\TalkAudioQuery; use Revolution\Voicevox\Voicevox; $response = Voicevox::talk('こんにちは', id: 1) ->tap(function (TalkAudioQuery $talk) { $talk->audioQuery['speedScale'] = 1.2; $talk->audioQuery['pitchScale'] = 0.05; $talk->audioQuery['intonationScale'] = 1.5; $talk->audioQuery['volumeScale'] = 1.0; }) ->generate(id: 1); ``` ### 歌声合成(Song) ```php theme={null} use Revolution\Voicevox\Song\Note; use Revolution\Voicevox\Song\Score; use Revolution\Voicevox\Voicevox; $score = Score::make([ Note::make(length: 15), Note::make(length: Note::len(ticks: 480, bpm: 120), lyric: 'ド', key: 60), Note::make(length: Note::len(480, 120), lyric: 'レ', key: 62), Note::make(length: Note::len(960, 120), lyric: 'ミ', key: 64), Note::make(length: 2), ]); $response = Voicevox::song($score)->generate(id: 3001); ``` ### ユーザー辞書とプリセット クライアントモードでは `Voicevox` Facade を使って公式エンジン側のユーザー辞書やプリセットを操作します。 ```php theme={null} use Revolution\Voicevox\Voicevox; $uuid = Voicevox::addWord( surface: 'Laravel', pronunciation: 'ララベル', accentType: 3, ); $presetId = Voicevox::addPreset([ 'id' => 0, 'name' => 'ゆっくり丁寧', 'speaker_uuid' => '7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff', 'style_id' => 1, 'speedScale' => 0.8, 'pitchScale' => 0.0, 'intonationScale' => 1.2, 'volumeScale' => 1.0, 'prePhonemeLength' => 0.1, 'postPhonemeLength' => 0.1, ]); ``` ### Laravel AI SDK 連携 ```php theme={null} use Laravel\Ai\Audio; $audio = Audio::of('こんにちは、Laravel です。') ->voice('ずんだもん') ->generate(); $audio->storeAs('output.wav'); ``` ## 次のステップ * [インストールと設定](/jp/packages/laravel-voicevox/installation) * [設定リファレンス](/jp/packages/laravel-voicevox/configuration) * [Client Talk](/jp/packages/laravel-voicevox/client-talk) * [Client Song](/jp/packages/laravel-voicevox/client-song) * [クライアントモード - ユーザー辞書](/jp/packages/laravel-voicevox/client-user-dict) * [クライアントモード - プリセット](/jp/packages/laravel-voicevox/client-presets) * [Native Talk](/jp/packages/laravel-voicevox/native-talk) * [Engine Talk](/jp/packages/laravel-voicevox/engine-talk) * [ネイティブモード - ユーザー辞書](/jp/packages/laravel-voicevox/native-user-dict) * [ネイティブモード - プリセット](/jp/packages/laravel-voicevox/native-presets) * [AI SDK 連携](/jp/packages/laravel-voicevox/ai-sdk) # VOICEVOX for Laravel Source: https://kawax.biz/jp/packages/laravel-voicevox/index Laravel から VOICEVOX のクライアント・ネイティブ・エンジン API を扱うパッケージの概要と各モードの入口をまとめます。 ## 概要 [revolution/laravel-voicevox](https://github.com/invokable/laravel-voicevox) は、Laravel から VOICEVOX を扱うためのパッケージです。 HTTP 経由で公式エンジンに接続するクライアントモードと、[VOICEVOX Core for PHP](/jp/packages/voicevox-core-php) を組み合わせて PHP FFI から直接合成するネイティブモードを同じ Laravel スタイルで扱えます。 FFI の前提知識を先に整理したいなら [PHP FFI](/jp/advanced/ffi) を、コアライブラリ自体のセットアップは [VOICEVOX Core for PHP](/jp/packages/voicevox-core-php) と [使い方](/jp/packages/voicevox-core-php/usage) を参照してください。 ## 機能表 | Feature | Supported | Description | | - | - | - | | VOICEVOX Client | ✅ | 公式 VOICEVOX エンジン API に HTTP でアクセスします。FFI は不要です。 | | VOICEVOX Core | ✅ | [VOICEVOX Core for PHP](/jp/packages/voicevox-core-php) を通して VOICEVOX CORE の動的ライブラリを利用します。 | | Laravel style | ✅ | Facade、ヘルパー、`config/voicevox.php`、`.env` で Laravel らしく扱えます。 | | Laravel AI SDK 統合 | ✅ | Laravel AI SDK の Audio ワークフローからも利用できます。 | | VOICEVOX Engine | ⚠️ | Laravel アプリ内に VOICEVOX 互換 API を内蔵します。未対応部分は公式エンジンへフォールバックします。 | | VOICEVOX Engine / OpenAI互換TTS API | ✅ | `/v1/audio/speech` voiceはAI SDKと同様にスタイルIDの他に「ずんだもん」などのエイリアスに対応しています。 | | VOICEVOX Editor | ⚠️ | macOS用のソング特化アプリを開発しましたがアプリストアで公開・配布する予定はありません。いつかGitHubでコードが公開されたらXcodeでビルドして使用できます。 | ## 要件 * PHP 8.3+ * Laravel 12+ * `ext-zip` * ネイティブモード / エンジン API を使う場合は `ext-ffi` * ローカル CLI 環境 Laravel Cloud を含む一般的な Web サーバー環境では FFI が無効なことが多いため、ネイティブモードとエンジン API は **ローカル CLI 前提** で考えてください。 ## モード一覧 | モード | 実行方法 | FFI | 主な用途 | ガイド | | - | - | - | - | - | | クライアント Talk | 公式エンジンへ HTTP リクエスト | 不要 | テキスト音声合成 | [client-talk](/jp/packages/laravel-voicevox/client-talk) | | クライアント Song | 公式エンジンへ HTTP リクエスト | 不要 | 歌声合成 | [client-song](/jp/packages/laravel-voicevox/client-song) | | ネイティブ Talk | VOICEVOX CORE を FFI で直接呼び出し | 必要 | ローカルでのテキスト音声合成 | [native-talk](/jp/packages/laravel-voicevox/native-talk) | | ネイティブ Song | VOICEVOX CORE を FFI で直接呼び出し | 必要 | ローカルでの歌声合成 | [native-song](/jp/packages/laravel-voicevox/native-song) | | エンジン API | Laravel アプリ内で互換 API を提供 | 必要 | VOICEVOX 互換 API の内蔵 | [installation](/jp/packages/laravel-voicevox/installation) | ## `tap()` が合う理由 このパッケージは `talk()` / `song()` の直後に Audio Query オブジェクトを返し、`generate()` の前に調整フェーズを差し込めます。 そのため `tap()` を使うと、**戻り値を変えずにクエリーだけを調整する** 書き方が自然です。 ```php theme={null} use Revolution\Voicevox\Client\TalkAudioQuery; use Revolution\Voicevox\Voicevox; $response = Voicevox::talk('タップで調整できるのだ', id: 1) ->tap(function (TalkAudioQuery $talk) { $talk->audioQuery['speedScale'] = 1.2; }) ->generate(id: 1); ``` * Talk では `speedScale` や `pitchScale` などを調整できます。 * Song では `sync()` で F0 とボリュームを再計算できます。 * `generate()` が返す `VoicevoxResponse` へきれいにチェーンできます。 `tap()` 自体の考え方は [tap() ヘルパーと Tappable トレイト](/jp/advanced/tap) で整理しています。このパッケージは実用例として特に分かりやすいケースです。 ## ドキュメント クライアントのみの導入、FFI を使う導入、設定ファイル公開、エンジンリソースの準備を確認します。 公式 VOICEVOX エンジンを使ったテキスト音声合成を始めます。 `Score` と `Note` を組み立てて歌声合成を行います。 PHP FFI と VOICEVOX CORE でローカル合成します。 FFI 経由で歌声モデルを直接使います。 コアライブラリのセットアップと Pure PHP 側の使い方を確認します。 ## 参考リンク * GitHub: [invokable/laravel-voicevox](https://github.com/invokable/laravel-voicevox) * VOICEVOX Core for PHP: [/jp/packages/voicevox-core-php](/jp/packages/voicevox-core-php) * VOICEVOX Core for PHP 使い方: [/jp/packages/voicevox-core-php/usage](/jp/packages/voicevox-core-php/usage) * PHP FFI: [/jp/advanced/ffi](/jp/advanced/ffi) * `tap()` ヘルパー: [/jp/advanced/tap](/jp/advanced/tap) * 公式 VOICEVOX エンジン: [VOICEVOX/voicevox\_engine](https://github.com/VOICEVOX/voicevox_engine) # インストールと設定 - VOICEVOX for Laravel Source: https://kawax.biz/jp/packages/laravel-voicevox/installation VOICEVOX for Laravel の導入方法、FFI の有無に応じたインストール、設定ファイル、.env、エンジンリソースの準備を説明します。 ## 何をインストールするか VOICEVOX for Laravel は、使いたいモードによって追加パッケージとローカルセットアップが変わります。 公式 VOICEVOX エンジンに HTTP で接続するだけなら `revolution/laravel-voicevox` のみで始められます。 ```shell theme={null} composer require revolution/laravel-voicevox ``` FFI を使うモードでは `revolution/voicevox-core` も一緒にインストールします。 ```shell theme={null} composer require revolution/laravel-voicevox revolution/voicevox-core ``` ## 要件 | 項目 | 内容 | | - | - | | PHP | 8.3+ | | Laravel | 12+ | | 必須拡張 | `ext-zip` | | FFI `ext-ffi` | ネイティブモード / エンジン API で必要 | Laravel Cloud を含む一般的な Web サーバーでは FFI が無効です。ネイティブモードとエンジン API は **ローカル CLI** で試してください。 ## セットアップ手順 使いたいモードに応じて Composer で導入します。ネイティブモードとエンジン API では [VOICEVOX Core for PHP](/jp/packages/voicevox-core-php) も必要です。 ```shell theme={null} php artisan vendor:publish --tag="voicevox-config" ``` クライアントモードだけなら `VOICEVOX_URL`、FFI を使うなら `VOICEVOX_CORE_PATH` を設定します。 `/speaker_info` や `/singer_info` を使う場合は追加リソースが必要です。 ## `config/voicevox.php` すべての設定項目は [設定リファレンス](/jp/packages/laravel-voicevox/configuration) で確認できます。 ```php theme={null} return [ 'client' => [ 'url' => env('VOICEVOX_URL', 'http://127.0.0.1:50021'), 'core_version' => env('VOICEVOX_CLIENT_CORE_VERSION'), ], 'core' => [ 'path' => env('VOICEVOX_CORE_PATH'), 'dict' => env('VOICEVOX_CORE_DICT_PATH', 'dict/open_jtalk_dic_utf_8-1.11'), 'models' => env('VOICEVOX_CORE_MODELS_PATH', 'models/vvms'), 'vvms' => ['0.vvm', '9.vvm', 's0.vvm'], ], 'engine' => [ 'disabled' => env('VOICEVOX_ENGINE_DISABLED', false), 'fallback_url' => env('VOICEVOX_ENGINE_FALLBACK_URL', 'http://127.0.0.1:50021'), ], ]; ``` ## 最低限必要な `.env` ### クライアントモードのみ デフォルトの `http://127.0.0.1:50021` で公式エンジンを起動するなら追加設定は不要です。 ```dotenv theme={null} VOICEVOX_URL=http://127.0.0.1:50021 ``` ### ネイティブモード / エンジン API ```dotenv theme={null} VOICEVOX_CORE_PATH=/path/to/voicevox_core/ ``` `VOICEVOX_CORE_PATH` の中身は [VOICEVOX Core for PHP](/jp/packages/voicevox-core-php) の手順で準備します。ライブラリ本体、OpenJTalk 辞書、モデルファイルの配置もそちらで確認してください。 ## FFI の有効化 CLI では通常そのまま使えます。 ローカルで Laravel 版エンジン API を起動する場合は `php.ini` で FFI を有効にします。 ```ini theme={null} ffi.enable=true ``` FFI の仕組みと制約は [PHP FFI](/jp/advanced/ffi) にまとめています。 ## エンジンリソースのインストール `/speaker_info` や `/singer_info` などのリソース系エンドポイントを使う場合は、別途リソースファイルを導入します。 ```shell theme={null} php artisan voicevox:install ``` Orchestra Testbench 環境では次のコマンドです。 ```shell theme={null} vendor/bin/testbench voicevox:install ``` ## モードごとの入口 公式 VOICEVOX エンジンを Docker で起動してテキスト音声合成します。 `Score` と `Note` を使って歌声合成します。 FFI で VOICEVOX CORE を直接呼び出します。 歌声モデルと `sync()` の使い所を確認します。 ## 参考リンク * [VOICEVOX for Laravel 概要](/jp/packages/laravel-voicevox) * [はじめに](/jp/packages/laravel-voicevox/getting-started) * [設定リファレンス](/jp/packages/laravel-voicevox/configuration) * [VOICEVOX Core for PHP](/jp/packages/voicevox-core-php) * [VOICEVOX Core for PHP 使い方](/jp/packages/voicevox-core-php/usage) * [PHP FFI](/jp/advanced/ffi) # ネイティブモード - プリセット - VOICEVOX for Laravel Source: https://kawax.biz/jp/packages/laravel-voicevox/native-presets VOICEVOX for Laravel のネイティブモードでプリセット機能を使い、音声合成パラメータを保存・再利用する方法を説明します。 ## プリセットとは プリセット機能を使うと、よく使う音声合成パラメータの組み合わせを保存して再利用できます。 話速・音高・抑揚などをまとめて管理すると、複数の音声を生成するときも一貫した品質を維持しやすくなります。 ## 概要 Laravel VOICEVOX のプリセット機能は、VOICEVOX Core を利用したネイティブ実装です。プリセットデータは JSON 形式で `storage/voicevox/presets.json` に永続化されます。 公式 VOICEVOX エンジンとは独立したストレージを使用するため、Laravel 側で作成したプリセットは公式エンジン側には反映されません(その逆も同様です)。 ## 設定 デフォルトでは `storage/voicevox/presets.json` に保存されます。別のパスを使いたい場合は `config/voicevox.php` で変更できます。 ```php theme={null} // config/voicevox.php return [ 'core' => [ 'presets' => storage_path('voicevox/presets.json'), ], ]; ``` ## プリセットの構造 プリセットは次のような構造です。 ```php theme={null} [ 'id' => 1, // プリセット ID(整数、自動採番) 'name' => 'ゆっくり', // プリセット名 'speaker_uuid' => 'uuid', // スピーカー UUID(文字列) 'style_id' => 1, // スタイル ID(整数) 'speedScale' => 0.8, // 話速(0.5 〜 2.0) 'pitchScale' => 0.0, // 音高(-0.15 〜 0.15) 'intonationScale' => 1.0, // 抑揚(0.0 〜 2.0) 'volumeScale' => 1.0, // 音量(0.0 〜 2.0) 'prePhonemeLength' => 0.1, // 開始無音(0.0 〜 1.5) 'postPhonemeLength' => 0.1, // 終了無音(0.0 〜 1.5) ] ``` ## 基本的な使い方 ### プリセットの作成 `preset()` ヘルパーの `add()` でプリセットを作成できます。 ```php theme={null} use function Revolution\Voicevox\preset; $id = preset()->add([ 'id' => 0, // 0 を指定すると自動採番される 'name' => 'ゆっくり丁寧', 'speaker_uuid' => '7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff', 'style_id' => 1, 'speedScale' => 0.8, 'pitchScale' => 0.0, 'intonationScale' => 1.2, 'volumeScale' => 1.0, 'prePhonemeLength' => 0.1, 'postPhonemeLength' => 0.1, ]); // 自動採番された ID が返される echo $id; // 例: 1 ``` ### 全プリセットの取得 ```php theme={null} use function Revolution\Voicevox\preset; $presets = preset()->all(); ``` ### プリセットの検索 ```php theme={null} use function Revolution\Voicevox\preset; $preset = preset()->find(1); if ($preset !== null) { echo $preset['name']; // "ゆっくり丁寧" } ``` ### プリセットの更新 ```php theme={null} use function Revolution\Voicevox\preset; preset()->update([ 'id' => 1, 'name' => 'ゆっくりはっきり', 'speaker_uuid' => '7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff', 'style_id' => 1, 'speedScale' => 0.7, 'pitchScale' => 0.0, 'intonationScale' => 1.5, 'volumeScale' => 1.0, 'prePhonemeLength' => 0.1, 'postPhonemeLength' => 0.1, ]); ``` ### プリセットの削除 ```php theme={null} use function Revolution\Voicevox\preset; preset()->delete(1); ``` ## プリセットを使った音声合成 `talk()` ヘルパーまたは Engine API の `/audio_query_from_preset` エンドポイントを利用します。 ### `talk()` ヘルパー `talk(preset:)` にプリセット ID を指定します。このとき通常のスタイル ID は無視されます。 ```php theme={null} use function Revolution\Voicevox\talk; $response = talk('プリセットを使うのだ', preset: 1)->generate(id: 1); ``` プリセット適用時も通常の音声合成フローは同じです。デフォルト `AudioQuery` の値だけがプリセットで置き換わります。 ### Engine API 経由でのアクセス Laravel VOICEVOX のエンジン API を起動している場合、HTTP 経由でプリセットを操作できます。 #### 全プリセットの取得 ```bash theme={null} curl http://localhost:50513/presets ``` #### プリセットの追加 ```bash theme={null} curl -X POST http://localhost:50513/add_preset \ -H "Content-Type: application/json" \ -d '{ "id": 0, "name": "ゆっくり丁寧", "speaker_uuid": "7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff", "style_id": 1, "speedScale": 0.8, "pitchScale": 0.0, "intonationScale": 1.2, "volumeScale": 1.0, "prePhonemeLength": 0.1, "postPhonemeLength": 0.1 }' ``` #### プリセットの更新 ```bash theme={null} curl -X POST http://localhost:50513/update_preset \ -H "Content-Type: application/json" \ -d '{ "id": 1, "name": "ゆっくりはっきり", "speaker_uuid": "7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff", "style_id": 1, "speedScale": 0.7, "pitchScale": 0.0, "intonationScale": 1.5, "volumeScale": 1.0, "prePhonemeLength": 0.1, "postPhonemeLength": 0.1 }' ``` #### プリセットの削除 ```bash theme={null} curl -X POST http://localhost:50513/delete_preset \ -H "Content-Type: application/json" \ -d '{"id": 1}' ``` #### プリセットを使った音声クエリの生成 ```bash theme={null} curl -X POST "http://localhost:50513/audio_query_from_preset?text=こんにちは&preset_id=1" ``` ## パラメータ詳細 * `speedScale`: 0.5 〜 2.0(話速) * `pitchScale`: -0.15 〜 0.15(音高) * `intonationScale`: 0.0 〜 2.0(抑揚) * `volumeScale`: 0.0 〜 2.0(音量) * `prePhonemeLength`: 0.0 〜 1.5 (開始無音・秒) * `postPhonemeLength`: 0.0 〜 1.5 (終了無音・秒) ## 注意事項 Laravel 版のプリセットと公式 VOICEVOX エンジンのプリセットは別管理です。追加・更新・削除は即座に JSON ファイルへ保存されます。 * プリセット ID に `0` を指定すると未使用 ID が自動採番されます。 * プリセットには `speaker_uuid`(UUID 文字列)と `style_id`(整数)の両方を含めてください。 ## トラブルシューティング ### プリセットが保存されない 1. ストレージディレクトリの書き込み権限を確認してください 2. `storage/voicevox/presets.json` が正しく作成されているか確認してください ### プリセットファイルの場所がわからない ```php theme={null} $path = config('voicevox.core.presets'); echo $path; ``` ### プリセットをリセットしたい ```bash theme={null} rm storage/voicevox/presets.json ``` ```php theme={null} $path = config('voicevox.core.presets'); if (file_exists($path)) { unlink($path); } ``` # ネイティブモード:ソング(歌声音声合成) - VOICEVOX for Laravel Source: https://kawax.biz/jp/packages/laravel-voicevox/native-song VOICEVOX for Laravel のネイティブモードで、VOICEVOX CORE を PHP FFI から直接呼び出して歌声音声合成する方法を説明します。 ## ネイティブモードで歌わせる ネイティブモードの歌声合成では、PHP FFI で VOICEVOX CORE を直接呼び出します。 `Score` と `Note` の組み立て方はクライアントモードと共通です。 ## 事前準備 1. [インストールと設定](/jp/packages/laravel-voicevox/installation) を完了していること 2. [VOICEVOX Core for PHP](/jp/packages/voicevox-core-php) をセットアップ済みであること 3. `.env` に `VOICEVOX_CORE_PATH` を設定していること ```dotenv theme={null} VOICEVOX_CORE_PATH=/path/to/voicevox_core/ ``` 歌声モデルを使うには `config/voicevox.php` の `core.vvms` に `s0.vvm` が含まれていることを確認してください。 ## `Score` と `Note` を作る ```php theme={null} use Revolution\Voicevox\Song\Note; use Revolution\Voicevox\Song\Score; $score = Score::make([ Note::make(length: 15), Note::make(length: Note::len(ticks: 480, bpm: 120), lyric: 'ド', key: 60), Note::make(length: Note::len(480, 120), lyric: 'レ', key: 62), Note::make(length: Note::len(960, 120), lyric: 'ミ', key: 64), Note::make(length: 2), ]); ``` ## 基本的な使い方 ```php theme={null} use Revolution\Voicevox\Song\Note; use Revolution\Voicevox\Song\Score; use function Revolution\Voicevox\song; $score = Score::make([ Note::make(length: 15), Note::make(length: Note::len(480, 120), lyric: 'ド', key: 60), Note::make(length: Note::len(480, 120), lyric: 'レ', key: 62), Note::make(length: Note::len(960, 120), lyric: 'ミ', key: 64), Note::make(length: 2), ]); $response = song($score, teacher: 6000) ->generate(id: 3001); $response->storeAs('native', 'song.wav'); ``` 同じ `Score` を使えば、クライアントモードとネイティブモードで共通の譜面を流用できます。 ## `tap()` で F0 とボリュームを再同期する ネイティブ Song でも `tap()` が役立ちます。`SongAudioQuery` を調整したあと、そのまま `generate()` に流せます。 ```php theme={null} use Revolution\Voicevox\Song\SongAudioQuery; use function Revolution\Voicevox\song; $response = song($score, teacher: 6000) ->tap(function (SongAudioQuery $song) { $song->sync(); }) ->generate(id: 3001); ``` F0 とボリュームを個別に更新する場合は、必ず次の順に実行します。 ```php theme={null} ->tap(function (SongAudioQuery $song) { $song->updateF0(); $song->updateVolume(); }) ``` ## クライアントモードとの違い | 項目 | クライアント Song | ネイティブ Song | | - | - | - | | エンジン起動 | 必要 | 不要 | | FFI | 不要 | 必要 | | API 入口 | `Voicevox::song()` | `song()` | | `Score` / `Note` | 共通 | 共通 | FFI の仕組みは [PHP FFI](/jp/advanced/ffi)、コアライブラリ単体の流れは [VOICEVOX Core for PHP 使い方](/jp/packages/voicevox-core-php/usage) を参照してください。 ## 次に読むページ Song 楽譜データの実用的な管理方法を確認します。 HTTP クライアント版との違いを比較します。 各モードと関連ページをまとめて確認します。 # ネイティブモード:トーク(テキスト音声合成) - VOICEVOX for Laravel Source: https://kawax.biz/jp/packages/laravel-voicevox/native-talk VOICEVOX for Laravel のネイティブモードで、VOICEVOX CORE を PHP FFI から直接呼び出してテキスト音声合成する方法を説明します。 ## ネイティブモードとは ネイティブモードは、[VOICEVOX Core for PHP](/jp/packages/voicevox-core-php) を通して VOICEVOX CORE を PHP FFI から直接呼び出します。 公式エンジンを起動しなくても合成できますが、FFI とローカルライブラリのセットアップが必要です。 ネイティブモードは Web サーバー向けではありません。ローカル CLI で利用してください。FFI の制約は [PHP FFI](/jp/advanced/ffi) でも説明しています。 ## 事前準備 1. [インストールと設定](/jp/packages/laravel-voicevox/installation) を完了していること 2. [VOICEVOX Core for PHP](/jp/packages/voicevox-core-php) のセットアップを完了していること 3. `.env` に `VOICEVOX_CORE_PATH` を設定していること ```dotenv theme={null} VOICEVOX_CORE_PATH=/path/to/voicevox_core/ ``` ## 基本的な使い方 ネイティブモードでは `talk()` ヘルパーを使います。 ```php theme={null} use function Revolution\Voicevox\talk; $response = talk('ネイティブ版なのだ', id: 1) ->generate(id: 1); $response->storeAs('native', 'talk.wav'); ``` ## スタイル ID の指定 `id` には読み込み済みの `.vvm` に含まれるスタイル ID を指定します。 ```php theme={null} use function Revolution\Voicevox\talk; $response = talk('ずんだもんなのだ', id: 1) ->generate(id: 1); ``` `config/voicevox.php` の `core.vvms` に含まれるモデルだけが使えます。 ## `tap()` で Audio Query を調整する ネイティブモードでも `tap()` の使い方は同じです。ただし `TalkAudioQuery` はクライアントモードとは別クラスです。 ```php theme={null} use Revolution\Voicevox\Talk\TalkAudioQuery; use function Revolution\Voicevox\talk; $response = talk('速めに話すのだ', id: 1) ->tap(function (TalkAudioQuery $talk) { $talk->audioQuery['speedScale'] = 1.2; $talk->audioQuery['pitchScale'] = 0.05; $talk->audioQuery['intonationScale'] = 1.5; $talk->audioQuery['volumeScale'] = 1.0; }) ->generate(id: 1); ``` `tap()` のパターン自体は [tap() ヘルパーと Tappable トレイト](/jp/advanced/tap) に整理しています。VOICEVOX for Laravel はその実践例です。 ## AquesTalk 風記法のカナを使う ネイティブモードには `enable_katakana_english` がないため、英語混じりの入力をそのまま読む用途には向きません。 代わりに AquesTalk 風記法のカナを直接渡す `kana()` を使えます。 ```php theme={null} use function Revolution\Voicevox\kana; $response = kana("ネイティブ'バンナ/ノダ'", id: 1) ->generate(id: 1); $response->storeAs('native', 'kana.wav'); ``` ## クライアントモードとの違い | 項目 | クライアント Talk | ネイティブ Talk | | - | - | - | | エンジン起動 | 必要 | 不要 | | FFI | 不要 | 必要 | | 英語の自動カタカナ変換 | あり | なし | | API 入口 | `Voicevox::talk()` | `talk()` | ## 次に読むページ 歌声モデルを FFI 経由で扱います。 純粋な PHP ラッパー側のセットアップと制約を確認します。 # ネイティブモード - ユーザー辞書 - VOICEVOX for Laravel Source: https://kawax.biz/jp/packages/laravel-voicevox/native-user-dict VOICEVOX for Laravel のネイティブモードでユーザー辞書を使い、固有名詞や専門用語の読み方を登録・更新・インポートする方法を説明します。 ## ユーザー辞書とは ユーザー辞書を使うと、固有名詞や専門用語の読み方を VOICEVOX に覚えさせられます。登録した単語は音声合成時に適切な発音で読み上げられます。 ## 概要 Laravel VOICEVOX のユーザー辞書機能は、VOICEVOX Core の UserDict を利用したネイティブ実装です。辞書データは JSON 形式で `storage/voicevox/user_dict.json` に永続化されます。 公式 VOICEVOX エンジンとは独立したストレージを使うため、Laravel 側で登録した単語は公式エンジン側には反映されません(逆方向も同様です)。 ## 設定 デフォルトでは `storage/voicevox/user_dict.json` に保存されます。別のパスを使いたい場合は `config/voicevox.php` で変更できます。 ```php theme={null} // config/voicevox.php return [ 'core' => [ 'user_dict' => storage_path('voicevox/user_dict.json'), ], ]; ``` ## 基本的な使い方 ### 単語の追加 `dict()` ヘルパーの `add()` で単語を登録します。 ```php theme={null} use function Revolution\Voicevox\dict; $uuid = dict()->add( surface: 'Laravel', pronunciation: 'ララベル', accentType: 0, ); // "-" なしの UUID が返る // 例: "550e8400e29b41d4a716446655440000" ``` #### パラメータ * `surface`(必須): 表記 * `pronunciation`(必須): 読み方(カタカナのみ) * `accentType` (必須): アクセント型(`0` = 平板、`1` 以上 = アクセント位置) * `wordType` (任意): 品詞(`COMMON_NOUN`、`PROPER_NOUN`、`VERB`、`ADJECTIVE`、`SUFFIX`) * `priority` (任意): 優先度(デフォルト `5`) #### アクセント型 * `0`: 平板アクセント * `1`: 一拍目 * `2`: 二拍目 * `3`: 三拍目 ### 単語の更新 ```php theme={null} use function Revolution\Voicevox\dict; dict()->update( wordUuid: '550e8400-e29b-41d4-a716-446655440000', surface: 'Laravel', pronunciation: 'ラレベル', accentType: 1, ); ``` ### 単語の削除 ```php theme={null} use function Revolution\Voicevox\dict; dict()->delete('550e8400-e29b-41d4-a716-446655440000'); ``` ### 全単語の取得 ```php theme={null} use function Revolution\Voicevox\dict; $words = dict()->all(); // または $words = dict()->toArray(); ``` ### 辞書のインポート ```php theme={null} use function Revolution\Voicevox\dict; $json = file_get_contents('other_user_dict.json'); dict()->import($json, override: false); // override: true なら上書き dict()->import($json, override: true); ``` ### 辞書のエクスポート ```php theme={null} use function Revolution\Voicevox\dict; $json = dict()->toJson(); file_put_contents('exported_dict.json', $json); ``` ## Engine API 経由でのアクセス Laravel VOICEVOX のエンジン API を起動している場合、HTTP 経由でもユーザー辞書を操作できます。公式エンジンへフォールバックせず、ネイティブ辞書を利用します。 ### 全単語の取得 ```bash theme={null} curl http://localhost:50513/user_dict ``` ### 単語の追加 ```bash theme={null} curl -X POST "http://localhost:50513/user_dict_word?surface=Laravel&pronunciation=ララベル&accent_type=0" ``` ### 単語の更新 ```bash theme={null} curl -X PUT "http://localhost:50513/user_dict_word/{uuid}?surface=Laravel&pronunciation=ラレベル&accent_type=1" ``` ### 単語の削除 ```bash theme={null} curl -X DELETE "http://localhost:50513/user_dict_word/{uuid}" ``` ### 辞書のインポート ```bash theme={null} curl -X POST "http://localhost:50513/import_user_dict?override=false" \ -H "Content-Type: application/json" \ -d @other_dict.json ``` `override=true` を指定すると、既存の辞書をすべて削除してからインポートします。 ## 辞書登録後の音声合成 ユーザー辞書に登録した単語は、音声合成時に自動参照されます。 ```php theme={null} use function Revolution\Voicevox\{dict, talk}; dict()->add( surface: 'Laravel', pronunciation: 'ララベル', accentType: 0, ); $response = talk('Laravelで開発するのは楽しいです', id: 1)->generate(id: 1); $response->storeAs('native', 'laravel.wav'); ``` ## 注意事項 `pronunciation` にはカタカナのみを指定してください。ひらがなや漢字は使用できません。 * Laravel 版のユーザー辞書と公式 VOICEVOX エンジンのユーザー辞書は別管理です。 * 単語の追加・更新・削除は即座に JSON ファイルへ保存されます。 * アクセント型が不明な場合はまず `0`(平板)を試してください。 ## トラブルシューティング ### 単語が反映されない 1. ストレージディレクトリの書き込み権限を確認してください 2. `storage/voicevox/user_dict.json` が正しく作成されているか確認してください 3. カタカナ表記が正しいか確認してください ### 辞書ファイルの場所がわからない ```php theme={null} $path = config('voicevox.core.user_dict'); echo $path; ``` ### 辞書をリセットしたい ```bash theme={null} rm storage/voicevox/user_dict.json ``` ```php theme={null} $path = config('voicevox.core.user_dict'); if (file_exists($path)) { unlink($path); } ``` # VoicevoxResponse - VOICEVOX for Laravel Source: https://kawax.biz/jp/packages/laravel-voicevox/response VOICEVOX for Laravel の音声合成結果を表す VoicevoxResponse クラスのメソッド解説。 ## 概要 `VoicevoxResponse` は、`talk()->generate()` や `song()->generate()` など、合成実行メソッドが返すレスポンスオブジェクトです。クライアントモードでもネイティブモードでも同じ`VoicevoxResponse`が使われます。 内部では WAV 形式のバイナリデータを保持し、保存・変換・文字列化のユーティリティを提供します。 ## `content()` `content()` は生の WAV バイナリを返します。HTTP レスポンスとしてそのまま返したいときに使います。 ```php theme={null} use Revolution\Voicevox\Voicevox; $response = Voicevox::talk('ずんだもんなのだ')->generate(); $wav = $response->content(); // WAVバイナリ return response($wav)->header('Content-Type', 'audio/wav'); ``` ## `storeAs()` `storeAs()` は Laravel の `Storage` に WAV を保存します。引数は `Storage::put()` と同じ考え方で扱えます。 ```php theme={null} use Revolution\Voicevox\Voicevox; $response = Voicevox::talk('ずんだもんなのだ')->generate(); // ファイル名のみ指定 $path = $response->storeAs('output.wav'); // パスとファイル名を分けて指定 $path = $response->storeAs('audio', 'output.wav'); // ディスクを指定 $path = $response->storeAs('audio', 'output.wav', 's3'); ``` 戻り値は保存したパス(`string`)です。保存に失敗すると `false` を返します。 ## `toBase64()` `toBase64()` は WAV バイナリを Base64 文字列へ変換します。JSON レスポンスや HTML 埋め込みに使えます。 ```php theme={null} $base64 = $response->toBase64(); return response()->json([ 'audio' => $base64, ]); ``` ```blade theme={null} ``` ## `__toString()` `VoicevoxResponse` を文字列としてキャストすると `content()` と同じ WAV バイナリが返ります。 ```php theme={null} $wav = (string) $response; ``` # Score と Note 詳細解説 - VOICEVOX for Laravel Source: https://kawax.biz/jp/packages/laravel-voicevox/song-score-note VOICEVOX for Laravel の Song 合成で使う Score と Note の設計、フレーム長計算、JSON 管理パターンを解説します。 ## 目的 Song 合成で迷いやすい `Score` と `Note` の仕様を整理します。 実装の根拠は `Revolution\Voicevox\Song\Note` / `Score` / `SongAudioQuery` のソースです。 ## `Note` クラス `Note` は 1 音符(または休符)を表します。 ```php theme={null} use Revolution\Voicevox\Song\Note; $note = Note::make( length: 94, lyric: 'ド', key: 60, id: 'intro-1', ); ``` ### コンストラクタ引数 | 引数 | 型 | 説明 | | - | - | - | | `length` | `int` | フレーム長。`秒数 × 93.75` を整数化した値 | | `lyric` | `string` | 歌詞。休符は空文字 `''` | | `key` | `int\|null` | MIDI ノート番号。休符は `null` | | `id` | `string\|null` | `FrameAudioQuery` 生成時に `note_id` へコピーされる補助 ID | ### フレーム長の計算 `length` はフレーム単位です。秒数からは次で求めます。 ```text theme={null} length = 秒数 × 93.75 ``` この値は直感的ではないため、MIDI に慣れている場合は `Note::len()` を使うのが実用的です。 ```php theme={null} Note::len(int $ticks, int $bpm = 125): int ``` * 4 分音符を `480 ticks` として扱う前提 * `125 BPM` がデフォルト * 内部で `round()` して整数化するため、長い曲では丸め誤差が徐々に蓄積します ```php theme={null} Note::make(length: Note::len(480, 120), lyric: 'ド', key: 60); // 4分音符 Note::make(length: Note::len(960, 120), lyric: 'ミ', key: 64); // 2分音符 ``` `Note::len()` は便利ですが、長尺の楽曲では累積誤差に注意してください。必要ならセクション単位で長さを再調整します。 ### 休符の表現 休符は次の組み合わせです。 ```php theme={null} Note::make(length: 15, lyric: '', key: null); ``` ### MIDI ノート番号の目安 | Note | MIDI | | - | - | | C4 | 60 | | D4 | 62 | | E4 | 64 | | F4 | 65 | | G4 | 67 | | A4 | 69 | | B4 | 71 | | C5 | 72 | ## `Score` クラス `Score` は `Note` の配列を持つ楽譜オブジェクトです。 ```php theme={null} use Revolution\Voicevox\Song\Score; $score = Score::make([ Note::make(length: 15, lyric: '', key: null), Note::make(length: Note::len(480, 120), lyric: 'ド', key: 60), ]); ``` ### 重要ルール 最初のノートは必ず休符にしてください。 ### 主な API * `Score::make(array $notes)` で生成 * `add(Note $note)` で追記 * `toJson()` で JSON 文字列化 ```php theme={null} $score = Score::make([ Note::make(length: 15, lyric: '', key: null), ]); $score->add(Note::make(length: Note::len(480, 120), lyric: 'ド', key: 60)); $json = $score->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); ``` `Score` コンストラクタは配列入力時に `lyric` の空文字/`null` を吸収する実装があり、`ConvertEmptyStringsToNull` ミドルウェア経由のフォーム入力でも扱いやすくなっています。 ## 実用パターン: JSON ファイル管理(推奨) 実際のプロダクトでは、PHP に楽譜を直書きせず JSON で管理するほうが運用しやすいです。 1. 独自フォーマット JSON を保存 2. 読み込んで `Note` / `Score` に変換 3. `Voicevox::song()` へ渡して生成 ### JSON 例 ```json theme={null} { "teacher": 6000, "speaker": 3001, "bpm": 120, "notes": [ { "ticks": 120, "lyric": "", "key": null, "id": "r0" }, { "ticks": 480, "lyric": "ド", "key": 60, "id": "n1" }, { "ticks": 480, "lyric": "レ", "key": 62, "id": "n2" }, { "ticks": 960, "lyric": "ミ", "key": 64, "id": "n3" }, { "ticks": 120, "lyric": "", "key": null, "id": "r1" } ] } ``` ### 読み込みと合成 ```php theme={null} use Illuminate\Support\Facades\Storage; use Revolution\Voicevox\Song\Note; use Revolution\Voicevox\Song\Score; use Revolution\Voicevox\Voicevox; $chart = json_decode( Storage::disk('local')->get('songs/sample.json'), true, flags: JSON_THROW_ON_ERROR, ); $score = Score::make( collect($chart['notes']) ->map(fn (array $note) => Note::make( length: Note::len(ticks: $note['ticks'], bpm: $chart['bpm']), lyric: $note['lyric'] ?? '', key: $note['key'], id: $note['id'] ?? null, )) ->all(), ); $response = Voicevox::song($score, teacher: $chart['teacher']) ->generate(id: $chart['speaker']); $response->storeAs('songs', 'sample.wav'); ``` ## 関連ページ * [クライアント: ソング](/jp/packages/laravel-voicevox/client-song) * [ネイティブ: ソング](/jp/packages/laravel-voicevox/native-song) * [エンジン API: ソング](/jp/packages/laravel-voicevox/engine-song) # .vvproj ファイル仕様 - VOICEVOX for Laravel Source: https://kawax.biz/jp/packages/laravel-voicevox/vvproj VOICEVOXエディターの .vvproj JSON を読み込み、Laravel でトーク・ソングを生成するための実践仕様を整理します。 ## 目的 VOICEVOXエディターの `.vvproj` をそのまま活用して、Laravel でトーク・ソングを再生成できるようにします。 ## トップレベル構造 `.vvproj` は UTF-8 JSON です。トークとソングを同じファイルに保存します。 ```json theme={null} { "appVersion": "0.25.2", "talk": { "audioKeys": [], "audioItems": {} }, "song": { "tpqn": 480, "tempos": [], "timeSignatures": [], "tracks": {}, "trackOrder": [] } } ``` | キー | 内容 | | - | - | | `appVersion` | 保存時のエディターバージョン | | `talk` | トーク用データ (`audioKeys` + `audioItems`) | | `song` | ソング用データ (テンポ・拍子・トラック群) | ## `talk` セクション `talk.audioKeys` は順序配列、`talk.audioItems` は ID をキーにした Record です。 ```json theme={null} { "audioKeys": ["audio-item-uuid"], "audioItems": { "audio-item-uuid": { "text": "ずんだもんなのだ", "voice": { "engineId": "engine-uuid", "speakerId": "speaker-uuid", "styleId": 3 }, "query": { "accentPhrases": [], "speedScale": 1, "pitchScale": 0 }, "presetKey": "preset-uuid" } } } ``` ### TalkAudioItem | キー | 内容 | | - | - | | `text` | 入力テキスト | | `voice.engineId` | エンジン ID (`/engine_manifest` と対応) | | `voice.speakerId` | 話者 UUID | | `voice.styleId` | スタイル ID。`/synthesis?speaker={styleId}` にそのまま渡す | | `query` | `AudioQuery` 相当 | | `presetKey` | エディターのプリセット ID。Laravel 側プリセットは [プリセット](/jp/packages/laravel-voicevox/native-presets) を参照 | `.vvproj` に `query` が保存済みなら、`/audio_query` を再実行せずにそのまま合成へ進めます。 ### `accentPhrases` | キー | 内容 | | - | - | | `moras` | モーラ配列 (`consonant` 系は省略される場合あり) | | `accent` | アクセント位置 (1始まり) | | `pauseMora` | 句読点などで無音を入れるときに付くことがある | | `isInterrogative` | 疑問文フラグ | ## `song` セクション ```json theme={null} { "tpqn": 480, "tempos": [{ "position": 0, "bpm": 120 }], "timeSignatures": [{ "measureNumber": 1, "beats": 4, "beatType": 4 }], "tracks": { "track-uuid": { "name": "無名トラック", "singer": { "engineId": "engine-uuid", "styleId": 3003 }, "notes": [] } }, "trackOrder": ["track-uuid"] } ``` | キー | 内容 | | - | - | | `tpqn` | Ticks Per Quarter Note。標準は 480 | | `tempos` | テンポマップ (`position` は tick) | | `timeSignatures` | 拍子マップ (`measureNumber` は 1 始まり) | | `tracks` | Track ID をキーにした Record | | `trackOrder` | 表示・再生順。`tracks` のキー集合と一致させる | ### Track | キー | 内容 | | - | - | | `singer.styleId` | 最終的に `/frame_synthesis?speaker={styleId}` へ渡す ID | | `notes` | ノート配列 | | `keyRangeAdjustment` | 半音単位のキー調整 | | `volumeRangeAdjustment` | 音量調整 | | `pitchEditData` / `volumeEditData` | フレーム単位の編集データ | | `phonemeTimingEditData` | Note ID ごとの音素タイミング編集 | | `solo` / `mute` | 再生対象トラック判定に使う | ### Note | キー | 内容 | | - | - | | `id` | ノート ID (一意) | | `position` | ノート開始 tick | | `duration` | ノート長 tick | | `noteNumber` | MIDI ノート番号 | | `lyric` | 歌詞 | ## tick・秒・フレーム変換 単一テンポでは次で変換します。 ```text theme={null} seconds = ticks / tpqn * 60 / bpm frames = round(seconds * frameRate) ``` テンポ変更ありでは、`tempos` を `position` 昇順で区間積算します。 ```php theme={null} function ticksToSeconds(int $targetTick, int $tpqn, array $tempos): float { $seconds = 0.0; $currentTick = 0; foreach ($tempos as $index => $tempo) { $nextTick = $tempos[$index + 1]['position'] ?? $targetTick; $segmentEnd = min($targetTick, $nextTick); if ($segmentEnd <= $currentTick) { break; } $bpm = $tempo['bpm']; $seconds += (($segmentEnd - $currentTick) / $tpqn) * (60 / $bpm); $currentTick = $segmentEnd; } return $seconds; } ``` `Note::len()` ヘルパーで tick からフレーム長へ変換する実装は [Score と Note 詳細](/jp/packages/laravel-voicevox/song-score-note) を参照してください。 ## ソング音声生成の流れ ```mermaid theme={null} flowchart TD A["1. trackOrder順でtracksを取り出す"] --> B["2. solo/muteで生成対象を決定"] B --> C["3. notesをposition昇順に並べる"] C --> D["4. 先頭と末尾に休符を追加してScore化"] D --> E["5. tick→秒→frameでframe_lengthを作る"] E --> F["6. sing_frame_audio_query→sing_frame_f0→sing_frame_volume"] F --> G["7. frame_synthesisでWAVを生成"] ``` ## 直接編集時の注意点 * `tracks` のキー集合と `trackOrder` を必ず一致させる * `talk.audioKeys` と `talk.audioItems` も同様に同期する * `position >= 0`、`duration >= 1`、`noteNumber` は `0..127` を維持する * `tempos[0].position` は通常 `0`、`timeSignatures[0].measureNumber` は通常 `1` * 未知キーは可能なら保持して再保存し、将来バージョンとの互換性を壊さない ## Laravel コード例 `.vvproj` を読み込み、トークとソングを生成する最小例です。 ```php theme={null} use Illuminate\Support\Facades\Storage; use Revolution\Voicevox\Client\TalkAudioQuery; use Revolution\Voicevox\Song\Note; use Revolution\Voicevox\Song\Score; use Revolution\Voicevox\Voicevox; $project = json_decode( Storage::disk('local')->get('voicevox/sample.vvproj'), true, flags: JSON_THROW_ON_ERROR, ); // Talk: 保存済み query をそのまま合成 foreach ($project['talk']['audioKeys'] as $audioKey) { $item = $project['talk']['audioItems'][$audioKey]; Voicevox::talk($item['text'], id: $item['voice']['styleId']) ->tap(fn (TalkAudioQuery $talk) => $talk->audioQuery = array_replace($talk->audioQuery, $item['query'])) ->generate(id: $item['voice']['styleId']) ->storeAs('vvproj/talk', "{$audioKey}.wav"); } // Song: ここでは先頭トラックの duration を frame_length に変換して生成 $trackId = $project['song']['trackOrder'][0]; $track = $project['song']['tracks'][$trackId]; $bpm = $project['song']['tempos'][0]['bpm'] ?? 120; $score = Score::make([ Note::make(length: 15, lyric: '', key: null), ...collect($track['notes'])->map( fn (array $note) => Note::make( length: Note::len(ticks: $note['duration'], bpm: $bpm), lyric: $note['lyric'] ?? 'ら', key: $note['noteNumber'], id: $note['id'] ?? null, ), )->all(), Note::make(length: 2, lyric: '', key: null), ]); Voicevox::song($score, teacher: 6000) ->generate(id: $track['singer']['styleId']) ->storeAs('vvproj/song', "{$trackId}.wav"); ``` # Socialite for Discord Source: https://kawax.biz/jp/packages/socialite-discord Discord OAuth2 認証を Laravel Socialite で実装するドライバーパッケージ。 ## 概要 [revolution/socialite-discord](https://github.com/invokable/socialite-discord) は、Discord OAuth2 認証を Laravel Socialite で利用できるようにするドライバーパッケージです。 Laravel Socialite の組み込みプロバイダーには Discord が含まれていません。このパッケージをインストールするだけで、`Socialite::driver('discord')` を使った Discord ログインをアプリに追加できます。 Laravel Socialite 自体の使い方については [Socialite ガイド](/jp/socialite) を参照してください。 ## インストール ```shell theme={null} composer require revolution/socialite-discord ``` ## 設定 ### Discord Developer Portal でアプリを登録する [Discord Developer Portal](https://discord.com/developers/applications) でアプリケーションを作成し、**OAuth2** セクションから Client ID と Client Secret を取得します。**Redirects** にコールバック URL(例: `https://example.com/auth/discord/callback`)を追加してください。 ### config/services.php ```php theme={null} 'discord' => [ 'client_id' => env('DISCORD_CLIENT_ID'), 'client_secret' => env('DISCORD_CLIENT_SECRET'), 'redirect' => env('DISCORD_REDIRECT'), ], ``` ### .env ```dotenv theme={null} DISCORD_CLIENT_ID=your-client-id DISCORD_CLIENT_SECRET=your-client-secret DISCORD_REDIRECT=https://example.com/auth/discord/callback ``` ## 基本的な使い方 `routes/web.php` にリダイレクト用とコールバック用の2つのルートを追加します。 ```php theme={null} use App\Http\Controllers\SocialiteController; Route::get('auth/discord', [SocialiteController::class, 'login']); Route::get('auth/discord/callback', [SocialiteController::class, 'callback']); ``` ```php theme={null} redirect(); } public function callback() { $discordUser = Socialite::driver('discord')->user(); // $discordUser->getId() Discord ユーザー ID // $discordUser->getName() ユーザー名 // $discordUser->getEmail() メールアドレス(email スコープが必要) // $discordUser->getAvatar() アバター URL // $discordUser->token アクセストークン $user = \App\Models\User::updateOrCreate( ['discord_id' => $discordUser->getId()], ['name' => $discordUser->getName()] ); \Illuminate\Support\Facades\Auth::login($user); return redirect('/dashboard'); } } ``` ## スコープの設定 Discord OAuth2 では取得する権限をスコープで指定します。デフォルトのスコープは `identify` のみです。 ```php theme={null} public function login() { return Socialite::driver('discord') ->setScopes(['identify', 'email', 'guilds', 'guilds.join']) ->redirect(); } ``` 主なスコープの一覧: | スコープ | 取得できる情報 | | - | - | | `identify` | ユーザー ID、ユーザー名、アバター(デフォルト) | | `email` | メールアドレス | | `guilds` | 参加しているサーバー一覧 | | `guilds.join` | ユーザーをサーバーに追加する権限 | すべてのスコープは [Discord OAuth2 ドキュメント](https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes) を参照してください。 ## ユーザーの保存とログイン コールバックルートでユーザー情報を取得し、データベースに保存してからログインする典型的な実装例です。 ```php theme={null} use App\Models\User; use Illuminate\Support\Facades\Auth; use Laravel\Socialite\Facades\Socialite; public function callback() { $discordUser = Socialite::driver('discord')->user(); $user = User::updateOrCreate( ['discord_id' => $discordUser->getId()], [ 'name' => $discordUser->getName(), 'email' => $discordUser->getEmail(), 'discord_token' => $discordUser->token, ] ); Auth::login($user); return redirect('/dashboard'); } ``` `email` を取得するには `email` スコープが必要です。スコープを指定しない場合、`getEmail()` は `null` を返すことがあります。 ## 他の Socialite ドライバーとの比較 このパッケージは `Socialite::extend()` を使った正規の方法で実装されており、組み込みプロバイダーとまったく同じ API で使えます。カスタムプロバイダーの仕組みや `Socialite::extend()` の詳細については [Socialite ガイド](/jp/socialite) を参照してください。 最新情報は [GitHub リポジトリ](https://github.com/invokable/socialite-discord) を参照してください。 # API リファレンス - VOICEVOX Core for PHP Source: https://kawax.biz/jp/packages/voicevox-core-php/api VOICEVOX Core for PHP の全クラス・メソッド・enum のリファレンス。 ## `Onnxruntime` ONNX Runtime ローダー。プロセスレベルのシングルトンで、1 プロセスにつき 1 インスタンスのみ存在します。 ### メソッド | メソッド | 説明 | | - | - | | `static loadOnce(string $filename = ''): self` | ONNX Runtime を読み込んで初期化します。2 回目以降の呼び出しでは引数を無視して既存のインスタンスを返します。 | | `static get(): ?self` | 既存のインスタンスを返します。まだ初期化されていない場合は `null` を返します。 | | `supportedDevices(): string` | 利用可能なデバイス情報を JSON 文字列で返します。 | | `static libVersionedFilename(): string` | ONNX Runtime ライブラリのバージョン付きファイル名を返します(例: `libvoicevox_onnxruntime.1.17.3.dylib`)。 | | `static libUnversionedFilename(): string` | ONNX Runtime ライブラリのバージョンなしファイル名を返します。 | ### 定数 | 定数 | 説明 | | - | - | | `LIB_NAME` | ライブラリベース名(`voicevox_onnxruntime`) | | `LIB_VERSION` | 推奨 ONNX Runtime バージョン | *** ## `OpenJtalk` OpenJTalk を使ったテキスト解析器。 ### メソッド | メソッド | 説明 | | - | - | | `__construct(string $openJtalkDictDir)` | OpenJTalk 辞書ディレクトリのパスで初期化します。 | | `analyze(string $text): string` | 日本語テキストを解析し、アクセント句配列の JSON を返します。 | | `useUserDict(UserDict $userDict): void` | ユーザー辞書を適用します。辞書が変更された場合は再度呼び出す必要があります。 | *** ## `VoiceModelFile` 音声モデルファイル(`.vvm` ファイル)。 ### メソッド | メソッド | 説明 | | - | - | | `static open(string $path): self` | `.vvm` ファイルを開きます。 | | `id(): string` | 音声モデル ID を hex 文字列(16 バイト)で返します。 | | `createMetasJson(): string` | スピーカーメタデータを JSON 文字列で返します。 | | `close(): void` | ファイルを閉じてリソースを解放します。 | *** ## `Synthesizer` テキスト読み上げのメインクラス。 ### メソッド | メソッド | 説明 | | - | - | | `__construct(Onnxruntime $onnxruntime, OpenJtalk $openJtalk, AccelerationMode $accelerationMode = Auto, int $cpuNumThreads = 0)` | シンセサイザーを初期化します。 | | `onnxruntime(): Onnxruntime` | このシンセサイザーが保持する `Onnxruntime` インスタンスを返します。 | | `isGpuMode(): bool` | GPU モードが有効かどうかを返します。 | | `metas(): string` | 読み込まれているスピーカーのメタデータを JSON 文字列で返します。 | | `loadVoiceModel(VoiceModelFile $model): void` | 音声モデルを読み込みます。 | | `unloadVoiceModel(string $voiceModelId): void` | hex ID で指定した音声モデルをアンロードします。 | | `isLoadedVoiceModel(string $voiceModelId): bool` | 音声モデルが読み込まれているかどうかを確認します。 | | `createAudioQuery(string $text, int $styleId): string` | 日本語テキストから AudioQuery JSON を生成します。 | | `createAudioQueryFromKana(string $kana, int $styleId): string` | AquesTalk スタイルのかな記法から AudioQuery JSON を生成します。 | | `createAccentPhrases(string $text, int $styleId): string` | 日本語テキストからアクセント句配列 JSON を生成します。 | | `createAccentPhrasesFromKana(string $kana, int $styleId): string` | かな記法からアクセント句配列 JSON を生成します。 | | `replaceMoraData(string $accentPhrasesJson, int $styleId): string` | モーラのピッチと音素長を更新した新しいアクセント句を返します。 | | `replacePhonemeLength(string $accentPhrasesJson, int $styleId): string` | 音素長を更新した新しいアクセント句を返します。 | | `replaceMoraPitch(string $accentPhrasesJson, int $styleId): string` | モーラのピッチを更新した新しいアクセント句を返します。 | | `synthesis(string $audioQueryJson, int $styleId, bool $enableInterrogativeUpspeak = true): string` | AudioQuery JSON から音声を合成します。WAV バイナリを返します。 | | `tts(string $text, int $styleId, bool $enableInterrogativeUpspeak = true): string` | 日本語テキストから 1 ステップで音声を合成します。WAV バイナリを返します。 | | `ttsFromKana(string $kana, int $styleId, bool $enableInterrogativeUpspeak = true): string` | かな記法から音声を合成します。WAV バイナリを返します。 | | `createSingFrameAudioQuery(string $scoreJson, int $styleId): string` | 楽譜 JSON から歌唱合成クエリ JSON を生成します。 | | `frameSynthesis(string $frameAudioQueryJson, int $styleId): string` | フレームオーディオクエリから歌唱音声を合成します。WAV バイナリを返します。 | | `createSingFrameF0(string $scoreJson, string $frameAudioQueryJson, int $styleId): string` | 楽譜と歌唱クエリからフレームごとの F0(基本周波数)を JSON の float 配列で返します。 | | `createSingFrameVolume(string $scoreJson, string $frameAudioQueryJson, int $styleId): string` | 楽譜と歌唱クエリからフレームごとの音量を JSON の float 配列で返します。 | *** ## `VoicevoxCore` VOICEVOX Core のグローバルユーティリティ関数。 ### メソッド | メソッド | 説明 | | - | - | | `static getVersion(): string` | VOICEVOX Core のバージョンを SemVer 文字列で返します。 | | `static audioQueryCreateFromAccentPhrases(string $accentPhrasesJson): string` | アクセント句配列 JSON から AudioQuery JSON を生成します。 | | `static audioQueryValidate(string $audioQueryJson): void` | `AudioQuery` JSON を検証します。不正な場合は `VoicevoxException` をスローします。 | | `static accentPhraseValidate(string $accentPhraseJson): void` | `AccentPhrase` JSON を検証します。不正な場合は `VoicevoxException` をスローします。 | | `static moraValidate(string $moraJson): void` | `Mora` JSON を検証します。不正な場合は `VoicevoxException` をスローします。 | | `static scoreValidate(string $scoreJson): void` | `Score` JSON を検証します。不正な場合は `VoicevoxException` をスローします。 | | `static noteValidate(string $noteJson): void` | `Note` JSON を検証します。不正な場合は `VoicevoxException` をスローします。 | | `static frameAudioQueryValidate(string $frameAudioQueryJson): void` | `FrameAudioQuery` JSON を検証します。不正な場合は `VoicevoxException` をスローします。 | | `static framePhonemeValidate(string $framePhonemeJson): void` | `FramePhoneme` JSON を検証します。不正な場合は `VoicevoxException` をスローします。 | | `static ensureCompatible(string $scoreJson, string $frameAudioQueryJson): void` | 楽譜と歌唱音声合成クエリの組み合わせが有効かを確認します。不正な場合は `VoicevoxException` をスローします。 | *** ## `UserDict` カスタム単語の発音を登録するユーザー辞書。 ### メソッド | メソッド | 説明 | | - | - | | `__construct()` | 空のユーザー辞書を作成します。 | | `load(string $path): void` | ファイルからユーザー辞書を読み込みます。 | | `save(string $path): void` | ユーザー辞書をファイルに保存します。 | | `addWord(string $surface, string $pronunciation, int $accentType, UserDictWordType $wordType = CommonNoun, int $priority = 5): string` | 単語を追加します。単語 UUID を hex 文字列で返します。 | | `updateWord(string $wordUuid, string $surface, string $pronunciation, int $accentType, UserDictWordType $wordType = CommonNoun, int $priority = 5): void` | UUID で指定した既存の単語を更新します。 | | `removeWord(string $wordUuid): void` | UUID で指定した単語を削除します。 | | `importDict(UserDict $other): void` | 別の `UserDict` から単語をインポートします。 | | `toJson(): string` | すべての単語を JSON 文字列で返します。 | *** ## Enum: `AccelerationMode` シンセサイザーのハードウェアアクセラレーションモード。 | Case | 値 | 説明 | | - | - | - | | `Auto` | `0` | 利用可能な最適モードを自動選択します。 | | `Cpu` | `1` | CPU モードを強制します。 | | `Gpu` | `2` | GPU モードを強制します。 | *** ## Enum: `UserDictWordType` ユーザー辞書エントリの品詞。 | Case | 値 | 説明 | | - | - | - | | `ProperNoun` | `0` | 固有名詞 | | `CommonNoun` | `1` | 一般名詞 | | `Verb` | `2` | 動詞 | | `Adjective` | `3` | 形容詞 | | `Suffix` | `4` | 接尾辞 | *** ## `VoicevoxException` VOICEVOX Core の C API 呼び出しがエラーコードを返した場合にスローされます。例外メッセージにはライブラリからのエラー説明が含まれます。 # VOICEVOX Core for PHP Source: https://kawax.biz/jp/packages/voicevox-core-php/index PHP FFI を使って VOICEVOX CORE の C 動的ライブラリをラップした Pure PHP パッケージ。テキスト読み上げ(TTS)を PHP から直接利用できます。 ## 概要 [revolution/voicevox-core](https://github.com/invokable/voicevox-core-php) は、[VOICEVOX CORE](https://github.com/VOICEVOX/voicevox_core) の C 動的ライブラリを PHP FFI でラップした Pure PHP パッケージです。 VOICEVOX は高品質な日本語テキスト読み上げ(TTS)エンジンで、このパッケージを使うことで PHP から直接音声合成を行えます。 PHP FFI 自体の仕組みと制約を先に整理したいなら、[PHP FFI](/jp/advanced/ffi) を参照してください。 Laravel アプリから扱うなら [VOICEVOX for Laravel](/jp/packages/laravel-voicevox) も合わせて読むと、クライアントモードとネイティブモードの違いまで整理できます。 PHP FFI は Web サーバー環境(FPM など)では無効にされていることが多いため(`ffi.enable=false`)、このパッケージは **ローカル CLI での使用を前提**としています。 ## 要件 * PHP 8.3+ * `ext-ffi` 拡張が有効になっていること * VOICEVOX CORE 0.16+ ## インストール ```bash theme={null} composer require revolution/voicevox-core ``` ## ライブラリのセットアップ このパッケージを使用するには、VOICEVOX CORE の動的ライブラリ(`.so` / `.dylib`)、ONNX Runtime ライブラリ、OpenJTalk 辞書が必要です。 [voicevox\_core releases](https://github.com/VOICEVOX/voicevox_core/releases) から OS・アーキテクチャに合ったダウンローダーをダウンロードして実行します。実行すると、カレントディレクトリに `voicevox_core` ディレクトリが作成されます。 作成されるディレクトリ構造: ``` voicevox_core/ ├── dict/open_jtalk_dic_*/ # OpenJTalk 辞書 ├── c_api/lib/ # 動的ライブラリ (.so, .dylib) ├── models/vvms/ # 音声モデルファイル (.vvm) └── onnxruntime/ # ONNX Runtime ライブラリ ``` ```bash theme={null} mv voicevox_core ~/.local/voicevox_core ``` ライブラリが自動的に見つかるようにシンボリックリンクを作成します。 `ln -s` には必ず**絶対パス**を使用してください。 **macOS:** ```bash theme={null} # [VOICEVOX_CORE_DIR] を voicevox_core の絶対パスに置き換えてください ln -s [VOICEVOX_CORE_DIR]/libvoicevox_core.dylib /usr/local/lib/libvoicevox_core.dylib ``` `/usr/local/lib/` からロードできない場合は `.zshrc` などで `DYLD_FALLBACK_LIBRARY_PATH` を設定します: ```bash theme={null} export DYLD_FALLBACK_LIBRARY_PATH="$HOME/lib:/usr/local/lib:/usr/lib" ``` **Linux:** ```bash theme={null} ln -s [VOICEVOX_CORE_DIR]/libvoicevox_core.so /usr/local/lib/libvoicevox_core.so ``` ### 代替手段: 環境変数で指定する シンボリックリンクが作成できない場合は、`VOICEVOX_CORE_LIB_PATH` 環境変数にライブラリファイルのフルパスを設定します: ```bash theme={null} export VOICEVOX_CORE_LIB_PATH=/path/to/libvoicevox_core.dylib ``` ```bash theme={null} export VOICEVOX_CORE_LIB_PATH="$HOME/.local/voicevox_core/c_api/lib/libvoicevox_core.dylib" ``` macOSで `DYLD_FALLBACK_LIBRARY_PATH` を使ってもロードできない場合は `VOICEVOX_CORE_LIB_PATH` で設定してください。 ## ドキュメント テキスト読み上げ・音声合成の基本的な使い方とコードサンプル。 全クラス・メソッド・enum の詳細リファレンス。 ## 参考リンク * GitHub: [invokable/voicevox-core-php](https://github.com/invokable/voicevox-core-php) * VOICEVOX CORE: [VOICEVOX/voicevox\_core](https://github.com/VOICEVOX/voicevox_core) # 使い方 - VOICEVOX Core for PHP Source: https://kawax.biz/jp/packages/voicevox-core-php/usage VOICEVOX Core for PHP の基本的な使い方。テキスト読み上げ(TTS)と音声合成のコードサンプルを紹介します。 ## 基本的な音声合成 以下の `talk.php` は、テキストから音声を合成して WAV ファイルに書き出すサンプルです。 PHP FFI は Web サーバー環境(FPM など)では通常無効です。このスクリプトは `php talk.php` のように **CLI から実行**してください。 ```php theme={null} loadVoiceModel($model); // 音声合成 $audioQuery = $synthesizer->createAudioQuery($text, $styleId); $wav = $synthesizer->synthesis($audioQuery, $styleId); file_put_contents($outPath, $wav); echo 'Wrote ' . $outPath . PHP_EOL; ``` 実行: ```bash theme={null} php talk.php ``` ## 1 ステップで音声合成(`tts` メソッド) `createAudioQuery` + `synthesis` の 2 ステップを省略したい場合は `tts()` メソッドが使えます: ```php theme={null} $wav = $synthesizer->tts($text, $styleId); file_put_contents('./output.wav', $wav); ``` ## カタカナ記法で音声合成 AquesTalk風のカタカナ記法からも合成できます: ```php theme={null} // ttsFromKana を使う場合 $wav = $synthesizer->ttsFromKana("コノオンセイワ'、ボイスボックスオ'/シヨーシテ'、シュツリョクサレテイマ'ス。", $styleId); file_put_contents('./output.wav', $wav); ``` ## ユーザー辞書を使う カスタム単語の読みを登録するには `UserDict` を使います: ```php theme={null} use Revolution\Voicevox\Core\UserDict; use Revolution\Voicevox\Core\Enums\UserDictWordType; $userDict = new UserDict(); $uuid = $userDict->addWord( surface: 'ボイボ', pronunciation: 'ボイボ', accentType: 0, wordType: UserDictWordType::ProperNoun, priority: 10, ); // 辞書を保存 $userDict->save('./mydict.json'); // OpenJTalk に辞書を適用 $openJtalk->useUserDict($userDict); ``` ## AudioQuery の調整 `createAudioQuery` で取得した JSON を調整してから合成することで、抑揚やテンポなどを細かく制御できます: ```php theme={null} $audioQueryJson = $synthesizer->createAudioQuery($text, $styleId); // JSON をデコードして調整 $audioQuery = json_decode($audioQueryJson, true); $audioQuery['speedScale'] = 1.2; // 速度を 1.2 倍に $audioQuery['pitchScale'] = 0.05; // ピッチを少し上げる $wav = $synthesizer->synthesis(json_encode($audioQuery), $styleId); file_put_contents('./output.wav', $wav); ``` ## アクセント句の取得 テキストのアクセント句情報を取得して確認・編集できます: ```php theme={null} $accentPhrasesJson = $synthesizer->createAccentPhrases($text, $styleId); $accentPhrases = json_decode($accentPhrasesJson, true); // モーラのピッチと音素長を別のスタイルで上書き $updatedJson = $synthesizer->replaceMoraData($accentPhrasesJson, $styleId); ``` ## GPU モードの使用 GPU が利用可能な環境では `AccelerationMode::Gpu` を指定すると高速化できます: ```php theme={null} use Revolution\Voicevox\Core\Enums\AccelerationMode; $synthesizer = new Synthesizer($onnxruntime, $openJtalk, AccelerationMode::Gpu); // GPU モードかどうかを確認 if ($synthesizer->isGpuMode()) { echo 'GPU モードで動作中' . PHP_EOL; } ``` ## 複数の音声モデルを使う 複数の `.vvm` ファイルを読み込んで、スタイル ID を切り替えることで複数のキャラクターボイスを使い分けられます: ```php theme={null} $model0 = VoiceModelFile::open($voicevoxCoreDir . '/models/vvms/0.vvm'); $model1 = VoiceModelFile::open($voicevoxCoreDir . '/models/vvms/1.vvm'); $synthesizer->loadVoiceModel($model0); $synthesizer->loadVoiceModel($model1); // モデルに含まれるスピーカーの情報を確認 $metas = json_decode($synthesizer->metas(), true); // モデルをアンロード(不要になった場合) $synthesizer->unloadVoiceModel($model0->id()); ``` # LaravelとAI開発 Source: https://kawax.biz/jp/ai LaravelはAI支援開発に最適なフレームワークです。Laravel BoostによるMCPサーバー連携、AIガイドライン、ドキュメント検索APIを活用して、AIコーディングエージェントの能力を最大限に引き出しましょう。 ## LaravelがAI開発に向いている理由 [Claude Code](https://docs.anthropic.com/en/docs/claude-code)、[Cursor](https://cursor.com)、[GitHub Copilot](https://github.com/features/copilot) などのAIコーディングエージェントは、コード生成・デバッグ・リファクタリングを劇的に加速させます。しかし、エージェントの能力を最大限に発揮するには、コードベースをエージェントが正確に理解できる必要があります。 Laravelはその点で他のフレームワークより優位に立っています。規約に基づいた設計のため、「コントローラーを追加して」と頼めばエージェントは置き場所を把握しています。「マイグレーションを作って」と頼めば命名規則もファイルの場所も予測できます。この一貫性が、AIツールを迷わせる「曖昧さ」を取り除きます。 Eloquentのリレーション、フォームリクエスト、ミドルウェアといった機能は、エージェントが確実に理解して再現できるパターンに従っています。結果として生成されるコードは、汎用的なPHPスニペットの寄せ集めではなく、熟練のLaravel開発者が書いたようなイディオマティックなコードになります。 ## Laravel Boost [Laravel Boost](https://github.com/laravel/boost) は、AIコーディングエージェントとLaravelアプリケーションの橋渡しをするパッケージです。Boostは **MCP(Model Context Protocol)サーバー** として動作し、15以上の専用ツールを通じてエージェントにアプリケーションの構造・データベース・ルートなどを深く理解させます。 Boostが提供する主な機能は3つです。 アプリケーションの検査・操作に使える15以上のツール。データベーススキーマ確認からArtisanコマンド実行まで対応。 Laravelエコシステム向けに設計されたAIガイドライン。慣用的なコードを書くためのベストプラクティスをエージェントに伝達。 17,000件以上のLaravel固有ドキュメントを検索。インストール済みパッケージのバージョンに合った正確な情報を提供。 ### インストール BoostはLaravel 10、11、12、13、PHP 8.1以上で動作します。まず開発依存として追加しましょう。 ```shell theme={null} composer require laravel/boost --dev ``` ```shell theme={null} php artisan boost:install ``` インストーラーがIDEとAIエージェントを自動検出します。使用する統合を選択すると、MCPサーバー用の `.mcp.json` やガイドラインファイルなど、必要な設定ファイルが生成されます。 `.mcp.json`、`CLAUDE.md`、`boost.json` などの生成ファイルは、開発者ごとに環境を設定したい場合、`.gitignore` に追加しても問題ありません。 ### 利用可能なMCPツール Boostがエージェントに公開するツールには以下のものがあります。 | ツール | 説明 | | - | - | | アプリケーション検査 | PHPとLaravelのバージョン、インストール済みパッケージ、設定・環境変数を確認 | | データベースツール | スキーマ確認、読み取り専用クエリ実行、データ構造の把握 | | ルート検査 | ミドルウェア・コントローラー・パラメーターを含む全登録ルートを一覧表示 | | Artisanコマンド | 利用可能なArtisanコマンドと引数を探索。適切なコマンドをエージェントが提案・実行 | | ログ解析 | アプリケーションログファイルの読み込みと解析でデバッグを支援 | | ブラウザログ | Laravelのフロントエンドツール使用時のブラウザコンソールログとエラーにアクセス | | Tinker統合 | Laravel Tinker経由でアプリケーションコンテキスト内のPHPコードを実行し、動作を検証 | | ドキュメント検索 | インストール済みパッケージバージョンに対応したLaravelエコシステムのドキュメントを検索 | ### AIガイドライン BoostのAIガイドラインは、Laravelエコシステム向けに設計されたコーディング規約集です。慣用的なLaravelコードの書き方、フレームワーク規約、よくある落とし穴を避ける方法をエージェントに伝えます。ガイドラインはインストール済みパッケージのバージョンに応じて自動的に組み合わされます。 対応パッケージの例: * Livewire(2.x、3.x、4.x) * Inertia.js(React、Svelte、Vueバリアント) * Tailwind CSS(3.x、4.x) * Filament(3.x、4.x) * PHPUnit・Pest PHP・Laravel Pint など `boost:install` を実行すると、`composer.json` に基づいて使用中のパッケージを自動検出し、関連するガイドラインをAIコンテキストファイルに組み込みます。 ### Agent Skills [Agent Skills](https://agentskills.io/home) は、エージェントが必要なときにオンデマンドで読み込む軽量な知識モジュールです。ガイドラインとは異なり、最初からすべてを読み込むのではなく、特定のドメインで作業するときだけ関連するスキルを有効化します。これによりコンテキストの肥大化を防ぎ、生成コードの精度を高めます。 Livewire、Inertia、Tailwind CSS、Pestなどの人気パッケージ向けスキルが用意されています。`boost:install` でスキル機能を選択すると、`composer.json` の内容に基づいて自動的にインストールされます。 ### ドキュメント検索API BoostのドキュメントAPIは、17,000件以上のLaravelエコシステムドキュメントをエージェントが検索できるようにします。一般的なウェブ検索とは異なり、インストール済みパッケージのバージョンに合わせてインデックス化・ベクトル化されています。 エージェントがある機能の動作を調べたいとき、このAPIを使ってバージョン固有の正確な情報を取得できます。古いフレームワークバージョンの非推奨メソッドや構文をエージェントが提案してしまう問題を解消します。 ### 対応エージェント Boostは Model Context Protocol をサポートする主要なIDEとAIツールに対応しています。 各エージェントの詳細なセットアップ手順は [Boostドキュメントの「Set Up Your Agents」セクション](https://laravel.com/docs/boost#set-up-your-agents) を参照してください。 ## 次のステップ 各エージェントとの詳細な連携方法、MCPツールのリファレンス、ガイドラインのカスタマイズについては公式ドキュメントを参照してください。 # 認証入門 Source: https://kawax.biz/jp/authentication Laravelの認証機能の基本を解説します。スターターキットを使った認証システムの導入から、認証済みユーザーの取得方法まで説明します。 ## 認証とは 認証(Authentication)とは、アクセスしてきたユーザーが「誰であるか」を確認する仕組みです。 Webアプリケーションでは、ログインフォームでメールアドレスとパスワードを受け取り、正しければセッションにユーザー情報を保存します。 以降のリクエストではそのセッションを参照してユーザーを識別します。 Laravelの認証機能は「ガード」と「プロバイダー」という2つの概念で構成されています。 * **ガード**: 各リクエストでユーザーをどう認証するかを定義します。デフォルトは `session` ガードで、セッションとCookieを使って状態を管理します。 * **プロバイダー**: ユーザーをデータベースからどう取得するかを定義します。デフォルトはEloquentを使います。 認証の設定ファイルは `config/auth.php` です。デフォルト設定のままほとんどのWebアプリケーションに対応できます。 ```mermaid theme={null} flowchart TD A["ログインリクエスト"] --> B["ガード
(認証方法を定義)"] B --> C["プロバイダー
(ユーザーをDBから取得)"] C --> D{"認証チェック"} D -- "成功" --> E["セッションに
ユーザー情報を保存"] E --> F["認証済みページへ
リダイレクト"] D -- "失敗" --> G["ログイン画面に
リダイレクト"] ``` ## スターターキットによる認証 Laravelでは、`laravel new` でアプリケーションを作成するときにスターターキットを選ぶだけで、ログイン・登録・パスワードリセットなどの認証機能が自動的に構築されます。これが最も推奨される方法です。 Laravelインストーラーでアプリケーションを作成します。途中でスターターキットを選択するプロンプトが表示されます。 ```shell theme={null} laravel new my-app ``` スターターキットとして **React**、**Vue**、**Livewire**、**Svelte** から選択できます。 チームの技術スタックに合わせて選んでください。 ```shell theme={null} cd my-app npm install && npm run build ``` `.env` ファイルのデータベース設定を確認した上で、マイグレーションを実行します。 ```shell theme={null} php artisan migrate ``` `users` テーブルを含む初期マイグレーションが適用されます。 ```shell theme={null} composer run dev ``` ブラウザで `http://localhost:8000` にアクセスすると、ナビゲーションに「Register」と「Log in」リンクが表示されます。 `/register` にアクセスしてユーザーを登録してみましょう。 スターターキットを使うと、以下の機能がすぐに利用できます。 | 機能 | URL | | - | - | | ユーザー登録 | `/register` | | ログイン | `/login` | | パスワードリセット | `/forgot-password` | | メール確認 | `/email/verify` | | プロフィール編集 | `/settings/profile` | スターターキットで生成されたコード(コントローラー・ルート・ビュー)はすべて自分のアプリケーション内に存在します。自由に修正してカスタマイズできます。 ### 利用可能なスターターキット #### React React 19・TypeScript・Tailwind・[shadcn/ui](https://ui.shadcn.com) を使ったモダンなSPAを構築できます。 [Inertia](https://inertiajs.com) を使うことで、サーバーサイドルーティングを維持しながらReactのフロントエンドを利用できます。 #### Vue Vue Composition API・TypeScript・Tailwind・[shadcn-vue](https://www.shadcn-vue.com/) を採用しています。 Reactと同様にInertiaを使ってサーバーサイドと連携します。 #### Livewire PHPだけで動的なUIを構築できる[Livewire](https://livewire.laravel.com)を使います。 Bladeテンプレートが中心のチームや、JavaScriptフレームワークを使わずに済ませたい場合に最適です。 [Flux UI](https://fluxui.dev) コンポーネントライブラリが含まれています。 #### Svelte Svelte 5・TypeScript・Tailwind・[shadcn-svelte](https://www.shadcn-svelte.com/) を使います。 Inertiaと組み合わせてモダンなSPAを構築できます。 ## 認証ファサード `Auth` ファサードを使うと、現在認証されているユーザーの情報を取得したり、認証状態を確認したりできます。 ### 認証済みユーザーの取得 ```php theme={null} use Illuminate\Support\Facades\Auth; // 現在のユーザーを取得する $user = Auth::user(); // ユーザーIDのみ取得する $id = Auth::id(); ``` コントローラーでは `Request` オブジェクトからもユーザーを取得できます。 ```php theme={null} user(); return view('dashboard', ['user' => $user]); } } ``` ### 認証状態の確認 `Auth::check()` は、ユーザーがログインしているかどうかを `true` / `false` で返します。 ```php theme={null} use Illuminate\Support\Facades\Auth; if (Auth::check()) { // ログイン済み } else { // 未ログイン } ``` Bladeテンプレートでは `@auth` と `@guest` ディレクティブを使うと便利です。 ```blade theme={null} @auth

こんにちは、{{ Auth::user()->name }} さん

ログアウト @endauth @guest ログイン 新規登録 @endguest ``` ## ルートの保護 ログイン済みのユーザーだけがアクセスできるルートを作るには、`auth` ミドルウェアを使います。 ```php theme={null} // routes/web.php // 認証済みユーザーのみアクセス可能 Route::get('/dashboard', function () { return view('dashboard'); })->middleware('auth'); ``` 未認証のユーザーがアクセスしようとすると、自動的に `/login` にリダイレクトされます。 複数のルートをまとめて保護するにはグループを使います。 ```php theme={null} Route::middleware('auth')->group(function () { Route::get('/dashboard', [DashboardController::class, 'index']); Route::get('/profile', [ProfileController::class, 'show']); Route::get('/settings', [SettingsController::class, 'index']); }); ``` `auth` ミドルウェアを付け忘れると、未ログインのユーザーがアクセスできてしまいます。保護が必要なルートには必ず適用してください。 ### ゲスト専用ルート ログイン済みユーザーをリダイレクトするには `guest` ミドルウェアを使います。 ログインページや登録ページに適用することで、すでにログインしているユーザーをダッシュボードに転送できます。 ```php theme={null} Route::middleware('guest')->group(function () { Route::get('/login', [AuthController::class, 'showLogin']); Route::get('/register', [AuthController::class, 'showRegister']); }); ``` ## 手動認証 スターターキットを使わずに手動でログイン処理を実装するには `Auth::attempt()` を使います。 ```php theme={null} validate([ 'email' => ['required', 'email'], 'password' => ['required'], ]); if (Auth::attempt($credentials)) { // 認証成功 — セッションを再生成してCSRF攻撃を防ぐ $request->session()->regenerate(); return redirect()->intended('/dashboard'); } // 認証失敗 return back()->withErrors([ 'email' => 'メールアドレスまたはパスワードが正しくありません。', ])->onlyInput('email'); } } ``` `Auth::attempt()` の第1引数に認証情報の配列を渡します。 パスワードは自動的にハッシュと比較されるので、平文のまま渡してください。 「ログイン状態を保持する」機能を実装するには、第2引数に `true` を渡します。 ```php theme={null} // 「ログイン状態を保持する」チェックボックスの値を使う Auth::attempt($credentials, $request->boolean('remember')); ``` 手動認証を実装する場合も、スターターキットのコードを参考にするのがおすすめです。安全な実装のお手本として活用できます。 ## ログアウト ユーザーをログアウトさせるには `Auth::logout()` を呼び出します。 セッションの無効化とCSRFトークンの再生成も合わせて行うのがベストプラクティスです。 ```php theme={null} session()->invalidate(); // CSRFトークンを再生成する $request->session()->regenerateToken(); return redirect('/'); } } ``` ルートにはPOSTメソッドを使います。 ```php theme={null} Route::post('/logout', [LogoutController::class, 'logout'])->middleware('auth'); ``` Bladeテンプレートではフォームを使ってPOSTリクエストを送ります。 ```blade theme={null}
@csrf
``` ## まとめ | やりたいこと | 方法 | | - | - | | 認証機能を素早く追加する | スターターキット(`laravel new`) | | 現在のユーザーを取得する | `Auth::user()` / `$request->user()` | | ログイン状態を確認する | `Auth::check()` | | ルートを保護する | `->middleware('auth')` | | 手動でログインする | `Auth::attempt($credentials)` | | ログアウトする | `Auth::logout()` | ## 次のステップ `auth` ミドルウェアの仕組みや独自ミドルウェアの作り方を詳しく学びます。 # 認可(ゲートとポリシー) Source: https://kawax.biz/jp/authorization Laravelのゲートとポリシーを使ってユーザーのアクション権限を管理する方法を解説します。 ## 認証と認可の違い **認証(Authentication)** はユーザーが「誰であるか」を確認します。ログイン処理がその代表です。 **認可(Authorization)** はそのユーザーが「何をできるか」を判断します。たとえば、自分の投稿だけを編集できる、管理者だけが設定を変更できる、といった制御です。 [認証](./authentication)でログイン機能を実装したら、次のステップが認可です。 Laravelの認可機能は、ゲート(Gates)とポリシー(Policies)の2つのアプローチを提供します。どちらを使うかはユースケースによって判断します。 **使い分けの基準** | シナリオ | 推奨 | | - | - | | 特定のモデルに紐づかないシンプルな判定 | ゲート | | モデルに対するCRUD操作の権限管理 | ポリシー | | 管理者ダッシュボードへのアクセス制御 | ゲート | | ブログ記事の投稿・編集・削除権限 | ポリシー | ## ゲート(Gates) ゲートはクロージャベースのシンプルな認可チェックです。特定のモデルに紐づかない権限判定に向いています。 ### Gateによる認可フロー ```mermaid theme={null} flowchart TD A["AppServiceProvider::boot()
(App\\Providers\\AppServiceProvider)"] --> B["Gate::define('ability', fn)
認可ロジックを登録"] C["コントローラーから認可チェック"] --> D{"Gate::before()
が設定されている?"} D -->|"はい"| E{"before() の返り値"} E -->|"true / false"| F["before() の結果を最終判定に使用"] E -->|"null"| G["define() のクロージャを実行"] D -->|"いいえ"| G G --> H{"クロージャの返り値"} H -->|"true"| I["✓ アクセス許可"] H -->|"false"| J["✗ アクセス拒否"] F -->|"true"| I F -->|"false"| J J -->|"Gate::allows()"| K["false を返す
→ abort(403) を手動で呼ぶ"] J -->|"Gate::authorize()"| L["AuthorizationException
→ 自動で 403 Forbidden"] ``` ### ゲートを定義する ゲートは `App\Providers\AppServiceProvider` の `boot` メソッド内で `Gate::define()` を使って定義します。 ```php theme={null} // app/Providers/AppServiceProvider.php use App\Models\Post; use App\Models\User; use Illuminate\Support\Facades\Gate; public function boot(): void { Gate::define('update-post', function (User $user, Post $post) { return $user->id === $post->user_id; }); } ``` ゲートのクロージャは常に第1引数で現在認証中のユーザーを受け取ります。第2引数以降に対象のモデルなど追加情報を渡せます。 ### ゲートで権限を確認する コントローラーでゲートを使って権限を確認するには `Gate::allows()` または `Gate::denies()` を使います。 ```php theme={null} use Illuminate\Support\Facades\Gate; public function update(Request $request, Post $post): RedirectResponse { if (! Gate::allows('update-post', $post)) { abort(403); } // 投稿を更新する処理... return redirect('/posts'); } ``` ### Gate::authorize() で例外を投げる 権限がない場合に自動で403レスポンスを返すには `Gate::authorize()` を使います。`abort(403)` を書く手間が省けます。 ```php theme={null} public function update(Request $request, Post $post): RedirectResponse { Gate::authorize('update-post', $post); // 権限があればここに到達する // 投稿を更新する処理... return redirect('/posts'); } ``` `Gate::authorize()` は権限がない場合に `Illuminate\Auth\Access\AuthorizationException` を投げます。Laravelはこれを自動的に403 HTTPレスポンスに変換します。 ### 管理者バイパス(before メソッド) 管理者ユーザーにすべての権限を与えたい場合は `Gate::before()` を使います。 ```php theme={null} use App\Models\User; use Illuminate\Support\Facades\Gate; public function boot(): void { // $ability には実行しようとしているゲート名('update-post' など)が入る Gate::before(function (User $user, string $ability) { if ($user->isAdministrator()) { return true; } }); Gate::define('update-post', function (User $user, Post $post) { return $user->id === $post->user_id; }); } ``` `before` のクロージャが `null` 以外の値を返すと、その結果が最終的な権限判定になります。`null` を返すか何も返さない場合は、通常のゲート判定に進みます。 ### Bladeテンプレートでの @can / @cannot テンプレートでゲート判定を使うには `@can` と `@cannot` ディレクティブが便利です。 ```blade theme={null} @can('update-post', $post) 編集 @endcan @cannot('update-post', $post)

この投稿を編集する権限がありません。

@endcannot ``` ## ポリシー(Policies) ポリシーは特定のモデルに関連する認可ロジックをクラスにまとめたものです。`Post` モデルに対する作成・閲覧・編集・削除の権限管理には、ゲートよりもポリシーが適しています。 ### Policyによる認可フロー ```mermaid theme={null} flowchart TD A["HTTPリクエスト"] --> B["コントローラー"] B --> C["Gate::authorize('update', $post)"] C --> D{"PostPolicy::before()
が定義されている?"} D -->|"はい"| E{"before() の返り値"} E -->|"true / false"| F["before() の結果を最終判定に使用"] E -->|"null"| G["PostPolicy::update(User, Post) を実行"] D -->|"いいえ"| G G --> H{"$user->id === $post->user_id?"} H -->|"true"| I["✓ 処理を続行"] H -->|"false"| J["✗ AuthorizationException
→ 403 Forbidden"] F -->|"true"| I F -->|"false"| J ``` ### ポリシーを生成する `make:policy` Artisanコマンドでポリシークラスを生成します。 ```shell theme={null} php artisan make:policy PostPolicy ``` モデルに対応するCRUDメソッドをすべて含むひな型を生成するには `--model` オプションを使います。 ```shell theme={null} php artisan make:policy PostPolicy --model=Post ``` `app/Policies/PostPolicy.php` が生成されます。 ### モデルとポリシーの自動検出 Laravelはデフォルトで命名規則に従ってポリシーを自動的に検出します。 * モデル: `app/Models/Post.php` * ポリシー: `app/Policies/PostPolicy.php` この命名規則を守れば、ポリシーの登録作業は不要です。 命名規則に従わない場合や手動で登録したい場合は、`AppServiceProvider` の `boot` メソッドで `Gate::policy()` を使います。 ```php theme={null} use App\Models\Post; use App\Policies\PostPolicy; use Illuminate\Support\Facades\Gate; Gate::policy(Post::class, PostPolicy::class); ``` Laravel 13では、モデルに `#[UsePolicy]` アトリビュートを付けて宣言的に登録することもできます。 ```php theme={null} use App\Policies\PostPolicy; use Illuminate\Database\Eloquent\Attributes\UsePolicy; #[UsePolicy(PostPolicy::class)] class Post extends Model {} ``` ### ポリシーメソッドを実装する `--model` オプションで生成したポリシーには、標準的なCRUDアクションに対応するメソッドが含まれています。 ```php theme={null} id === $post->user_id; // 自分の投稿のみ編集可能 } /** * 投稿を削除できるか */ public function delete(User $user, Post $post): bool { return $user->id === $post->user_id; // 自分の投稿のみ削除可能 } } ``` ### 管理者バイパス(before メソッド) ポリシーでも `before` メソッドを定義することで、管理者にすべての権限を与えられます。 ```php theme={null} /** * 他のポリシーメソッドより先に実行される事前チェック * $ability にはポリシーメソッド名('update', 'delete' など)が入る */ public function before(User $user, string $ability): bool|null { if ($user->isAdministrator()) { return true; // 管理者はすべての操作を許可 } return null; // null を返すと通常のポリシーメソッドへ進む } ``` `before` メソッドはポリシークラスに対応するメソッドが存在する場合にのみ呼び出されます。たとえば `update` メソッドが存在しない場合、`before` も呼び出されません。 ### before コールバックの実行順序 ```mermaid theme={null} flowchart TD A["認可チェック開始
authorize('update', $post)"] --> B["PostPolicy::before() を実行"] B --> C{"返り値は null?"} C -->|"null 以外 (true / false)"| D["before() の結果を最終判定として使用"] C -->|"null"| E["通常のポリシーメソッドを実行
update() / delete() など"] E --> F{"ポリシーメソッドの返り値"} F -->|"true"| G["✓ アクセス許可"] F -->|"false"| H["✗ アクセス拒否 → 403"] D -->|"true (管理者バイパス)"| G D -->|"false"| H ``` ## コントローラーでポリシーを使う ### authorize() メソッド Laravelのコントローラーには `authorize()` ヘルパーメソッドがあります(ベースコントローラーを使わない場合は `Gate::authorize()` を使います)。 ```php theme={null} route('posts.index'); } public function store(Request $request): RedirectResponse { Gate::authorize('create', Post::class); // 投稿を作成する処理... return redirect()->route('posts.index'); } public function destroy(Post $post): RedirectResponse { Gate::authorize('delete', $post); $post->delete(); return redirect()->route('posts.index'); } } ``` ```php theme={null} user()->cannot('update', $post)) { abort(403); } // 投稿を更新する処理... return redirect()->route('posts.index'); } } ``` ### authorizeResource() でRESTfulポリシーを一括登録 `authorizeResource()` をコンストラクターで呼ぶと、コントローラーの各アクションに対応するポリシーメソッドを自動的に紐付けます。 ```php theme={null} authorizeResource(Post::class, 'post'); } // index(), show(), create(), store(), edit(), update(), destroy() に // 対応するポリシーメソッドが自動で適用される } ``` コントローラーアクションとポリシーメソッドの対応関係: | コントローラーアクション | ポリシーメソッド | | - | - | | `index` | `viewAny` | | `show` | `view` | | `create` / `store` | `create` | | `edit` / `update` | `update` | | `destroy` | `delete` | `authorizeResource()` を使うと、各アクションに個別で `authorize()` を書く必要がなくなります。RESTfulなリソースコントローラーと組み合わせると特に便利です。 ## ミドルウェアでの認可 ルートレベルで認可チェックを行うには `can` ミドルウェアを使います。 ```php theme={null} use App\Models\Post; // 投稿の更新権限があるユーザーのみアクセス可能 Route::put('/posts/{post}', [PostController::class, 'update']) ->middleware('can:update,post'); // 投稿の作成権限があるユーザーのみアクセス可能 Route::post('/posts', [PostController::class, 'store']) ->middleware('can:create,App\Models\Post'); ``` `can` メソッドを使ったより簡潔な記述: ```php theme={null} Route::put('/posts/{post}', [PostController::class, 'update']) ->can('update', 'post'); ``` ## Bladeテンプレートでポリシーを使う ポリシーを登録すると、Bladeの `@can` / `@cannot` ディレクティブでもポリシーが自動的に使われます。 ```blade theme={null} {{-- 投稿の編集ボタンは権限を持つユーザーにのみ表示 --}} @can('update', $post) 編集 @endcan {{-- 投稿の削除ボタンも同様 --}} @can('delete', $post)
@csrf @method('DELETE')
@endcan {{-- 新規投稿ボタン --}} @can('create', App\Models\Post::class) 新規投稿 @endcan ``` ## 実践例: ブログ記事の権限管理 ブログアプリで「投稿者だけが自分の記事を編集・削除できる」権限を実装する例です。 ```shell theme={null} php artisan make:policy PostPolicy --model=Post ``` ```php theme={null} is_admin) { return true; } return null; } public function viewAny(User $user): bool { return true; } public function view(User $user, Post $post): bool { return true; } public function create(User $user): bool { return true; } public function update(User $user, Post $post): bool { return $user->id === $post->user_id; } public function delete(User $user, Post $post): bool { return $user->id === $post->user_id; } } ``` ```php theme={null} authorizeResource(Post::class, 'post'); } public function index(): View { $posts = Post::latest()->paginate(10); return view('posts.index', compact('posts')); } public function store(Request $request): RedirectResponse { $post = $request->user()->posts()->create( $request->validate([ 'title' => ['required', 'string', 'max:255'], 'body' => ['required', 'string'], ]) ); return redirect()->route('posts.show', $post); } public function update(Request $request, Post $post): RedirectResponse { $post->update( $request->validate([ 'title' => ['required', 'string', 'max:255'], 'body' => ['required', 'string'], ]) ); return redirect()->route('posts.show', $post); } public function destroy(Post $post): RedirectResponse { $post->delete(); return redirect()->route('posts.index'); } } ``` ```blade theme={null} {{-- resources/views/posts/show.blade.php --}}

{{ $post->title }}

{{ $post->body }}

投稿者: {{ $post->user->name }}

@can('update', $post) 編集 @endcan @can('delete', $post)
@csrf @method('DELETE')
@endcan ```
## まとめ * **ゲート**: 特定のモデルに紐づかないシンプルな権限判定。管理者ダッシュボードへのアクセスや、グローバルな設定の変更権限など。 * **ポリシー**: モデルに対するCRUD操作の権限管理。`Post`、`Order`、`Comment` といったリソースごとにポリシークラスを作成する。 ```php theme={null} // ゲートで確認 Gate::allows('update-post', $post); // true/false Gate::denies('update-post', $post); // true/false Gate::authorize('update-post', $post); // 失敗時に例外 // ユーザーモデルで確認 $user->can('update', $post); // true/false $user->cannot('update', $post); // true/false // コントローラーで確認 Gate::authorize('update', $post); // 失敗時に403 // Bladeで確認 @can('update', $post) ... @endcan @cannot('update', $post) ... @endcannot ``` ```shell theme={null} # 空のポリシーを生成 php artisan make:policy PostPolicy # モデルに対応するCRUDメソッド付きで生成 php artisan make:policy PostPolicy --model=Post ``` # Bladeテンプレート Source: https://kawax.biz/jp/blade Laravelのテンプレートエンジン「Blade」の基本構文、条件分岐、ループ、レイアウト継承を解説します。 ## はじめに BladeはLaravelに含まれるシンプルかつ強力なテンプレートエンジンです。 PHPのテンプレートエンジンの一部と異なり、Bladeはテンプレート内で素のPHPコードの使用を制限しません。 すべてのBladeテンプレートはPHPコードにコンパイルされてキャッシュされるため、Bladeはアプリケーションにほとんどオーバーヘッドを加えません。 Bladeテンプレートファイルは `.blade.php` 拡張子を使い、通常は `resources/views` ディレクトリに格納されます。 ### Bladeコンパイルフロー `.blade.php` ファイルがどのようにHTMLレスポンスに変換されるかを示します。一度コンパイルされたファイルはキャッシュされるため、同一テンプレートの2回目以降のリクエストはコンパイルをスキップします。 ```mermaid theme={null} flowchart LR A[".blade.phpファイル"] --> B{"キャッシュ存在?"} B -->|"Yes"| C["キャッシュ済みPHPを使用"] B -->|"No"| D["Bladeコンパイラー"] D --> E["PHPコードに変換"] E --> F["storage/framework/views/に保存"] F --> G["PHPを実行"] C --> G G --> H["HTMLレスポンス"] ``` ```php theme={null} Route::get('/', function () { return view('greeting', ['name' => '太郎']); }); ``` ## データの表示 Bladeビューに渡されたデータを表示するには、変数を二重の波括弧で囲みます。 ```blade theme={null} こんにちは、{{ $name }}。 ``` Bladeの `{{ }}` は自動的にPHPの `htmlspecialchars` 関数を通してXSS攻撃を防ぎます。 変数だけでなく、PHP関数の結果も表示できます。 ```blade theme={null} 現在のUNIXタイムスタンプ: {{ time() }} ``` ### エスケープしないデータの表示 エスケープを省略したい場合は `{!! !!}` 構文を使います。 ```blade theme={null} こんにちは、{!! $name !!}。 ``` ユーザーが入力したデータを表示するときは、必ず `{{ }}` を使って自動エスケープを有効にしてください。 `{!! !!}` を信頼できない入力に使うとXSS脆弱性につながります。 ### JavaScriptフレームワークとの共存 JavaScriptフレームワークも波括弧を使う場合は、`@` シンボルでBladeのレンダリングをスキップさせます。 ```blade theme={null}

Laravel

こんにちは、@{{ name }}。 ``` この場合、`@` はBladeによって除去されますが、`{{ name }}` はそのままブラウザに送られてJavaScriptフレームワークによってレンダリングされます。 Bladeを中心に進めるか、LivewireやInertiaへ広げるかを比較したい場合は [フロントエンド](/jp/frontend) を参照してください。 ## Bladeディレクティブ ### 条件分岐(if文) ```blade theme={null} @if (count($records) === 1) レコードが1件あります。 @elseif (count($records) > 1) 複数のレコードがあります。 @else レコードがありません。 @endif ``` `@unless` で「〜でなければ」という条件も書けます。 ```blade theme={null} @unless (Auth::check()) ログインしていません。 @endunless ``` 変数が定義されているか・nullでないかを確認するには `@isset` と `@empty` を使います。 ```blade theme={null} @isset($records) // $records が定義済みかつnullでない場合 @endisset @empty($records) // $records が「空」の場合 @endempty ``` ### 認証ディレクティブ ```blade theme={null} @auth // 認証済みユーザー向けのコンテンツ @endauth @guest // 未認証ユーザー向けのコンテンツ @endguest ``` ### Switch文 ```blade theme={null} @switch($i) @case(1) 最初のケース... @break @case(2) 2番目のケース... @break @default デフォルトのケース... @endswitch ``` ### ループ Bladeはループを扱うための便利なディレクティブを提供します。 ```blade theme={null} @for ($i = 0; $i < 10; $i++) 現在の値: {{ $i }} @endfor @foreach ($users as $user)

ユーザー: {{ $user->name }}

@endforeach @forelse ($users as $user)
  • {{ $user->name }}
  • @empty

    ユーザーがいません

    @endforelse @while (true)

    永久にループします。

    @endwhile ``` ループ内では `@break` と `@continue` を使ってループを制御できます。 ```blade theme={null} @foreach ($users as $user) @if ($user->type == 1) @continue @endif
  • {{ $user->name }}
  • @if ($user->number == 5) @break @endif @endforeach ``` 条件を直接ディレクティブに渡すこともできます。 ```blade theme={null} @foreach ($users as $user) @continue($user->type == 1)
  • {{ $user->name }}
  • @break($user->number == 5) @endforeach ``` ### ループ変数 `@foreach` ループ内では、`$loop` 変数を使ってループに関する情報を取得できます。 ```blade theme={null} @foreach ($users as $user) @if ($loop->first) 最初の繰り返しです。 @endif @if ($loop->last) 最後の繰り返しです。 @endif

    ユーザー: {{ $user->name }}

    @endforeach ``` | プロパティ | 説明 | | - | - | | `$loop->index` | 現在のループインデックス(0始まり) | | `$loop->iteration` | 現在のループ反復回数(1始まり) | | `$loop->remaining` | 残りの反復回数 | | `$loop->count` | 配列の要素数 | | `$loop->first` | 最初の繰り返しか | | `$loop->last` | 最後の繰り返しか | | `$loop->even` | 偶数番目の繰り返しか | | `$loop->odd` | 奇数番目の繰り返しか | | `$loop->depth` | 現在のループのネスト深度 | | `$loop->parent` | ネストしたループの場合、親ループの変数 | ## コメント Bladeコメントはレンダリングされたページに含まれません。 ```blade theme={null} {{-- このコメントはレンダリングされたHTMLに含まれません --}} ``` ## レイアウト ### コンポーネントを使ったレイアウト Laravelのレイアウト構築には、Bladeコンポーネントを使う方法が推奨されます。 まずレイアウトコンポーネントを作成します。 ```blade theme={null} {{ $title ?? 'アプリ名' }}
    {{ $slot }}
    ``` 次にこのレイアウトを使うビューを作ります。 ```blade theme={null} ダッシュボード

    ダッシュボード

    ようこそ!

    ``` ### テンプレート継承を使ったレイアウト 古典的な方法として、`@extends`・`@section`・`@yield` を使ったテンプレート継承があります。 ```mermaid theme={null} flowchart TD A["子ビュー
    @extends('layouts.app')"] --> B["親レイアウト
    layouts/app.blade.php"] A --> C["@section('title', '子ページ')"] A --> D["@section('content')
    コンテンツ本体
    @endsection"] B --> E["@yield('title')
    ← titleを埋め込み"] B --> F["@yield('content')
    ← contentを埋め込み"] C --> E D --> F E --> G["最終HTML"] F --> G ``` **親レイアウトの定義:** ```blade theme={null} アプリ名 - @yield('title') @section('sidebar') デフォルトのサイドバー @show
    @yield('content')
    ``` **子ビューでのレイアウト継承:** ```blade theme={null} @extends('layouts.app') @section('title', '子ページ') @section('sidebar') @parent

    サイドバーに追加されるコンテンツ

    @endsection @section('content')

    コンテンツ本体

    @endsection ``` `@parent` を使うと、親レイアウトの `@section` の内容を維持しながら追記できます。 ## サブビューのインクルード `@include` ディレクティブを使うと、Bladeビューを別のビューに埋め込めます。 親ビューで利用可能なすべての変数は、インクルードされたビューでも利用できます。 ```blade theme={null}
    @include('shared.errors')
    ``` 変数を追加で渡すこともできます。 ```blade theme={null} @include('view.name', ['status' => 'complete']) ``` ## フォームのCSRF保護 `web` ルートファイルで `POST`・`PUT`・`PATCH`・`DELETE` リクエストを送るHTMLフォームには、必ず `@csrf` ディレクティブを含めてください。 ```blade theme={null}
    @csrf
    ``` `PUT`・`PATCH`・`DELETE` リクエストをフォームから送る場合は `@method` ディレクティブを使います。 ```blade theme={null}
    @csrf @method('DELETE')
    ``` Origin検証や `X-CSRF-TOKEN` / `X-XSRF-TOKEN` ヘッダーを含む実運用のCSRF対策は [CSRFプロテクション](/jp/csrf) を参照してください。 ## まとめ Bladeテンプレートエンジンの主な機能をまとめます。 `{{ $variable }}` でエスケープ付き表示、`{!! $variable !!}` でエスケープなし表示。 `@if`・`@elseif`・`@else`・`@endif`、`@unless`、`@isset`、`@empty`、`@auth`、`@guest`。 `@for`・`@foreach`・`@forelse`・`@while` と `$loop` 変数を使った制御。 コンポーネントベース(`` と `{{ $slot }}`)またはテンプレート継承(`@extends`・`@section`・`@yield`)。 `@include` でビューを再利用可能なパーツとして埋め込み。 # コレクション Source: https://kawax.biz/jp/collections LaravelのCollectionクラスを使ってデータを効率的に操作する方法を解説します。 ## コレクションとは `Illuminate\Support\Collection` クラスは、配列データを操作するための流暢なラッパーです。 PHPの標準的な配列関数を個別に呼び出す代わりに、メソッドチェーンで直感的にデータを加工できます。 ```php theme={null} // 通常の配列操作 $names = array_filter( array_map(fn ($user) => $user['name'], $users), fn ($name) => $name !== null ); // コレクションを使うと $names = collect($users) ->pluck('name') ->filter() ->values(); ``` コレクションは**イミュータブル**(不変)です。各メソッドは元のコレクションを変更せず、新しいコレクションインスタンスを返します。元のデータを保持しながら安全に操作できます。 ## コレクションの作成 ### `collect()` ヘルパー 最もよく使われる方法です。配列を渡してコレクションを生成します。 ```php theme={null} use Illuminate\Support\Collection; // 配列からコレクションを作成 $users = collect([ ['name' => '山田太郎', 'age' => 28, 'role' => 'admin'], ['name' => '鈴木花子', 'age' => 34, 'role' => 'editor'], ['name' => '佐藤次郎', 'age' => 22, 'role' => 'viewer'], ]); // ネストした配列もOK $products = collect([ ['name' => 'ノートPC', 'price' => 120000, 'stock' => 5], ['name' => 'マウス', 'price' => 3500, 'stock' => 20], ['name' => 'キーボード', 'price' => 8000, 'stock' => 12], ]); ``` ### `Collection::make()` `collect()` と同等です。ファサードスタイルで書きたい場合に使います。 ```php theme={null} $collection = Collection::make([1, 2, 3]); ``` ### `Collection::fromJson()` JSON文字列からコレクションを作成します。外部APIのレスポンスを処理する場合に便利です。 ```php theme={null} $collection = Collection::fromJson('[{"name":"山田太郎"},{"name":"鈴木花子"}]'); ``` ## よく使うメソッド ### `map` — 各要素を変換する コレクションの各要素に処理を適用し、変換された新しいコレクションを返します。 ```php theme={null} $users = collect([ ['name' => '山田太郎', 'email' => 'yamada@example.com'], ['name' => '鈴木花子', 'email' => 'suzuki@example.com'], ]); // メールアドレスをマスクして表示用に変換 $masked = $users->map(function (array $user) { $parts = explode('@', $user['email']); return [ 'name' => $user['name'], 'email' => substr($parts[0], 0, 2) . '***@' . $parts[1], ]; }); // [['name' => '山田太郎', 'email' => 'ya***@example.com'], ...] ``` ### `filter` / `reject` — 条件で絞り込む `filter()` は条件を満たす要素だけを残し、`reject()` は条件を満たす要素を除外します。 ```php theme={null} $products = collect([ ['name' => 'ノートPC', 'price' => 120000, 'in_stock' => true], ['name' => 'マウス', 'price' => 3500, 'in_stock' => false], ['name' => 'キーボード', 'price' => 8000, 'in_stock' => true], ]); // 在庫ありの商品だけ取得 $inStock = $products->filter(fn ($product) => $product['in_stock']); // 在庫なしの商品を除外(reject は filter の反転) $available = $products->reject(fn ($product) => ! $product['in_stock']); // 引数なしの filter() は falsy な値を除去 $names = collect(['山田', '', null, '鈴木', false])->filter()->values(); // ['山田', '鈴木'] ``` `filter()` 後はインデックスが歯抜けになります。`values()` を呼ぶと0始まりの連番に振り直されます。 ### `first` / `last` — 要素を1件取得する 条件を満たす最初または最後の要素を返します。 ```php theme={null} $orders = collect([ ['id' => 1, 'status' => 'shipped', 'amount' => 5000], ['id' => 2, 'status' => 'pending', 'amount' => 12000], ['id' => 3, 'status' => 'pending', 'amount' => 3500], ]); // 最初の未処理注文を取得 $nextOrder = $orders->first(fn ($order) => $order['status'] === 'pending'); // ['id' => 2, 'status' => 'pending', 'amount' => 12000] // 条件に一致しない場合のデフォルト値 $order = $orders->first(fn ($order) => $order['status'] === 'cancelled', null); // 最後の要素 $latest = $orders->last(); ``` ### `pluck` — 特定のキーの値だけ取り出す ネストした配列やモデルから特定のフィールドだけを抜き出します。 ```php theme={null} $users = collect([ ['id' => 1, 'name' => '山田太郎', 'department' => '開発部'], ['id' => 2, 'name' => '鈴木花子', 'department' => '営業部'], ['id' => 3, 'name' => '佐藤次郎', 'department' => '開発部'], ]); // 名前だけ取り出す $names = $users->pluck('name'); // ['山田太郎', '鈴木花子', '佐藤次郎'] // id をキーにしてマッピング $nameById = $users->pluck('name', 'id'); // [1 => '山田太郎', 2 => '鈴木花子', 3 => '佐藤次郎'] ``` ### `groupBy` — 特定のキーでグループ化する ```php theme={null} $users = collect([ ['name' => '山田太郎', 'department' => '開発部'], ['name' => '鈴木花子', 'department' => '営業部'], ['name' => '佐藤次郎', 'department' => '開発部'], ['name' => '田中美咲', 'department' => '営業部'], ]); $byDepartment = $users->groupBy('department'); // [ // '開発部' => [['name' => '山田太郎', ...], ['name' => '佐藤次郎', ...]], // '営業部' => [['name' => '鈴木花子', ...], ['name' => '田中美咲', ...]], // ] // クロージャでグループキーを動的に指定 $byFirstChar = $users->groupBy(fn ($user) => mb_substr($user['name'], 0, 1)); ``` ### `sortBy` / `sortByDesc` — 並び替える ```php theme={null} $products = collect([ ['name' => 'ノートPC', 'price' => 120000], ['name' => 'マウス', 'price' => 3500], ['name' => 'キーボード', 'price' => 8000], ]); // 価格の昇順 $cheapFirst = $products->sortBy('price'); // 価格の降順 $expensiveFirst = $products->sortByDesc('price'); // 複数キーで並び替え $sorted = $products->sortBy([ ['price', 'asc'], ['name', 'asc'], ]); ``` ### `each` — 各要素に処理を実行する 副作用のある処理(ログ出力、メール送信など)に使います。コレクション自体は変更しません。 ```php theme={null} $orders = collect([ ['id' => 1, 'user_id' => 10, 'amount' => 5000], ['id' => 2, 'user_id' => 11, 'amount' => 12000], ]); $orders->each(function (array $order) { \Log::info("注文 #{$order['id']} を処理中", ['amount' => $order['amount']]); // 通知送信やキューへのディスパッチなど }); // false を返すと途中で処理を抜けられる $orders->each(function (array $order) { if ($order['amount'] > 10000) { return false; // ループを抜ける } // ... }); ``` ### `flatMap` — マップ後に1段階フラット化する 各要素を配列に変換し、全体を1次元に平坦化します。 ```php theme={null} $users = collect([ ['name' => '山田太郎', 'tags' => ['php', 'laravel']], ['name' => '鈴木花子', 'tags' => ['javascript', 'vue']], ]); // 全ユーザーのタグをフラットなリストに $allTags = $users->flatMap(fn ($user) => $user['tags']); // ['php', 'laravel', 'javascript', 'vue'] ``` ### `reduce` — 集計する コレクション全体を1つの値に畳み込みます。 ```php theme={null} $orders = collect([ ['product' => 'ノートPC', 'quantity' => 1, 'price' => 120000], ['product' => 'マウス', 'quantity' => 2, 'price' => 3500], ['product' => 'キーボード', 'quantity' => 1, 'price' => 8000], ]); // 合計金額を計算 $total = $orders->reduce( fn ($carry, $order) => $carry + ($order['price'] * $order['quantity']), 0 ); // 135000 ``` 単純な合計なら `sum()` が使えます: `$orders->sum(fn ($o) => $o['price'] * $o['quantity'])` ### `reduceInto` — オブジェクトに集計する `reduce` と同様に畳み込み処理を行いますが、コールバックが返り値を返す必要がない点が異なります。既存のオブジェクトを直接ミュートするときに便利です。 ```php theme={null} class OrderStats { public int $total = 0; public int $count = 0; } $orders = collect([ ['amount' => 100], ['amount' => 250], ['amount' => 50], ]); $stats = $orders->reduceInto(new OrderStats, function (OrderStats $stats, array $order) { $stats->total += $order['amount']; $stats->count++; }); $stats->total; // 400 $stats->count; // 3 ``` `reduce` はコールバックの戻り値が次の `$carry` になるため、プリミティブな値の集計に向いています。`reduceInto` はオブジェクトをそのまま変更し続けるためシンプルなコードになります。 スカラーや配列に集計する場合は、コールバックで参照渡し(`&`)を使います。 ```php theme={null} $collection = collect([1, 2, 3, 4, 5]); $even = $collection->reduceInto([], function (array &$result, int $value) { if ($value % 2 === 0) { $result[] = $value; } }); // [2, 4] ``` ### `chunk` — 分割する 大きなコレクションを指定サイズに分割します。バッチ処理や画面表示で便利です。 ```php theme={null} $users = collect(range(1, 100))->map(fn ($i) => ['id' => $i, 'name' => "ユーザー{$i}"]); // 10件ずつに分割 $chunks = $users->chunk(10); // $chunks->count() === 10 // バッチごとに処理 $chunks->each(function (\Illuminate\Support\Collection $batch) { // バッチごとのDB操作やメール送信など }); ``` ### `chunkBy` — 隣接する値でグループ化して分割する `chunkBy` は、指定したキーまたはコールバックの値が同じ「隣接する」要素をグループ化して、複数の小さなコレクションに分割します。例えば、同じ親を持つ隣り合う商品をグループ化できます。 ```php theme={null} $chunks = $products->chunkBy('parent'); ``` `groupBy` と異なり、同じ値でも隣接していない要素は別々のチャンクになります。 ```php theme={null} $collection = collect([1, 1, 2, 2, 1]); $chunks = $collection->chunkBy(fn (int $value) => $value); $chunks->all(); // [[1, 1], [2, 2], [1]] ``` ## メソッドチェーン コレクションの真価はメソッドチェーンにあります。複数の操作をつなげて、複雑なデータ変換を1つの式で表現できます。 ```php theme={null} $orders = collect([ ['customer' => '山田太郎', 'status' => 'completed', 'amount' => 5000, 'category' => '電子機器'], ['customer' => '鈴木花子', 'status' => 'pending', 'amount' => 12000, 'category' => '電子機器'], ['customer' => '佐藤次郎', 'status' => 'completed', 'amount' => 3500, 'category' => '文具'], ['customer' => '田中美咲', 'status' => 'completed', 'amount' => 8000, 'category' => '電子機器'], ['customer' => '伊藤健一', 'status' => 'cancelled', 'amount' => 2000, 'category' => '文具'], ]); // 完了済みの電子機器注文を金額降順で取得し、顧客名と金額だけ抽出する $result = $orders ->filter(fn ($order) => $order['status'] === 'completed') ->filter(fn ($order) => $order['category'] === '電子機器') ->sortByDesc('amount') ->map(fn ($order) => [ 'customer' => $order['customer'], 'amount' => number_format($order['amount']) . '円', ]) ->values(); // [ // ['customer' => '田中美咲', 'amount' => '8,000円'], // ['customer' => '山田太郎', 'amount' => '5,000円'], // ] ``` ## Eloquentとの連携 Eloquentクエリの結果は常に `Illuminate\Database\Eloquent\Collection` インスタンスとして返されます。 これは基本の `Collection` を継承しており、上記のメソッドがすべて使えます。 ```php theme={null} use App\Models\User; use App\Models\Order; // get() の結果はコレクション $users = User::where('is_active', true)->get(); // Collection // コレクションのメソッドをそのまま使える $adminEmails = User::all() ->filter(fn ($user) => $user->role === 'admin') ->pluck('email'); // リレーションのロード済みデータもコレクション操作できる $orders = Order::with('items')->where('status', 'completed')->get(); $summary = $orders->map(fn ($order) => [ 'id' => $order->id, 'customer' => $order->user->name, 'item_count' => $order->items->count(), 'total' => $order->items->sum('price'), ]); ``` データベースで処理できるフィルタリングや並び替えは、コレクション操作ではなくクエリビルダ(`where`、`orderBy` など)で行いましょう。コレクションへの変換後にフィルタリングすると、不要なデータをすべてメモリに読み込んでしまいます。 ### Eloquent固有のコレクションメソッド `Eloquent\Collection` には追加のメソッドがあります。 ```php theme={null} $users = User::all(); // モデルをIDで検索 $user = $users->find(1); // 主キーのコレクションを取得 $ids = $users->modelKeys(); // [1, 2, 3, ...] // リレーションをまとめてロード $users->load('orders', 'profile'); // 差集合・積集合 $diff = $users->diff($otherUsers); $intersect = $users->intersect($otherUsers); ``` ## Lazy Collections 通常のコレクションはデータ全体をメモリに読み込みますが、`LazyCollection` はPHPのジェネレーターを活用し、データを1件ずつ処理します。 数万件以上の大量データを扱う場合に、メモリを節約できます。 ```php theme={null} use Illuminate\Support\LazyCollection; // 通常のコレクション: 全データをメモリに読み込む $users = User::all(); // 10万件あれば10万件がメモリに乗る // LazyCollection: 1件ずつ処理 User::cursor()->each(function (User $user) { // ユーザーを1件ずつ処理 // メモリに保持するのは常に1件分だけ }); ``` ### LazyCollectionの作成 ```php theme={null} use Illuminate\Support\LazyCollection; // クロージャ(ジェネレーター)から作成 $lazy = LazyCollection::make(function () { $handle = fopen('large-file.csv', 'r'); while (($line = fgetcsv($handle)) !== false) { yield $line; } fclose($handle); }); // Eloquentの cursor() メソッドが LazyCollection を返す $lazy = User::where('is_active', true)->cursor(); ``` ### 大量データのバッチ処理 ```php theme={null} use App\Models\Order; // 全注文を1件ずつ処理(メモリ効率が良い) Order::cursor() ->filter(fn ($order) => $order->amount > 10000) ->each(fn ($order) => $order->sendConfirmationEmail()); // cursor() で全件を1件ずつフェッチしつつ filter/each でパイプライン処理 Order::where('status', 'completed') ->cursor() ->filter(fn ($order) => $order->amount > 10000) ->each(fn ($order) => $order->sendConfirmationEmail()); ``` `cursor()` はデータベースから1件ずつフェッチするため、大量のレコードを処理する際にメモリを大幅に節約できます。ただし、データベース接続は処理の完了まで維持されます。 ### `take` と `skip` でページング ```php theme={null} $lazy = LazyCollection::make(function () { foreach (range(1, 1000000) as $i) { yield $i; } }); // 最初の1000件をスキップして、次の100件を取得 $page = $lazy->skip(1000)->take(100)->values(); ``` ## まとめ | メソッド | 説明 | | - | - | | `collect($array)` | コレクションを作成する | | `map($callback)` | 各要素を変換する | | `filter($callback)` | 条件で絞り込む | | `reject($callback)` | 条件に合う要素を除外する | | `first($callback)` | 条件を満たす最初の要素を取得 | | `pluck($key)` | 特定キーの値を抜き出す | | `groupBy($key)` | 特定キーでグループ化 | | `sortBy($key)` | 昇順で並び替え | | `sortByDesc($key)` | 降順で並び替え | | `each($callback)` | 各要素に処理を実行(副作用) | | `flatMap($callback)` | マップ後にフラット化 | | `reduce($callback, $initial)` | 集計して1つの値にする | | `chunk($size)` | 指定サイズに分割 | | `values()` | インデックスを振り直す | | `sum($key)` | 合計値を計算 | | `count()` | 件数を取得 | コレクションは配列関数よりもはるかに可読性が高くなります。 ```php theme={null} // 配列関数を使った場合 $result = array_values(array_filter( array_map(fn ($u) => $u['name'], $users), fn ($name) => strlen($name) > 2 )); // コレクションを使った場合 $result = collect($users) ->pluck('name') ->filter(fn ($name) => strlen($name) > 2) ->values() ->all(); ``` * **通常のコレクション**: 数百〜数千件程度のデータ。シンプルで直感的。 * **LazyCollection**: 数万件以上の大量データ。メモリを節約したい場合。`cursor()` と組み合わせると効果的。 * Eloquentの `get()` は通常のコレクション、`cursor()` は LazyCollection を返します。 # 設定 Source: https://kawax.biz/jp/configuration Laravelの設定ファイル、環境変数(.env)、設定値へのアクセス方法、キャッシュ、デバッグモード、メンテナンスモードを解説します。 ## はじめに Laravelフレームワークの設定ファイルはすべて `config/` ディレクトリに格納されています。各オプションにはコメントが付いているので、ファイルを開いて利用可能なオプションを確認してみてください。 設定ファイルでは、データベース接続情報・メールサーバー情報・アプリケーションURLや暗号化キーなど、さまざまなコア設定値を管理します。 ```mermaid theme={null} flowchart LR A[".env ファイル"] -->|"env()"| B["config/ ファイル"] B -->|"config()"| C["アプリケーション"] ``` ### `about` コマンド `about` Artisanコマンドを使うと、アプリケーションの設定・ドライバー・環境の概要を確認できます。 ```shell theme={null} php artisan about ``` 特定のセクションだけを確認したい場合は `--only` オプションを使います。 ```shell theme={null} php artisan about --only=environment ``` 特定の設定ファイルの値を詳しく確認するには `config:show` コマンドが便利です。 ```shell theme={null} php artisan config:show database ``` ## 環境設定(.env) 実行環境によって設定値を切り替えたい場合がよくあります。たとえば、ローカルと本番でキャッシュドライバーを変えたいケースなどです。 Laravelはこれを簡単にするために [DotEnv](https://github.com/vlucas/phpdotenv) PHPライブラリを利用しています。新規インストール時、アプリケーションのルートディレクトリには一般的な環境変数が定義された `.env.example` ファイルが含まれており、インストールプロセス中に自動的に `.env` にコピーされます。 チームで開発する場合は、`.env.example` ファイルを更新しながらリポジトリに含めておくと便利です。プレースホルダー値を設定しておくことで、チームの他の開発者がどの環境変数が必要かを明確に把握できます。 ### 環境ファイルのセキュリティ `.env` ファイルはソースコントロールにコミットしないでください。開発者やサーバーごとに異なる設定が必要であり、リポジトリに含めるとインシデント発生時に機密情報が漏洩するリスクがあります。 ただし、Laravelには環境ファイルを暗号化する機能が組み込まれており、暗号化された状態であればソースコントロールに安全に保存できます。 ### 追加の環境ファイル アプリケーション起動時、Laravelは `APP_ENV` 環境変数またはCLIの `--env` 引数が指定されているかを確認します。指定されている場合、`.env.[APP_ENV]` ファイルが存在すればそれを読み込みます。存在しない場合はデフォルトの `.env` ファイルが使われます。 ### 環境変数の型 `.env` ファイルの変数はすべて文字列として解析されますが、`env()` 関数からより幅広い型を返せるよう、以下の特殊な値が用意されています。 | `.env` の値 | `env()` の返り値 | | - | - | | `true` | `(bool) true` | | `(true)` | `(bool) true` | | `false` | `(bool) false` | | `(false)` | `(bool) false` | | `empty` | `(string) ''` | | `(empty)` | `(string) ''` | | `null` | `(null) null` | | `(null)` | `(null) null` | スペースを含む値を設定するにはダブルクォートで囲みます。 ```ini theme={null} APP_NAME="My Application" ``` ### 環境変数の取得 `.env` ファイルに記述したすべての変数は、リクエストを受け取ったとき `$_ENV` PHPスーパーグローバルに読み込まれます。設定ファイル内では `env()` 関数を使って値を取得できます。 ```php theme={null} 'debug' => (bool) env('APP_DEBUG', false), ``` 第2引数はデフォルト値です。その環境変数が存在しない場合に返されます。 ### 現在の環境を確認する 現在の環境は `.env` ファイルの `APP_ENV` 変数で決まります。`App` ファサードの `environment` メソッドで取得できます。 ```php theme={null} use Illuminate\Support\Facades\App; $environment = App::environment(); ``` 引数を渡して特定の環境かどうかを確認することもできます。 ```php theme={null} if (App::environment('local')) { // ローカル環境 } if (App::environment(['local', 'staging'])) { // ローカルまたはステージング環境 } ``` ### 環境ファイルの暗号化 暗号化されていない環境ファイルはソースコントロールに保存すべきではありませんが、Laravelには環境ファイルを暗号化する機能があります。 #### 暗号化 環境ファイルを暗号化するには `env:encrypt` コマンドを使います。 ```shell theme={null} php artisan env:encrypt ``` 実行すると `.env` が暗号化され、暗号化された内容が `.env.encrypted` ファイルに保存されます。復号キーはコマンドの出力に表示されるので、安全なパスワードマネージャーに保存してください。 独自の暗号化キーを指定したい場合は `--key` オプションを使います。 ```shell theme={null} php artisan env:encrypt --key=3UVsEgGVK36XN82KKeyLFMhvosbZN1aF ``` 指定するキーの長さは、使用する暗号化方式が要求するキー長と一致させる必要があります。デフォルトでは32文字のキーが必要な `AES-256-CBC` が使われます。`--cipher` オプションを使えば、[暗号化](/jp/encryption)がサポートする任意の暗号方式を利用できます。 `.env` と `.env.staging` のように複数の環境ファイルがある場合は、`--env` オプションで暗号化対象の環境名を指定できます。 ```shell theme={null} php artisan env:encrypt --env=staging ``` #### 可読な変数名(Readable Variable Names) 環境ファイルを暗号化する際、`--readable` オプションを使うと、値だけを暗号化して変数名は読める状態のまま残せます。 ```shell theme={null} php artisan env:encrypt --readable ``` これにより、以下のような形式の暗号化ファイルが生成されます。 ```ini theme={null} APP_NAME=eyJpdiI6... APP_ENV=eyJpdiI6... APP_KEY=eyJpdiI6... APP_DEBUG=eyJpdiI6... APP_URL=eyJpdiI6... ``` 可読形式を使うことで、機密データを露出させずにどの環境変数が存在するかを確認できます。また、プルリクエストのレビュー時にも、ファイルを復号しなくてもどの変数が追加・削除・変更されたか把握しやすくなります。 環境ファイルを復号する際、Laravelは使用されている形式を自動的に判定するため、`env:decrypt` コマンドに追加のオプションは不要です。 `--readable` オプションを使用する場合、元の環境ファイルのコメントや空行は暗号化後の出力に含まれません。 #### 可読な暗号化ファイルの更新 可読な暗号化済み環境ファイルを更新する場合、Laravelは変更のない値をそのまま保持し、元の環境ファイルに存在しなくなった変数を削除します。 ```shell theme={null} php artisan env:encrypt --readable --key=3UVsEgGVK36XN82KKeyLFMhvosbZN1aF ``` 暗号化ファイルが存在しない場合は新規作成されます。既存のファイルを更新する場合は、作成時と同じ暗号化キー・暗号方式を指定してください。復号に失敗した場合、コマンドは既存ファイルを上書きせずに失敗します。 すべての値を強制的に再暗号化したい場合は `--force` オプションを使います。暗号化キーの変更や、無効になった暗号化ファイルの置き換えに利用できます。 ```shell theme={null} php artisan env:encrypt --readable --force --key=3UVsEgGVK36XN82KKeyLFMhvosbZN1aF ``` #### 復号 環境ファイルを復号するには `env:decrypt` コマンドを使います。このコマンドは復号キーを必要とし、Laravelは `LARAVEL_ENV_ENCRYPTION_KEY` 環境変数からキーを取得します。 ```shell theme={null} php artisan env:decrypt ``` キーを直接 `--key` オプションで渡すこともできます。 ```shell theme={null} php artisan env:decrypt --key=3UVsEgGVK36XN82KKeyLFMhvosbZN1aF ``` `env:decrypt` コマンドが実行されると、Laravelは `.env.encrypted` ファイルの内容を復号し、`.env` ファイルに書き出します。 カスタムの暗号方式を使う場合は `--cipher` オプションを指定します。 ```shell theme={null} php artisan env:decrypt --key=qUWuNRdfuImXcKxZ --cipher=AES-128-CBC ``` `.env` と `.env.staging` のように複数の環境ファイルがある場合は、`--env` オプションで復号対象の環境名を指定できます。 ```shell theme={null} php artisan env:decrypt --env=staging ``` 既存の環境ファイルを上書きするには `--force` オプションを指定します。 ```shell theme={null} php artisan env:decrypt --force ``` ## 設定値へのアクセス `Config` ファサードまたはグローバルの `config()` 関数を使って、アプリケーションのどこからでも設定値にアクセスできます。「ドット記法」を使い、ファイル名とオプション名を組み合わせて指定します。 ```php theme={null} use Illuminate\Support\Facades\Config; $value = Config::get('app.timezone'); // グローバル関数でも取得できる $value = config('app.timezone'); // 存在しない場合のデフォルト値を指定する $value = config('app.timezone', 'Asia/Tokyo'); ``` 実行時に設定値を変更するには `Config::set()` または配列を `config()` に渡します。 ```php theme={null} Config::set('app.timezone', 'America/Chicago'); config(['app.timezone' => 'America/Chicago']); ``` 型付きの取得メソッドも用意されており、型が一致しない場合は例外がスローされます。 ```php theme={null} Config::string('config-key'); Config::integer('config-key'); Config::float('config-key'); Config::boolean('config-key'); Config::array('config-key'); Config::collection('config-key'); ``` ## 設定のキャッシュ アプリケーションのパフォーマンスを向上させるには、すべての設定ファイルを1つのファイルにキャッシュします。 ```shell theme={null} php artisan config:cache ``` このコマンドはすべての設定オプションを1つのファイルにまとめ、フレームワークが素早く読み込めるようにします。 `config:cache` コマンドは本番環境のデプロイプロセスの一部として実行してください。ローカル開発中は設定が頻繁に変わるため、実行しないことを推奨します。 キャッシュが作成されると、`.env` ファイルはフレームワークのリクエストやArtisanコマンド実行時に読み込まれなくなります。そのため `env()` 関数はシステムレベルの環境変数のみを返します。 このため、`env()` 関数は必ず `config/` ディレクトリ内の設定ファイルからのみ呼び出すようにしてください。アプリケーション内の他の場所からは `config()` 関数を使って設定値を取得します。 キャッシュを削除するには `config:clear` コマンドを使います。 ```shell theme={null} php artisan config:clear ``` ### 設定ファイルの公開 Laravelの設定ファイルの多くはすでに `config/` ディレクトリに公開されていますが、`cors.php` や `view.php` など一部のファイルはデフォルトでは公開されていません。 未公開の設定ファイルを公開するには `config:publish` コマンドを使います。 ```shell theme={null} php artisan config:publish php artisan config:publish --all ``` ## デバッグモード `config/app.php` の `debug` オプションは、エラー情報をユーザーにどの程度表示するかを決定します。このオプションはデフォルトで `.env` ファイルの `APP_DEBUG` 環境変数の値に従います。 **本番環境では `APP_DEBUG` を必ず `false` に設定してください。** `true` のままにすると、アプリケーションの機密設定値がエンドユーザーに公開される危険があります。 ```ini theme={null} # ローカル開発 APP_DEBUG=true # 本番環境 APP_DEBUG=false ``` ## メンテナンスモード アプリケーションがメンテナンスモードのとき、すべてのリクエストに対してカスタムビューが表示されます。これにより、アップデート中や保守作業中にアプリケーションを一時的に「無効化」できます。 ### メンテナンスモードの有効化 ```shell theme={null} php artisan down ``` `--refresh` オプションを指定すると、ブラウザが指定秒数後に自動的にリロードされます。 ```shell theme={null} php artisan down --refresh=15 ``` `--retry` オプションは `Retry-After` HTTPヘッダーの値として設定されます。 ```shell theme={null} php artisan down --retry=60 ``` ### メンテナンスモードのバイパス シークレットトークンを使って特定のユーザーだけがアクセスできるようにするには `--secret` オプションを使います。 ```shell theme={null} php artisan down --secret="1630542a-246b-4b66-afa1-dd72a4c43515" ``` `--with-secret` オプションを使うとLaravelがトークンを自動生成します。 ```shell theme={null} php artisan down --with-secret ``` ### 複数サーバーでのメンテナンスモード デフォルトではLaravelはファイルベースでメンテナンスモードを管理します。複数サーバー構成の場合は、キャッシュベースの方法が便利です。 ```ini theme={null} APP_MAINTENANCE_DRIVER=cache APP_MAINTENANCE_STORE=database ``` ### メンテナンスビューの事前レンダリング デプロイ中に `php artisan down` を実行する場合、依存関係更新中にアクセスしたユーザーがエラーを目にすることがあります。これを防ぐには `--render` オプションでビューを事前レンダリングします。 ```shell theme={null} php artisan down --render="errors::503" ``` メンテナンスモード中にすべてのリクエストを特定のURLにリダイレクトすることもできます。 ```shell theme={null} php artisan down --redirect=/ ``` ### メンテナンスモードの無効化 ```shell theme={null} php artisan up ``` メンテナンスモードのデフォルトテンプレートは `resources/views/errors/503.blade.php` を作成することでカスタマイズできます。 ダウンタイムを完全になくしたい場合は、[Laravel Cloud](https://cloud.laravel.com) のようなフルマネージドプラットフォームの利用を検討してください。 ## 次のステップ URLとコントローラーを結びつけるルーティングの基礎を学びます。 # コンソールテスト Source: https://kawax.biz/jp/console-tests LaravelでArtisanコマンドをテストする方法(入力・出力の期待値、終了コード検証、コンソールイベント検証)を学びます。 # コンソールテスト Laravelでは、Artisanコマンドに対して入力・出力を含むテストを簡潔に記述できます。 このページはLaravel最新のコンソールテストAPIに合わせており、Laravel Promptsの検索入力を検証する `expectsSearch` も扱います。 ## はじめに `artisan` メソッドでコマンドを実行し、期待値をチェーンして検証します。 ```php theme={null} test('questionコマンド', function () { // ユーザー入力と出力を順番に検証する $this->artisan('question') ->expectsQuestion('What is your name?', 'Taylor Otwell') ->expectsQuestion('Which language do you prefer?', 'PHP') ->expectsOutput('Your name is Taylor Otwell and you prefer PHP.') ->doesntExpectOutput('Your name is Taylor Otwell and you prefer Ruby.') ->assertExitCode(0); }); ``` ```php theme={null} public function test_question_command(): void { // コマンドの対話フローを確認する $this->artisan('question') ->expectsQuestion('What is your name?', 'Taylor Otwell') ->expectsQuestion('Which language do you prefer?', 'PHP') ->expectsOutput('Your name is Taylor Otwell and you prefer PHP.') ->doesntExpectOutput('Your name is Taylor Otwell and you prefer Ruby.') ->assertExitCode(0); } ``` ```mermaid theme={null} flowchart TD A[artisanでコマンド実行] --> B[入力期待値を定義] B --> C[出力またはテーブルを検証] C --> D[終了ステータスを検証] ``` ## 成功 / 失敗のアサーション 終了ステータスを検証して、コマンドの成功・失敗を判定できます。 ```php theme={null} $this->artisan('inspire')->assertExitCode(0); $this->artisan('inspire')->assertSuccessful(); $this->artisan('inspire')->assertFailed(); ``` ## 入力 / 出力の期待値 ### 入力の期待値 質問入力や検索入力に対するユーザー操作をモックできます。 ```php theme={null} // 質問入力と検索入力の両方をモックする $this->artisan('example') ->expectsQuestion('What is your name?', 'Taylor Otwell') ->expectsSearch('What is your name?', search: 'Tay', answers: [ 'Taylor Otwell', 'Taylor Swift', 'Darian Taylor', ], answer: 'Taylor Otwell') ->assertExitCode(0); ``` ### 出力の期待値 出力文字列の完全一致・部分一致・テーブル表示を検証できます。 ```php theme={null} // 期待する出力のみが表示されることを確認する $this->artisan('users:all') ->expectsOutput('The expected output') ->doesntExpectOutput('Unexpected output') ->expectsOutputToContain('expected') ->expectsTable([ 'ID', 'Email', ], [ [1, 'taylor@example.com'], [2, 'abigail@example.com'], ]) ->assertExitCode(0); ``` ## 確認の期待値 Yes / No の確認プロンプトには `expectsConfirmation` を使います。 ```php theme={null} $this->artisan('module:import') ->expectsConfirmation('Do you really wish to run this command?', 'no') ->assertExitCode(1); ``` ## コンソールイベント デフォルトではテスト実行時に `CommandStarting` / `CommandFinished` は発火しません。 イベント検証が必要なテストクラスには `WithConsoleEvents` を追加します。 ```php theme={null} `WithConsoleEvents` は必要なテストに限定して使うと、通常のテスト速度を維持しやすくなります。 # Contracts(契約) Source: https://kawax.biz/jp/contracts LaravelのContractsの役割、ファサードとの違い、型ヒントによる注入方法、カスタムContractの作成方法を解説します。 ## Contractとは Laravelの「Contracts」は、フレームワークが提供するコアサービスを定義するインターフェースのセットです。例えば、`Illuminate\Contracts\Queue\Queue` Contractはジョブのキューイングに必要なメソッドを定義し、`Illuminate\Contracts\Mail\Mailer` Contractはメール送信に必要なメソッドを定義しています。 各Contractにはフレームワークが提供する対応する実装があります。例えば、Laravelはさまざまなドライバーのキュー実装と、[Symfony Mailer](https://symfony.com/doc/current/mailer.html)を使ったメーラー実装を提供しています。 すべてのLaravel Contractsは[専用のGitHubリポジトリ](https://github.com/illuminate/contracts)にあります。これはすべての利用可能なContractへのクイックリファレンスを提供し、Laravelサービスと連携するパッケージを構築する際に利用できる単一の分離されたパッケージです。 Contractはただのインターフェースです。PHPのインターフェースと全く同じ仕組みで動作します。Laravelはこのインターフェースへの実装を提供し、サービスコンテナを通じて注入します。 ## ContractとFacadeの違い [ファサード](/jp/facades)とヘルパー関数は、サービスコンテナからContractを型ヒントで解決する必要なく、Laravelのサービスを簡単に利用する方法を提供しています。ほとんどの場合、各ファサードには対応するContractがあります。 ファサードとContractの主な違いは次のとおりです。 | 観点 | ファサード | Contract | | - | - | - | | 依存の宣言 | 不要(どこからでも呼び出せる) | コンストラクターで明示的に宣言 | | テスト | `shouldReceive()` でモック | 標準的なモックライブラリで差し替え | | 主な用途 | アプリケーション内の手軽な使用 | パッケージ開発、明示的な依存管理 | | コードの可読性 | インポートが1行で済む | コンストラクターで依存が一目瞭然 | ファサードはクラスのコンストラクターで要求する必要がありませんが、Contractはクラスのコンストラクターで明示的な依存として定義できます。一部の開発者はこの明示的な依存の定義を好み、Contractを使います。他の開発者はファサードの利便性を好みます。**一般的に、ほとんどのアプリケーションは開発中にファサードを問題なく使えます。** ## Contractをいつ使うか ContractとFacadeのどちらを使うかは、個人の好みや開発チームの好みによります。ContractとFacadeはどちらも堅牢でテストしやすいLaravelアプリケーションを作成するために使えます。ContractとFacadeは相互に排他的ではありません。アプリケーションの一部ではFacadeを使い、他の部分ではContractに依存することも可能です。 特にContractが有用な場面は次のとおりです。 * **複数のPHPフレームワークと連携するパッケージを構築する場合** — `illuminate/contracts` パッケージを使ってLaravelサービスとの連携を定義することで、`composer.json` にLaravelの具体的な実装を要求せずに済みます。 * **依存を明示的にしたい場合** — コンストラクターを見るだけでクラスが何に依存しているか一目でわかります。 * **実装を差し替えたい場合** — サービスコンテナを通じて別の実装に差し替えることが容易になります。 ## Contractの使い方 Contractの実装を取得するにはどうすればよいでしょうか。実はとても簡単です。 コントローラー、イベントリスナー、ミドルウェア、キュージョブ、ルートクロージャなど、Laravelの多くの種類のクラスはサービスコンテナを通じて解決されます。そのため、Contractの実装を取得するには、解決されているクラスのコンストラクターでインターフェースを「型ヒント」するだけです。 ```mermaid theme={null} flowchart LR A["サービスプロバイダ
    bind(Interface, Impl)"] --> B["サービスコンテナ
    バインディング登録"] C["コンストラクター
    型ヒント: Interface"] --> B B --> D["自動注入
    Interfaceの実装を解決"] D --> E["具体クラスのインスタンス
    差し替え自由"] ``` 例えば、次のイベントリスナーを見てください。 ```php theme={null} `app/Contracts` ディレクトリにインターフェースを作成します。 ```php theme={null} Contractを実装するクラスを作成します。 ```php theme={null} [サービスプロバイダ](/jp/service-providers)でContractと実装をバインドします。 ```php theme={null} use App\Contracts\PaymentGateway; use App\Services\StripePaymentGateway; $this->app->singleton(PaymentGateway::class, StripePaymentGateway::class); ``` コントローラーや他のクラスのコンストラクターで型ヒントします。 ```php theme={null} payment->charge( $request->amount, $request->payment_token ); // ... } } ``` このパターンにより、決済サービスを `Stripe` から別のプロバイダに切り替える場合も、バインディングを1か所変更するだけで対応でき、コントローラーのコードは変更不要です。 ## 主要なContract一覧 よく使うContractとその対応するファサードの対応表です(一部抜粋)。 | Contract | 対応するファサード | | - | - | | `Illuminate\Contracts\Auth\Access\Gate` | `Gate` | | `Illuminate\Contracts\Auth\Factory` | `Auth` | | `Illuminate\Contracts\Bus\Dispatcher` | `Bus` | | `Illuminate\Contracts\Cache\Factory` | `Cache` | | `Illuminate\Contracts\Cache\Repository` | `Cache::driver()` | | `Illuminate\Contracts\Config\Repository` | `Config` | | `Illuminate\Contracts\Console\Kernel` | `Artisan` | | `Illuminate\Contracts\Container\Container` | `App` | | `Illuminate\Contracts\Encryption\Encrypter` | `Crypt` | | `Illuminate\Contracts\Events\Dispatcher` | `Event` | | `Illuminate\Contracts\Filesystem\Factory` | `Storage` | | `Illuminate\Contracts\Filesystem\Filesystem` | `Storage::disk()` | | `Illuminate\Contracts\Hashing\Hasher` | `Hash` | | `Illuminate\Contracts\Mail\Mailer` | `Mail` | | `Illuminate\Contracts\Notifications\Dispatcher` | `Notification` | | `Illuminate\Contracts\Queue\Factory` | `Queue` | | `Illuminate\Contracts\Queue\Queue` | `Queue::connection()` | | `Illuminate\Contracts\Queue\ShouldQueue` | — | | `Illuminate\Contracts\Redis\Factory` | `Redis` | | `Illuminate\Contracts\Routing\ResponseFactory` | `Response` | | `Illuminate\Contracts\Routing\UrlGenerator` | `URL` | | `Illuminate\Contracts\Session\Session` | `Session::driver()` | | `Illuminate\Contracts\Translation\Translator` | `Lang` | | `Illuminate\Contracts\Validation\Factory` | `Validator` | | `Illuminate\Contracts\View\Factory` | `View` | すべてのContractの一覧は[illuminate/contracts](https://github.com/illuminate/contracts)リポジトリで確認できます。 ## 次のステップ ファサードの仕組みとテスト方法を確認します。 # コントローラー Source: https://kawax.biz/jp/controllers Laravelのコントローラーの作成方法とルーティングとの連携を解説します。 ## コントローラーとは ルートファイルにすべてのリクエスト処理ロジックをクロージャとして定義する代わりに、「コントローラー」クラスを使ってこの動作を整理できます。 コントローラーは関連するリクエスト処理ロジックを1つのクラスにまとめられます。 例えば `UserController` クラスは、ユーザーの表示・作成・更新・削除など、ユーザーに関するすべての受信リクエストを処理できます。 デフォルトでは、コントローラーは `app/Http/Controllers` ディレクトリに格納されます。 ## コントローラーの作成 新しいコントローラーを素早く生成するには、`make:controller` Artisanコマンドを使います。 アプリケーションのすべてのコントローラーは、デフォルトで `app/Http/Controllers` ディレクトリに格納されます。 ```shell theme={null} php artisan make:controller UserController ``` ## 基本的なコントローラー コントローラークラスは、受信するHTTPリクエストに応答するいくつかのpublicメソッドを持てます。 ```php theme={null} User::findOrFail($id) ]); } } ``` コントローラークラスとメソッドを作成したら、次のようにコントローラーメソッドへのルートを定義できます。 ```php theme={null} use App\Http\Controllers\UserController; Route::get('/user/{id}', [UserController::class, 'show']); ``` 受信リクエストが指定されたルートURIに一致すると、`App\Http\Controllers\UserController` クラスの `show` メソッドが呼び出され、ルートパラメータがメソッドに渡されます。 コントローラーはベースクラスを継承する必要はありません。ただし、すべてのコントローラーで共有すべきメソッドを持つベースコントローラークラスを継承すると便利な場合があります。 ## シングルアクションコントローラー コントローラーのアクションが特に複雑な場合、コントローラークラス全体をその単一のアクションに専念させると便利です。 これを実現するには、コントローラー内に単一の `__invoke` メソッドを定義します。 ```php theme={null} `php artisan route:list` コマンドを実行すると、アプリケーションのルートの概要を素早く確認できます。 ### 部分的なリソースルート リソースルートを宣言する際に、コントローラーが処理すべきアクションのサブセットを指定できます。 ```php theme={null} use App\Http\Controllers\PhotoController; Route::resource('photos', PhotoController::class)->only([ 'index', 'show' ]); Route::resource('photos', PhotoController::class)->except([ 'create', 'store', 'update', 'destroy' ]); ``` ### APIリソースルート APIで使用するリソースルートを宣言する場合、`create` や `edit` などHTMLテンプレートを表示するルートを除外したいことがよくあります。 `apiResource` メソッドを使うと、これら2つのルートを自動的に除外できます。 ```php theme={null} use App\Http\Controllers\PhotoController; Route::apiResource('photos', PhotoController::class); ``` ## ミドルウェア属性 PHP Attribute を使ってコントローラーにミドルウェアを割り当てることもできます。 ```php theme={null} name; // ユーザーを保存する処理... return redirect('/users'); } } ``` コントローラーメソッドがルートパラメータからの入力も受け取る場合は、他の依存の後にルート引数をリストします。 例えば、ルートが次のように定義されている場合を考えます。 ```php theme={null} use App\Http\Controllers\UserController; Route::put('/user/{id}', [UserController::class, 'update']); ``` 次のようにコントローラーメソッドを定義することで、`Illuminate\Http\Request` をタイプヒントしながら `id` パラメータにもアクセスできます。 ```php theme={null} 依存性の注入を活用することで、テスト時にモックを差し込みやすくなり、コードのテスタビリティが向上します。 ## 次のステップ ルートの定義方法やコントローラーとの連携を振り返ります。 # CSRFプロテクション Source: https://kawax.biz/jp/csrf CSRF攻撃の仕組みと、Laravel 13のOrigin検証・トークン検証による防御方法を解説します。 ## イントロダクション CSRF(Cross-Site Request Forgery)は、ログイン済みユーザーになりすまして意図しないリクエストを送らせる攻撃です。 例えば、あなたのアプリに `POST /user/email` があり、メールアドレス変更を受け付けているとします。攻撃者が別サイトでこのURLに自動送信するフォームを仕込むと、ユーザーが気づかないうちにメールアドレスが変更される可能性があります。 Laravel 13では、`web` ミドルウェアグループに含まれる仕組みにより、CSRF保護がデフォルトで有効です。 ## CSRF攻撃の防止 Laravelの `PreventRequestForgery` ミドルウェアは、次の2層でCSRFを防ぎます。 1. **Origin検証**(`Sec-Fetch-Site` ヘッダー) 2. **トークン検証**(セッション単位のCSRFトークン) まずOriginを確認し、判定できない場合や失敗した場合はトークン検証にフォールバックします。 ## Origin検証 Laravelは最初に `Sec-Fetch-Site` を確認し、同一Originからのリクエストかどうかを判定します。これはHTTPS環境で特に有効です。 Origin検証が通れば、その時点でリクエストは許可されます。通らない場合は、従来どおりCSRFトークン検証が実行されます。 `Sec-Fetch-Site` はHTTPS接続で利用されるのが前提です。HTTP環境ではOrigin検証が機能せず、トークン検証が主な防御になります。 ### Origin-onlyモード トークン検証へのフォールバックを無効化し、Origin検証のみで判定することもできます。 ```php theme={null} use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Middleware; return Application::configure(basePath: dirname(__DIR__)) ->withMiddleware(function (Middleware $middleware): void { $middleware->preventRequestForgery(originOnly: true); }); ``` Origin-onlyモードでは、Origin検証に失敗したリクエストは `419` ではなく `403` を返します。 サブドメイン間リクエストなどでsame-siteを許可したい場合は、`allowSameSite` を設定します。 ```php theme={null} $middleware->preventRequestForgery(allowSameSite: true); ``` ## トークン検証 LaravelはセッションごとにCSRFトークンを生成します。`csrf_token()` またはセッションから取得できます。 ```php theme={null} use Illuminate\Http\Request; Route::get('/token', function (Request $request) { $tokenFromSession = $request->session()->token(); $tokenFromHelper = csrf_token(); }); ``` `web` ルートで `POST`・`PUT`・`PATCH`・`DELETE` のフォームを作る場合は、必ず `@csrf` を含めます。 ```blade theme={null}
    @csrf
    ``` ## URIの除外 StripeのWebhookのように外部サービスから送られるリクエストでは、特定URIをCSRF保護から除外することがあります。 ```php theme={null} use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Middleware; return Application::configure(basePath: dirname(__DIR__)) ->withMiddleware(function (Middleware $middleware): void { $middleware->preventRequestForgery(except: [ 'stripe/*', 'http://example.com/foo/bar', 'http://example.com/foo/*', ]); }); ``` 可能であればWebhookルートは `web` ミドルウェアグループの外に配置し、除外設定は最小限にしてください。 ## X-CSRF-TOKEN ヘッダー Laravelはフォームの `_token` だけでなく、`X-CSRF-TOKEN` ヘッダーも検証します。 まずトークンをmetaタグに出力します。 ```blade theme={null} ``` その値をAJAXリクエストヘッダーに付与します。 ```js theme={null} $.ajaxSetup({ headers: { 'X-CSRF-TOKEN': document .querySelector('meta[name="csrf-token"]') .getAttribute('content'), }, }); ``` ## X-XSRF-TOKEN ヘッダー Laravelは暗号化された `XSRF-TOKEN` Cookieも送信します。AxiosやAngularは、同一Originリクエスト時にこの値を `X-XSRF-TOKEN` ヘッダーへ自動設定できます。 そのため、SPAやAJAXの実装ではヘッダー設定を手動で書かなくてもCSRF対策が有効になるケースがあります。 ## SPAでの考慮事項 SPAでLaravelをAPIバックエンドとして使う場合、まずCSRF Cookieを取得してからログインリクエストを送ります。 ```js theme={null} await axios.get('/sanctum/csrf-cookie'); await axios.post('/login', { email: 'user@example.com', password: 'password', }); ``` 詳細は [Sanctum](/jp/sanctum) を参照してください。 # データベース設定 Source: https://kawax.biz/jp/database Laravelのデータベース接続設定、read/write分離、複数接続、SQL実行、クエリリスニング、transactionの基本を解説します。 ## はじめに Laravelは次のデータベースを公式にサポートしています。 * MySQL / MariaDB * PostgreSQL * SQLite * SQL Server あなたは生SQL、Query Builder、Eloquent ORMのどれでも同じ接続設定を使えます。 このページはデータベース接続の前提を扱います。実践的なクエリは[Query Builder](/jp/query-builder)、スキーマ管理は[Migrations](/jp/migrations)、初期データ投入は[Seeding](/jp/seeding)を参照してください。 ## 設定 データベース設定は `config/database.php` に集約されています。 あなたは `default` で既定の接続名を選び、`connections` で接続ごとの詳細を定義します。 ```mermaid theme={null} flowchart TD A[".env"] --> B["config/database.php"] B --> C["default connection"] B --> D["connections.mysql"] B --> E["connections.pgsql"] B --> F["connections.sqlite"] C --> G["DB facade / Query Builder / Eloquent"] ``` ```php theme={null} // config/database.php 'default' => env('DB_CONNECTION', 'sqlite'), 'connections' => [ 'mysql' => [ 'driver' => 'mysql', 'host' => env('DB_HOST', '127.0.0.1'), 'port' => env('DB_PORT', '3306'), 'database' => env('DB_DATABASE', 'laravel'), 'username' => env('DB_USERNAME', 'root'), 'password' => env('DB_PASSWORD', ''), ], ], ``` `.env` では少なくとも次を設定してください。 ```ini theme={null} DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=app DB_USERNAME=app_user DB_PASSWORD=secret ``` SQLiteを使う場合は `DB_CONNECTION=sqlite` と `DB_DATABASE` のパスを設定します。 ## 読み取り・書き込み接続の分離 読み取り(`SELECT`)と書き込み(`INSERT` / `UPDATE` / `DELETE`)を別ホストに分離したい場合、同じ接続内で `read` と `write` を設定してください。 ```php theme={null} 'mysql' => [ 'driver' => 'mysql', 'read' => [ 'host' => ['10.0.0.10', '10.0.0.11'], ], 'write' => [ 'host' => ['10.0.0.20'], ], 'sticky' => true, 'port' => env('DB_PORT', '3306'), 'database' => env('DB_DATABASE', 'laravel'), 'username' => env('DB_USERNAME', 'root'), 'password' => env('DB_PASSWORD', ''), ], ``` `sticky` を `true` にすると、同一リクエスト内で書き込み後の読み取りがwrite接続に固定されます。 レプリカ遅延がある構成では `sticky` を有効にすると、書き込み直後に古いデータを読むリスクを下げられます。 ## Pooled PostgreSQL 接続 PgBouncer のようなトランザクションモード接続プーリングを提供するマネージド PostgreSQL サービスを使う場合、`pooled` オプションと `direct` オプションを設定してください。 ```php theme={null} 'pgsql' => [ 'driver' => 'pgsql', // ... 'pooled' => env('DB_POOLED', false), 'direct' => array_filter([ 'host' => env('DB_DIRECT_HOST'), 'port' => env('DB_DIRECT_PORT'), 'username' => env('DB_DIRECT_USERNAME'), 'password' => env('DB_DIRECT_PASSWORD'), 'sslmode' => env('DB_DIRECT_SSLMODE'), ]), ], ``` `pooled` を有効にすると、Laravel は自動的に振り分けを行います。 | 操作 | 使用される接続 | | - | - | | アプリケーションクエリ | pooled(エミュレートプリペアが自動有効化) | | マイグレーション / `db:wipe` / `db:show` / `db:table` | direct(自動) | | `php artisan db` | direct(デフォルト)、`--pooled` で pooled に切替 | アプリケーションコード内で明示的に direct 接続を使いたい場合は、接続名に `::direct` サフィックスを付けてください。 ```php theme={null} DB::connection('pgsql::direct')->statement('create extension if not exists "uuid-ossp"'); ``` ## 複数データベース接続 あなたは `config/database.php` に接続を複数定義し、`DB::connection()` で切り替えられます。 ```php theme={null} use Illuminate\Support\Facades\DB; $users = DB::connection('sqlite')->select('select * from users'); $pdo = DB::connection('pgsql')->getPdo(); ``` ```mermaid theme={null} flowchart LR A["DB::connection('mysql')"] --> B["Primary DB"] C["DB::connection('pgsql')"] --> D["Analytics DB"] E["DB::connection('sqlite')"] --> F["Local file DB"] ``` ## SQLクエリの実行 `DB` facadeはクエリ種別ごとに専用メソッドを持ちます。 ```php theme={null} use Illuminate\Support\Facades\DB; $users = DB::select('select * from users where active = ?', [1]); DB::insert('insert into users (name, email) values (?, ?)', ['Taylor', 'taylor@example.com']); $affected = DB::update('update users set votes = 100 where name = ?', ['Taylor']); $deleted = DB::delete('delete from sessions where user_id = ?', [1]); DB::statement('drop table temporary_imports'); ``` ユーザー入力をSQL文字列に直接連結しないでください。必ずバインディング引数を使ってSQL injectionを防いでください。 ## クエリリスニング 実行されたSQLを追跡したいときは、Service Providerの `boot()` で `DB::listen()` を登録してください。 ```php theme={null} use Illuminate\Database\Events\QueryExecuted; use Illuminate\Support\Facades\DB; public function boot(): void { DB::listen(function (QueryExecuted $query) { logger()->debug('SQL executed', [ 'sql' => $query->toRawSql(), 'time_ms' => $query->time, ]); }); } ``` ## transaction 複数の更新を1つの単位として扱うなら `DB::transaction()` を使ってください。 例外が発生するとLaravelが自動でロールバックします。 ```php theme={null} use Illuminate\Support\Facades\DB; DB::transaction(function () { DB::update('update users set votes = 1'); DB::delete('delete from posts where archived = 1'); }); ``` デッドロック再試行が必要なら `attempts` を指定できます。 ```php theme={null} DB::transaction(function () { // ... }, attempts: 5); ``` 手動で制御したい場合は `beginTransaction` / `rollBack` / `commit` を使ってください。 ```php theme={null} DB::beginTransaction(); try { DB::update('update accounts set balance = balance - 100 where id = ?', [1]); DB::update('update accounts set balance = balance + 100 where id = ?', [2]); DB::commit(); } catch (\Throwable $e) { DB::rollBack(); throw $e; } ``` ## 次のステップ 接続設定を使って安全にクエリを組み立てる方法を学びます。 接続したデータベースのスキーマをバージョン管理する方法を学びます。 開発・テスト用データを再現可能な形で投入する方法を学びます。 # データベーステスト Source: https://kawax.biz/jp/database-testing RefreshDatabase・シーダー・ファクトリ・専用アサーションでLaravelのDBテストを効率よく書く方法 ## はじめに Laravelには、データベースを使う機能を安全かつ高速に検証するための仕組みが標準で用意されています。データベースリセット用トレイト、モデルファクトリ、シーダー、専用アサーションを組み合わせることで、壊れにくいテストを作成できます。 まずは `RefreshDatabase` を使うのが基本です。多くのケースで、完全リセットより高速にテストの独立性を保てます。 ## 各テスト後のデータベースリセット 前のテストデータが次のテストに影響しないように、Laravelのデータベースリセット用トレイトを利用します。 ```php Pest theme={null} get('/'); $response->assertOk(); }); ``` ```php PHPUnit theme={null} get('/'); $response->assertOk(); } } ``` `RefreshDatabase` は、スキーマが最新かどうかを判定します。最新ならトランザクションでテストを実行し、最新でなければ先にマイグレーションを実行します。 ```mermaid theme={null} flowchart TD A[RefreshDatabaseを使うテスト開始] --> B{スキーマは最新か?} B -- はい --> C[DBトランザクションで実行] B -- いいえ --> D[マイグレーション実行] D --> E[テスト実行] C --> E[テスト実行] E --> F[ロールバック / 状態リセット] ``` トランザクション方式ではなく、毎回完全にリセットしたい場合は次のトレイトを使います。 | トレイト | 挙動 | 使いどころ | | - | - | - | | `RefreshDatabase` | スキーマが最新ならトランザクション、必要時のみマイグレーション | 通常はこれを第一候補にする | | `DatabaseMigrations` | 各テストごとにマイグレーションを実行 | 毎回マイグレーションのライフサイクルを通したい場合 | | `DatabaseTruncation` | テーブルをTRUNCATEして初期化 | トランザクションが適さない環境で全件クリアしたい場合 | ## モデルファクトリ テスト前にデータを準備する際、モデルファクトリを使うとEloquentモデルを簡潔に生成できます。 ファクトリ定義・state・リレーションの詳細は [Eloquentファクトリ](/jp/eloquent-factories) を参照してください。 ```php Pest theme={null} create(); expect($user->exists)->toBeTrue(); }); ``` ```php PHPUnit theme={null} create(); $this->assertTrue($user->exists); } } ``` ## シーダーの実行 テスト中にデータを投入したい場合は `seed()` を使います。引数なしなら `DatabaseSeeder`、引数ありなら特定シーダー(または配列)を実行できます。 ```php Pest theme={null} seed(); $this->seed(OrderStatusSeeder::class); $this->seed([ OrderStatusSeeder::class, TransactionStatusSeeder::class, ]); // ... }); ``` ```php PHPUnit theme={null} seed(); $this->seed(OrderStatusSeeder::class); $this->seed([ OrderStatusSeeder::class, TransactionStatusSeeder::class, ]); // ... } } ``` `RefreshDatabase` を使う全テストで毎回自動シードしたい場合は、基底のテストクラスに `#[Seed]` 属性を付けます。 ```php theme={null} ```php Pest theme={null} $this->assertDatabaseCount('users', 5); ``` ```php PHPUnit theme={null} $this->assertDatabaseCount('users', 5); ``` ### `assertDatabaseEmpty` テーブルが空であることを確認します。 ```php Pest theme={null} $this->assertDatabaseEmpty('users'); ``` ```php PHPUnit theme={null} $this->assertDatabaseEmpty('users'); ``` ### `assertDatabaseHas` 指定した条件に一致するレコードが存在することを確認します。 ```php Pest theme={null} $this->assertDatabaseHas('users', [ 'email' => 'sally@example.com', ]); ``` ```php PHPUnit theme={null} $this->assertDatabaseHas('users', [ 'email' => 'sally@example.com', ]); ``` ### `assertDatabaseMissing` 指定した条件に一致するレコードが存在しないことを確認します。 ```php Pest theme={null} $this->assertDatabaseMissing('users', [ 'email' => 'sally@example.com', ]); ``` ```php PHPUnit theme={null} $this->assertDatabaseMissing('users', [ 'email' => 'sally@example.com', ]); ``` ### `assertSoftDeleted` 対象のEloquentモデルがソフトデリート済みであることを確認します。 ```php Pest theme={null} $this->assertSoftDeleted($user); ``` ```php PHPUnit theme={null} $this->assertSoftDeleted($user); ``` ### `assertNotSoftDeleted` 対象のEloquentモデルがソフトデリートされていないことを確認します。 ```php Pest theme={null} $this->assertNotSoftDeleted($user); ``` ```php PHPUnit theme={null} $this->assertNotSoftDeleted($user); ``` ### `assertModelExists` 指定したモデル(またはモデルコレクション)がDB上に存在することを確認します。 ```php Pest theme={null} create(); $this->assertModelExists($user); ``` ```php PHPUnit theme={null} create(); $this->assertModelExists($user); ``` ### `assertModelMissing` 指定したモデル(またはモデルコレクション)がDB上に存在しないことを確認します。 ```php Pest theme={null} create(); $user->delete(); $this->assertModelMissing($user); ``` ```php PHPUnit theme={null} create(); $user->delete(); $this->assertModelMissing($user); ``` ### `expectsDatabaseQueryCount` テスト冒頭で、実行されるデータベースクエリの総数を厳密に指定します。実際の実行回数が一致しない場合は失敗します。 ```php Pest theme={null} $this->expectsDatabaseQueryCount(5); // Test... ``` ```php PHPUnit theme={null} $this->expectsDatabaseQueryCount(5); // Test... ``` # デプロイメント Source: https://kawax.biz/jp/deployment Laravelアプリケーションを本番環境にデプロイするための設定・最適化・運用のガイド ## はじめに Laravelアプリケーションを本番環境にデプロイするときは、できるだけ効率よく動作するよう準備が必要です。 このガイドでは、本番デプロイを確実に行うための重要なポイントを解説します。 ## デプロイフロー ```mermaid theme={null} flowchart TD A["コードをプッシュ"] --> B["php artisan optimize"] B --> C["Nginx / FrankenPHP で配信"] C --> D["php artisan reload"] D --> E["キューワーカー
    Reverb / Octane 再起動"] E --> F["デプロイ完了"] ``` ## サーバー要件 Laravelフレームワークには以下のシステム要件があります。 **PHP 8.3 以上**と、下記のPHP拡張モジュールが必要です。 | 拡張モジュール | 説明 | | - | - | | Ctype | 文字型チェック | | cURL | HTTP通信 | | DOM | XML/HTMLのDOM操作 | | Fileinfo | MIMEタイプ検出 | | Filter | データのフィルタリング | | Hash | ハッシュ関数 | | Mbstring | マルチバイト文字列処理 | | OpenSSL | 暗号化 | | PCRE | 正規表現 | | PDO | データベース接続 | | Session | セッション管理 | | Tokenizer | PHPトークン解析 | | XML | XML処理 | ## サーバー設定 ### Nginx Nginxを使用している場合は、以下の設定ファイルをベースにしてください。 **すべてのリクエストを `public/index.php` に転送する**ことが重要です。 `index.php` をプロジェクトルートに移動しないでください。機密設定ファイルが外部に公開されてしまいます。 ```nginx theme={null} server { listen 80; listen [::]:80; server_name example.com; root /srv/example.com/public; add_header X-Frame-Options "SAMEORIGIN"; add_header X-Content-Type-Options "nosniff"; index index.php; charset utf-8; location / { try_files $uri $uri/ /index.php?$query_string; } location = /favicon.ico { access_log off; log_not_found off; } location = /robots.txt { access_log off; log_not_found off; } error_page 404 /index.php; location ~ ^/index\.php(/|$) { fastcgi_pass unix:/var/run/php/php8.3-fpm.sock; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; include fastcgi_params; fastcgi_buffer_size 32k; fastcgi_buffers 8 32k; fastcgi_busy_buffers_size 64k; fastcgi_hide_header X-Powered-By; } location ~ /\.(?!well-known).* { deny all; } } ``` ### FrankenPHP [FrankenPHP](https://frankenphp.dev/) はGoで書かれたモダンなPHPアプリケーションサーバーです。 以下のコマンドだけでLaravelアプリケーションを起動できます。 ```shell theme={null} frankenphp php-server -r public/ ``` HTTP/3、モダンな圧縮、[Laravel Octane](https://laravel.com/docs/octane) 連携、スタンドアロンバイナリ化などの高度な機能は [FrankenPHPのLaravelドキュメント](https://frankenphp.dev/docs/laravel/) を参照してください。 ### ディレクトリ権限 Laravelは `bootstrap/cache` と `storage` ディレクトリへの書き込みが必要です。 Webサーバーのプロセスオーナーがこれらのディレクトリに書き込めるよう権限を設定してください。 ```shell theme={null} chmod -R 775 storage bootstrap/cache chown -R www-data:www-data storage bootstrap/cache ``` ## 最適化 本番環境へのデプロイ時には、設定・イベント・ルート・ビューをキャッシュしてパフォーマンスを向上させます。 `optimize` コマンドで一括キャッシュできます。 ```shell theme={null} php artisan optimize ``` キャッシュを削除するには `optimize:clear` を使います。 ```shell theme={null} php artisan optimize:clear ``` ### 個別の最適化コマンド `optimize` は以下のコマンドをまとめて実行します。必要に応じて個別に実行することも可能です。 | コマンド | 説明 | | - | - | | `php artisan config:cache` | 設定ファイルを1ファイルにまとめてキャッシュする | | `php artisan event:cache` | イベント→リスナーのマッピングをキャッシュする | | `php artisan route:cache` | ルート定義をキャッシュしてルート登録を高速化する | | `php artisan view:cache` | Bladeビューをプリコンパイルしてリクエストを高速化する | `config:cache` を実行した後は、設定ファイル内でのみ `env()` 関数を呼び出してください。 キャッシュ後は `.env` ファイルが読み込まれなくなるため、設定ファイル以外で `env()` を呼ぶと `null` が返ります。 ## サービスのリロード 新しいバージョンをデプロイした後、キューワーカー・Laravel Reverb・Laravel Octane などの長時間動作するサービスは、新しいコードを使うために再起動が必要です。 ```shell theme={null} php artisan reload ``` このコマンドはリロード可能なサービスを終了させます。 プロセスモニター(Supervisorなど)が自動的に再起動するよう設定しておいてください。 Laravel Cloudを使用している場合、すべてのサービスのグレースフルリロードは自動的に処理されるため、`reload` コマンドは不要です。 ## デバッグモード `config/app.php` の `debug` オプションは、エラー情報をユーザーにどれだけ表示するかを制御します。 デフォルトでは `.env` ファイルの `APP_DEBUG` 環境変数の値が使われます。 **本番環境では `APP_DEBUG` を必ず `false` にしてください。** `APP_DEBUG=true` のまま本番環境で動作させると、データベース接続情報や秘密鍵などの機密設定値がエンドユーザーに露出するリスクがあります。 ```ini theme={null} # .env(本番環境) APP_DEBUG=false ``` ## ヘルスチェックルート Laravelにはアプリケーションの状態を監視するためのヘルスチェックルートが組み込まれています。 アップタイムモニター・ロードバランサー・Kubernetesなどのオーケストレーションシステムと連携できます。 デフォルトでは `/up` エンドポイントが用意されており、アプリケーションが正常に起動していれば `200`、起動時に例外が発生していれば `500` を返します。 `bootstrap/app.php` でURIをカスタマイズできます。 ```php theme={null} ->withRouting( web: __DIR__.'/../routes/web.php', commands: __DIR__.'/../routes/console.php', health: '/status', // デフォルトは /up ) ``` このルートへのリクエスト時に `Illuminate\Foundation\Events\DiagnosingHealth` イベントが発行されるため、 データベースやキャッシュの追加チェックをリスナーで実装できます。 ## Laravel CloudまたはForgeでデプロイする ### Laravel Cloud 完全マネージドの自動スケールデプロイプラットフォームをお探しなら、[Laravel Cloud](https://cloud.laravel.com) がおすすめです。 マネージドコンピュート・データベース・キャッシュ・オブジェクトストレージを提供する、Laravelに最適化されたPaaSです。 Laravelの開発チームが直接チューニングしており、フレームワークとシームレスに連携します。 ### Laravel Forge 自分でサーバーを管理したいけれど、NginxやMySQLなどの各種サービスのセットアップには手間をかけたくない場合は、[Laravel Forge](https://forge.laravel.com) が便利です。 DigitalOcean・Linode・AWSなど主要なクラウドプロバイダーにサーバーを作成し、Nginx・MySQL・Redis・Memcached・Beanstalkなどのツールを自動的にインストール・管理します。 ## 次のステップ 公式ドキュメントでは最新のデプロイ設定の詳細を確認できます。 # ディレクトリ構造 Source: https://kawax.biz/jp/directory-structure Laravelプロジェクトの主要なディレクトリとファイルの役割を解説します。 ## はじめに Laravelのデフォルトのアプリケーション構造は、大規模・小規模どちらのアプリケーションにも適した出発点を提供します。 ただし、アプリケーションをどのように構成するかは自由です。 Laravelはクラスの配置場所にほとんど制限を設けていません。Composerがクラスをオートロードできさえすれば問題ありません。 ## ルートディレクトリ 新しいLaravelプロジェクトを作成すると、以下のようなディレクトリ構造が生成されます。 ``` example-app/ ├── app/ ├── bootstrap/ ├── config/ ├── database/ ├── public/ ├── resources/ ├── routes/ ├── storage/ ├── tests/ ├── vendor/ ├── .env ├── artisan └── composer.json ``` ### `app/` ディレクトリ アプリケーションのコアコードを格納するディレクトリです。 コントローラー・モデル・ミドルウェアなど、アプリケーションのほぼすべてのクラスがここに置かれます。 デフォルトでは `Http`・`Models`・`Providers` サブディレクトリが含まれています。 Artisanコマンドでクラスを生成すると、他のサブディレクトリも自動的に作成されます。 ### `bootstrap/` ディレクトリ フレームワークを起動する `app.php` ファイルを含みます。 また、ルートキャッシュやサービスキャッシュなどのパフォーマンス最適化ファイルを格納する `cache/` ディレクトリも含まれます。 ### `config/` ディレクトリ アプリケーションのすべての設定ファイルを格納します。 これらのファイルを読んで、利用可能なオプションを把握しておくことを推奨します。 ### `database/` ディレクトリ データベースのマイグレーション・モデルファクトリー・シーダーを格納します。 SQLiteデータベースファイルもここに配置できます。 ### `public/` ディレクトリ アプリケーションへのすべてのリクエストのエントリーポイントである `index.php` を含みます。 また、画像・JavaScript・CSSなどのアセットもここに格納されます。 WebサーバーのドキュメントルートはLaravelプロジェクトの `public/` ディレクトリに設定してください。 プロジェクトルートやサブディレクトリを直接公開しようとすると、機密ファイルが露出するリスクがあります。 ### `resources/` ディレクトリ [ビュー](/jp/views)と、コンパイル前のCSSやJavaScriptなどの生アセットを格納します。 ### `routes/` ディレクトリ アプリケーションのすべてのルート定義を格納します。 デフォルトでは `web.php` と `console.php` の2つのルートファイルが含まれます。 | ファイル | 用途 | | - | - | | `web.php` | ブラウザ向けのルート。セッション・CSRF保護・クッキー暗号化を提供する `web` ミドルウェアグループが適用されます。 | | `console.php` | Artisanコマンドをクロージャで定義するファイルです。 | | `api.php` | ステートレスなAPIルート。`install:api` コマンドで追加します。 | | `channels.php` | イベントブロードキャストチャンネルを登録するファイルです。 | ### `storage/` ディレクトリ ログ・コンパイル済みBladeテンプレート・ファイルベースのセッション・ファイルキャッシュなど、フレームワークが生成したファイルを格納します。 `app/`・`framework/`・`logs/` の3つのサブディレクトリに分かれています。 ### `tests/` ディレクトリ 自動化されたテストを格納します。 [Pest](https://pestphp.com)または[PHPUnit](https://phpunit.de/)によるユニットテストと機能テストのサンプルが最初から含まれています。 ### `vendor/` ディレクトリ [Composer](https://getcomposer.org)の依存関係を格納します。このディレクトリはソースコントロールにコミットしないでください。 ## `app/` ディレクトリの詳細 `app` ディレクトリの多くのクラスはArtisanコマンドで生成できます。 利用可能なコマンドを確認するには、次を実行してください。 ```shell theme={null} php artisan list make ``` コントローラー・ミドルウェア・フォームリクエストを格納します。 アプリケーションへのリクエスト処理ロジックのほぼすべてがここに置かれます。 [Eloquentモデル](/jp/eloquent)クラスをすべて格納します。 各データベーステーブルに対応する「モデル」が存在し、データの照会や挿入に使います。 アプリケーションのすべての[サービスプロバイダー](/jp/service-providers)を格納します。 サービスプロバイダーはサービスをコンテナにバインドし、イベントを登録するなど、アプリケーションを起動するための処理を行います。 カスタムArtisanコマンドをすべて格納します。 `make:command` コマンドで生成できます。 アプリケーションのカスタム例外をすべて格納します。 `make:exception` コマンドで生成できます。 ## 重要なファイル ### `.env` ファイル 環境ごとに異なる設定値(データベース接続情報・APIキーなど)を管理します。 `config/` 内の設定ファイルから `env()` ヘルパーを通して参照されます。 ```ini theme={null} APP_NAME=Laravel APP_ENV=local APP_DEBUG=true APP_URL=http://localhost DB_CONNECTION=sqlite ``` ### `artisan` ファイル Artisanコマンドラインインターフェースのエントリーポイントです。 `php artisan <コマンド名>` の形式で実行します。 ## 次のステップ ルートの定義方法とURLとアプリケーションロジックの紐付けを学びます。 # Eloquent入門 Source: https://kawax.biz/jp/eloquent LaravelのORM「Eloquent」を使ってデータベースのデータを操作する方法を解説します。 ## Eloquent ORMとは LaravelにはEloquentというオブジェクトリレーショナルマッパー(ORM)が含まれています。 Eloquentはデータベースとの対話を簡単にするためのもので、**ActiveRecordパターン**を実装しています。 Eloquentを使うと、データベースの各テーブルに対応する「モデル」クラスを用意します。 モデルを通じてレコードの取得・挿入・更新・削除が行えます。 Eloquentを使う前に、`config/database.php` でデータベース接続を設定してください。 デフォルトでは `.env` ファイルの `DB_*` 設定が使われます。 ## モデルの作成 `make:model` Artisanコマンドで新しいモデルを生成します。 ```shell theme={null} php artisan make:model Post ``` マイグレーションと同時に作成する場合は `-m` オプションを使います。 ```shell theme={null} php artisan make:model Post -m ``` モデルは `app/Models` ディレクトリに作成されます。 ```php theme={null} timestamps()` を追加しておくと、モデルの保存・更新時に自動的に値がセットされます。 タイムスタンプの自動管理を無効にするには `$timestamps` を `false` に設定します。 ```php theme={null} class Post extends Model { public $timestamps = false; } ``` ## マスアサインメント保護 Eloquentでまとめてデータを保存する際は、マスアサインメント保護の設定が必要です。 ### fillable 代入を許可するカラムを `$fillable` プロパティで指定します。 ```php theme={null} class Post extends Model { protected $fillable = [ 'user_id', 'title', 'body', 'published', ]; } ``` ### guarded 逆に代入を禁止するカラムを指定するには `$guarded` を使います。 ```php theme={null} class Post extends Model { // 主キーのみ保護し、他はすべて許可する protected $guarded = ['id']; } ``` `$guarded` を空配列にするとすべてのカラムへの代入を許可します。 ユーザー入力をそのまま渡す場合は意図しないカラムが書き換えられる危険があるため、`$fillable` で許可するカラムを明示的に指定することを推奨します。 ## Eloquentクエリ実行フロー `User::where()->get()` のようなクエリがどのように実行されるか、内部の流れを示します。 ```mermaid theme={null} flowchart LR A["User::where('active', true)"] --> B["クエリビルダー構築"] B --> C["->get() 呼び出し"] C --> D["SQL生成"] D --> E["データベース実行"] E --> F["結果をEloquentモデルに変換"] F --> G["Collectionとして返却"] ``` ## 基本的なCRUD操作 ### レコードの取得(Read) すべてのレコードを取得します。 ```php theme={null} use App\Models\Post; $posts = Post::all(); ``` 条件を指定して取得します。 ```php theme={null} // publishedがtrueの投稿を取得 $publishedPosts = Post::where('published', true)->get(); // 最初の1件を取得 $post = Post::where('published', true)->first(); // IDで1件を取得 $post = Post::find(1); // 見つからない場合は404レスポンスを返す $post = Post::findOrFail(1); ``` ### レコードの作成(Create) `create` メソッドでレコードを1つ挿入します(`$fillable` の設定が必要です)。 ```php theme={null} $post = Post::create([ 'title' => 'はじめての投稿', 'body' => 'Laravelは素晴らしいフレームワークです。', 'published' => true, ]); ``` インスタンスを作成して個別に代入することもできます。 ```php theme={null} $post = new Post; $post->title = 'はじめての投稿'; $post->body = 'Laravelは素晴らしいフレームワークです。'; $post->save(); ``` ### レコードの更新(Update) モデルを取得して値を変更し、`save` を呼ぶと更新されます。 ```php theme={null} $post = Post::find(1); $post->title = '更新されたタイトル'; $post->save(); ``` `update` メソッドを使うと、まとめて複数のカラムを更新できます。 ```php theme={null} Post::find(1)->update([ 'title' => '更新されたタイトル', 'published' => true, ]); ``` 条件に一致する複数のレコードをまとめて更新することもできます。 ```php theme={null} Post::where('published', false)->update(['published' => true]); ``` ### 悲観的ロック付きでモデルを再読み込みする トランザクション内でモデルを再読み込みし、同時に悲観的ロックを取得するには `refreshForUpdate` メソッドを使います。モデルは `FOR UPDATE` ロックを取得して再読み込みされます。 ```php theme={null} use Illuminate\Support\Facades\DB; DB::transaction(function () use ($post) { $post->refreshForUpdate(); // ロックされたモデルを更新する... }); ``` ### レコードの削除(Delete) `delete` メソッドでレコードを削除します。 ```php theme={null} $post = Post::find(1); $post->delete(); ``` IDを指定して直接削除することもできます。 ```php theme={null} Post::destroy(1); // 複数IDをまとめて削除 Post::destroy([1, 2, 3]); ``` ## 基本的なクエリメソッド | メソッド | 説明 | | - | - | | `Post::all()` | すべてのレコードを取得 | | `Post::find($id)` | IDで1件取得(見つからなければ `null`) | | `Post::findOrFail($id)` | IDで1件取得(見つからなければ404) | | `Post::where('column', 'value')->get()` | 条件に一致するレコードを取得 | | `Post::where('column', 'value')->first()` | 条件に一致する最初の1件を取得 | | `Post::where('column', 'value')->count()` | 条件に一致する件数を取得 | | `Post::orderBy('created_at', 'desc')->get()` | 並び順を指定して取得 | | `Post::latest()->get()` | `created_at` の降順で取得 | | `Post::limit(10)->get()` | 件数を絞って取得 | ## 実践例:Postモデルを使ったデータ操作 マイグレーションで作成した `posts` テーブルを操作するコントローラーの例です。 ```php theme={null} latest() ->get(); return view('posts.index', ['posts' => $posts]); } // 投稿を作成 public function store(Request $request): RedirectResponse { $validated = $request->validate([ 'title' => ['required', 'string', 'max:255'], 'body' => ['required', 'string'], 'published' => ['boolean'], ]); Post::create([ ...$validated, 'user_id' => $request->user()->id, ]); return redirect('/posts'); } // 投稿を更新 public function update(Request $request, Post $post): RedirectResponse { $validated = $request->validate([ 'title' => ['required', 'string', 'max:255'], 'body' => ['required', 'string'], ]); $post->update($validated); return redirect('/posts'); } // 投稿を削除 public function destroy(Post $post): RedirectResponse { $post->delete(); return redirect('/posts'); } } ``` コントローラーのメソッド引数に `Post $post` と書くと、Laravelがルートパラメーターからモデルを自動的に取得してくれます(ルートモデルバインディング)。 `Post::findOrFail($id)` を自分で書く必要がなくなります。 ## モデルイベントのライフサイクル `save()` を呼び出したときに発火するイベントの流れを示します。新規作成か更新かで異なるイベントが発火しますが、`saving`・`saved` は両方の場合に共通して発火します。 ```mermaid theme={null} flowchart TD A["新規インスタンス生成"] --> B{"save()"} B --> I["saving イベント"] I -->|"新規"| C["creating イベント"] C --> D["INSERT実行"] D --> E["created イベント"] I -->|"更新"| F["updating イベント"] F --> G["UPDATE実行"] G --> H["updated イベント"] E --> J["saved イベント"] H --> J ``` ## 次のステップ Eloquentが利用するテーブルをマイグレーションで作成する方法を振り返ります。 # Eloquentコレクション Source: https://kawax.biz/jp/eloquent-collections Illuminate\Database\Eloquent\Collection の専用メソッドとカスタムコレクションの作り方を解説します。 ## はじめに Eloquentで複数件のモデルを取得すると、結果は `Illuminate\Database\Eloquent\Collection` として返ります。\ これは `Illuminate\Support\Collection` を継承しているため、ベースコレクションのメソッドをそのまま使えます。 ```mermaid theme={null} classDiagram class "Illuminate\\Support\\Collection" as SupportCollection class "Illuminate\\Database\\Eloquent\\Collection" as EloquentCollection SupportCollection <|-- EloquentCollection ``` ```php theme={null} use App\Models\User; $users = User::where('active', true)->get(); foreach ($users as $user) { echo $user->name; } ``` まずはベースとなる [コレクション](/jp/collections) と [Eloquent入門](/jp/eloquent) を押さえておくと、Eloquentコレクションの拡張点を理解しやすくなります。 ## 利用可能なメソッド `Eloquent\Collection` はベースコレクションのすべてのメソッドを継承し、さらにモデル向けメソッドを追加します。 ### `append` / `withoutAppends` / `setAppends` シリアライズ時の追加属性(appends)をコレクション全体に対して操作できます。 ```php theme={null} $users->append('team'); $users->append(['team', 'is_admin']); $users = $users->withoutAppends(); $users = $users->setAppends(['is_admin']); ``` ### `contains` / `diff` / `except` / `intersect` / `only` モデルインスタンスや主キーを基準に、包含判定・差集合・積集合・除外・抽出を行います。 ```php theme={null} use App\Models\User; $users->contains(1); $users->contains(User::find(1)); $subset = User::whereIn('id', [1, 2, 3])->get(); $users->diff($subset); $users->intersect($subset); $users->except([1, 2, 3]); $users->only([1, 2, 3]); ``` ### `find` / `findOrFail` 取得済みコレクションから主キーでモデルを検索します。 ```php theme={null} $users = User::all(); $user = $users->find(1); $user = $users->findOrFail(1); ``` ### `fresh` コレクション内の各モデルを、データベースの最新状態で再取得します。 ```php theme={null} $users = $users->fresh(); $users = $users->fresh('comments'); ``` ### `load` / `loadMissing` 取得済みコレクションに対して、後からリレーションをEager Loadingできます。 ```php theme={null} $users->load(['comments', 'posts']); $users->load('comments.author'); $users->load(['comments', 'posts' => fn ($query) => $query->where('active', 1)]); $users->loadMissing(['comments', 'posts']); $users->loadMissing('comments.author'); $users->loadMissing(['comments', 'posts' => fn ($query) => $query->where('active', 1)]); ``` ### `modelKeys` コレクション内モデルの主キー一覧を取得します。 ```php theme={null} $users->modelKeys(); // [1, 2, 3, ...] ``` ### `makeVisible` / `makeHidden` / `mergeVisible` / `mergeHidden` / `setVisible` / `setHidden` シリアライズ時の可視属性・非可視属性を、コレクション単位で調整できます。 ```php theme={null} $users = $users->makeVisible(['address', 'phone_number']); $users = $users->makeHidden(['address', 'phone_number']); $users = $users->mergeVisible(['middle_name']); $users = $users->mergeHidden(['last_login_at']); $users = $users->setVisible(['id', 'name']); $users = $users->setHidden(['email', 'password', 'remember_token']); ``` ### `partition` 条件で2つの `Eloquent\Collection` に分割し、外側は `Illuminate\Support\Collection` として返します。 ```php theme={null} $partition = $users->partition(fn ($user) => $user->age > 18); dump($partition::class); // Illuminate\Support\Collection dump($partition[0]::class); // Illuminate\Database\Eloquent\Collection dump($partition[1]::class); // Illuminate\Database\Eloquent\Collection ``` ### `toQuery` 取得済みモデル群の主キーに対する `whereIn` クエリを作り、まとめて更新や削除に使えます。 ```php theme={null} use App\Models\User; // まず条件に一致するユーザーを取得 $vipUsers = User::where('status', 'VIP')->get(); // コレクション対象だけを一括更新 $vipUsers->toQuery()->update([ 'status' => 'Administrator', ]); ``` 個別 `save()` のループより、`toQuery()` で一括クエリに変換すると、実運用での性能と可読性を両立しやすくなります。 ### `unique` 同じ主キーを持つ重複モデルを取り除きます。 ```php theme={null} $users = $users->unique(); ``` ## Eloquentコレクションからベースコレクションへの変換 `collapse`、`flatten`、`flip`、`keys`、`pluck`、`zip` は `Illuminate\Support\Collection` を返します。\ また、`map` の結果にEloquentモデルが含まれない場合もベースコレクションへ変換されます。 ## カスタムコレクション モデル専用のコレクションクラスを使いたい場合は、まず `#[CollectedBy]` 属性を使う方法を選ぶと明快です。 ### `#[CollectedBy]` 属性(推奨) ```php theme={null} withRelationshipAutoloading(); } return $collection; } } ``` `newCollection()` または `#[CollectedBy]` を定義すると、通常 `Eloquent\Collection` が返る場面でカスタムコレクションが返るようになります。\ 全モデルに適用したい場合は、共通のベースモデルで `newCollection()` を定義します。 ## 関連ページ `Illuminate\\Support\\Collection` の基本操作を先に確認します。 モデルとクエリの基礎を整理してからEloquentコレクションを使いましょう。 # Eloquent Factories Source: https://kawax.biz/jp/eloquent-factories モデルファクトリを使ってテストやシーディング用の偽データを効率よく生成する方法を解説します。 ## ファクトリとは テストやデータベースシーディングでは、データベースにレコードを挿入する必要があります。 手動で各カラムの値を指定する代わりに、Laravelの**モデルファクトリ**を使うと各Eloquentモデルのデフォルト属性セットを定義できます。 すべての新しいLaravelアプリケーションには `database/factories/UserFactory.php` が付属しています。 ```php theme={null} namespace Database\Factories; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; class UserFactory extends Factory { // 同じパスワードを毎回再ハッシュしないためにキャッシュする protected static ?string $password; public function definition(): array { return [ 'name' => fake()->name(), 'email' => fake()->unique()->safeEmail(), 'email_verified_at' => now(), 'password' => static::$password ??= Hash::make('password'), 'remember_token' => Str::random(10), ]; } } ``` `fake()` ヘルパーを通じて [Faker](https://github.com/FakerPHP/Faker) ライブラリが利用でき、様々なランダムデータを生成できます。 ```mermaid theme={null} flowchart LR F["Factory
    database/factories/UserFactory"] -->|"make() / create()"| M["Model Instance
    App\\Models\\User"] M -->|"create()"| DB[("Database")] F -->|"Faker"| R["ランダムデータ
    name, email, ..."] R --> F ``` Fakerのロケールは `config/app.php` の `faker_locale` オプションで変更できます。 ## ファクトリの生成 `make:factory` Artisanコマンドでファクトリを作成します。 ```shell theme={null} php artisan make:factory PostFactory ``` 新しいファクトリクラスは `database/factories` ディレクトリに配置されます。 ### モデルとファクトリの自動検出 モデルに `HasFactory` トレイトを使用すると、Laravelが自動的に対応するファクトリを見つけます。 `Database\Factories` 名前空間で、モデル名に `Factory` を付けたクラス名を検索します。 命名規則が合わない場合は `UseFactory` 属性で明示的に指定できます。 ```php theme={null} use Illuminate\Database\Eloquent\Attributes\UseFactory; use Database\Factories\Administration\FlightFactory; #[UseFactory(FlightFactory::class)] class Flight extends Model { // ... } ``` ## ファクトリの定義 ### `definition()` メソッド ファクトリの中心となる `definition()` メソッドでデフォルト属性を定義します。 ```php theme={null} namespace Database\Factories; use Illuminate\Database\Eloquent\Factories\Factory; class PostFactory extends Factory { public function definition(): array { return [ 'user_id' => \App\Models\User::factory(), 'title' => fake()->sentence(), 'content' => fake()->paragraphs(3, true), 'published_at' => fake()->optional()->dateTimeBetween('-1 year', 'now'), ]; } } ``` Fakerがよく使われるメソッドの一覧: | メソッド | 生成されるデータ例 | | - | - | | `fake()->name()` | `John Doe` | | `fake()->email()` | `john@example.com` | | `fake()->sentence()` | ランダムな文 | | `fake()->paragraph()` | ランダムな段落 | | `fake()->numberBetween(1, 100)` | `1`〜`100`の整数 | | `fake()->dateTime()` | ランダムな日時 | | `fake()->boolean()` | `true` / `false` | ## ファクトリステート (States) ステートメソッドを使うと、ファクトリに対して個別の変更を定義できます。 ```php theme={null} use Illuminate\Database\Eloquent\Factories\Factory; class UserFactory extends Factory { public function definition(): array { return [ 'name' => fake()->name(), 'email' => fake()->unique()->safeEmail(), 'account_status' => 'active', ]; } // 停止中ユーザーのステート public function suspended(): static { return $this->state(fn (array $attributes) => [ 'account_status' => 'suspended', ]); } // 管理者ユーザーのステート public function admin(): static { return $this->state(fn (array $attributes) => [ 'is_admin' => true, ]); } } ``` ステートは組み合わせて使えます。 ```php theme={null} $user = User::factory()->suspended()->create(); $admin = User::factory()->admin()->create(); ``` ### ソフトデリートのステート ソフトデリート対応のモデルには、ビルトインの `trashed()` ステートが利用できます。 ```php theme={null} $user = User::factory()->trashed()->create(); ``` ## ファクトリコールバック (Callbacks) `afterMaking` と `afterCreating` でモデル生成後の追加処理を定義します。 ```php theme={null} namespace Database\Factories; use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; class UserFactory extends Factory { public function configure(): static { return $this->afterMaking(function (User $user) { // make() 後に実行(DBには保存されていない) })->afterCreating(function (User $user) { // create() 後に実行(DBに保存済み) }); } // ... } ``` ステートメソッド内でもコールバックを登録できます。 ```php theme={null} public function withProfile(): static { return $this->state(fn (array $attributes) => []) ->afterCreating(function (User $user) { $user->profile()->create([ 'bio' => fake()->paragraph(), ]); }); } ``` ## モデルの生成 ### make() — DBに保存しない ```php theme={null} use App\Models\User; // 1件生成 $user = User::factory()->make(); // 3件生成(コレクションで返る) $users = User::factory()->count(3)->make(); ``` ### create() — DBに保存する ```php theme={null} // 1件生成してDBに保存 $user = User::factory()->create(); // 3件生成してDBに保存 $users = User::factory()->count(3)->create(); ``` ### 属性の上書き `make()` や `create()` に配列を渡すと、特定の属性だけ上書きできます。 ```php theme={null} $user = User::factory()->make([ 'name' => '山田太郎', ]); $user = User::factory()->create([ 'name' => '鈴木花子', 'email' => 'hanako@example.com', ]); ``` ステートを使ったインライン上書きも可能です。 ```php theme={null} $user = User::factory()->state([ 'name' => '田中一郎', ])->make(); ``` ファクトリでモデルを生成する際、マスアサインメント保護は自動的に無効になります。 ### Sequence(シーケンス) 複数モデルを生成する際に属性を交互に変える場合は `Sequence` を使います。 ```php theme={null} use App\Models\User; use Illuminate\Database\Eloquent\Factories\Sequence; $users = User::factory() ->count(10) ->state(new Sequence( ['admin' => 'Y'], ['admin' => 'N'], )) ->create(); // 5件はadmin='Y'、5件はadmin='N'で生成される ``` `sequence()` メソッドを使うとより簡潔に書けます。 ```php theme={null} $users = User::factory() ->count(2) ->sequence( ['name' => '最初のユーザー'], ['name' => '2番目のユーザー'], ) ->create(); ``` クロージャでランダムな値を動的に生成することもできます。 ```php theme={null} use Illuminate\Database\Eloquent\Factories\Sequence; $users = User::factory() ->count(10) ->state(new Sequence( fn (Sequence $sequence) => ['name' => 'ユーザー ' . $sequence->index], )) ->create(); ``` ## リレーションシップのファクトリ ```mermaid theme={null} flowchart TD UF["UserFactory"] -->|"has(PostFactory)"| PF["PostFactory"] PF -->|"for(UserFactory)"| UF UF -->|"hasAttached(RoleFactory)"| RF["RoleFactory"] UF -->|"create()"| U["User モデル"] PF -->|"create()"| P["Post モデル"] RF -->|"create()"| R["Role モデル"] U -->|"hasMany"| P U -->|"belongsToMany"| R ``` ### Has Many(1対多) `has()` メソッドで「1対多」リレーションを持つモデルを生成します。 ```php theme={null} use App\Models\Post; use App\Models\User; // 3件のPostを持つUserを生成 $user = User::factory() ->has(Post::factory()->count(3)) ->create(); ``` マジックメソッドを使うとより簡潔に書けます(`has` + リレーション名の複数形)。 ```php theme={null} $user = User::factory() ->hasPosts(3) ->create(); // 属性を上書き $user = User::factory() ->hasPosts(3, ['published' => false]) ->create(); ``` ### Belongs To(逆リレーション) `for()` メソッドで「属する」親モデルを指定します。 ```php theme={null} use App\Models\Post; use App\Models\User; // 特定ユーザーに属する3件のPostを生成 $posts = Post::factory() ->count(3) ->for(User::factory()->state(['name' => '田中花子'])) ->create(); // 既存モデルを親として使用 $user = User::factory()->create(); $posts = Post::factory()->count(3)->for($user)->create(); ``` マジックメソッド版: ```php theme={null} $posts = Post::factory() ->count(3) ->forUser(['name' => '田中花子']) ->create(); ``` ### Many to Many(多対多) `hasAttached()` メソッドで多対多リレーションを扱います。中間テーブルの属性も指定できます。 ```php theme={null} use App\Models\Role; use App\Models\User; $user = User::factory() ->hasAttached( Role::factory()->count(3), ['active' => true] // 中間テーブルの属性 ) ->create(); ``` マジックメソッド版: ```php theme={null} $user = User::factory() ->hasRoles(1, ['name' => 'Editor']) ->create(); ``` ### ポリモーフィックリレーション 通常の「1対多」と同様に `has()` またはマジックメソッドが使えます。 ```php theme={null} use App\Models\Post; $post = Post::factory()->hasComments(3)->create(); ``` `morphTo` リレーションにはマジックメソッドは使えません。`for()` に関係名を明示します。 ```php theme={null} $comments = Comment::factory()->count(3)->for( Post::factory(), 'commentable' )->create(); ``` ### ファクトリ内でのリレーション定義 `definition()` 内で外部キーに別のファクトリを割り当てると、モデル生成時に自動的に親モデルも生成されます。 ```php theme={null} class PostFactory extends Factory { public function definition(): array { return [ 'user_id' => \App\Models\User::factory(), 'title' => fake()->sentence(), 'content' => fake()->paragraph(), ]; } } ``` ### recycle() — 既存モデルの再利用 複数のリレーションで同じモデルインスタンスを再利用したい場合は `recycle()` を使います。 ```php theme={null} // 同じAirlineをTicketとFlightの両方で使い回す Ticket::factory() ->recycle(Airline::factory()->create()) ->create(); // コレクションからランダムに選択 $airlines = Airline::factory()->count(3)->create(); Ticket::factory()->count(10)->recycle($airlines)->create(); ``` ## シーディングでの利用 `DatabaseSeeder` やシーダークラスからファクトリを呼び出します。 ```php theme={null} namespace Database\Seeders; use App\Models\User; use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { public function run(): void { // 10件のユーザーを生成し、各ユーザーに3件の投稿を持たせる User::factory() ->count(10) ->hasPosts(3) ->create(); } } ``` シーダーの実行: ```shell theme={null} php artisan db:seed ``` ## テストでの利用 テストでは `RefreshDatabase` トレイトと組み合わせてファクトリを使います。 ```php theme={null} namespace Tests\Feature; use App\Models\User; use App\Models\Post; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class PostTest extends TestCase { use RefreshDatabase; public function test_user_can_view_their_posts(): void { $user = User::factory()->create(); $posts = Post::factory()->count(3)->for($user)->create(); $response = $this->actingAs($user) ->get('/dashboard'); $response->assertOk(); $response->assertSee($posts->first()->title); } public function test_suspended_user_cannot_post(): void { $user = User::factory()->suspended()->create(); $response = $this->actingAs($user) ->post('/posts', ['title' => 'テスト']); $response->assertForbidden(); } } ``` `RefreshDatabase` はテストごとにデータベースをリセットするため、テスト間でデータが干渉しません。 PestでテストするときもファクトリのAPIは同じです。 `uses(RefreshDatabase::class)` を使ってデータベースをリセットしてください。 ## 次のステップ シーダーとファクトリを組み合わせて初期データを投入する方法を確認します。 Laravelのテスト機能とRefreshDatabaseトレイトの使い方を確認します。 # Eloquent アクセサ・ミューテタ・キャスト Source: https://kawax.biz/jp/eloquent-mutators Eloquentモデルの属性値を取得・保存時に変換する仕組みを解説します。アクセサ・ミューテタの定義方法と組み込みキャストの使い方を実践的に紹介します。 ## はじめに **アクセサ**・**ミューテタ**・**属性キャスト**は、Eloquentモデルの属性値をモデルインスタンスで取得・設定するときに変換する仕組みです。 * **アクセサ** — DBから取得した生の値を加工してアプリケーションに渡す * **ミューテタ** — アプリケーションからセットされた値を加工してDBに保存する * **キャスト** — 追加メソッドなしで属性の型変換を宣言的に定義する ```mermaid theme={null} flowchart LR DB["データベース
    (生の値)"] -->|"取得時
    アクセサ/キャスト"| APP["アプリケーション
    (変換後の値)"] APP -->|"保存時
    ミューテタ/キャスト"| DB ``` ## アクセサの定義 アクセサを定義するには、モデルに `protected` メソッドを追加します。メソッド名はキャメルケース、戻り値型は `Illuminate\Database\Eloquent\Casts\Attribute` にします。 ```php theme={null} ucfirst($value), ); } } ``` `get` クロージャにDBの生の値が渡されます。モデルインスタンスから `first_name` プロパティとしてアクセスできます。 ```php theme={null} $user = User::find(1); $firstName = $user->first_name; // ucfirst() が適用された値 ``` アクセサで計算した値をJSON/配列に含めたい場合は、モデルの `$appends` プロパティにスネークケースで追加してください。 ### 複数の属性から値オブジェクトを生成する `get` クロージャは第2引数として `$attributes`(モデルのすべての属性)を受け取れます。複数カラムを組み合わせて1つの値オブジェクトを返すことができます。 ```php theme={null} use App\Support\Address; use Illuminate\Database\Eloquent\Casts\Attribute; protected function address(): Attribute { return Attribute::make( get: fn (mixed $value, array $attributes) => new Address( $attributes['address_line_one'], $attributes['address_line_two'], ), ); } ``` ### アクセサのキャッシュ 値オブジェクトを返すアクセサは、Eloquentが同じインスタンスを返すよう自動でキャッシュします。文字列や数値など基本型でもキャッシュしたい場合は `shouldCache()` を呼び出します。 ```php theme={null} protected function hash(): Attribute { return Attribute::make( get: fn (string $value) => bcrypt(gzuncompress($value)), )->shouldCache(); } ``` オブジェクトのキャッシュを無効にしたい場合は `withoutObjectCaching()` を使います。 ```php theme={null} protected function address(): Attribute { return Attribute::make( get: fn (mixed $value, array $attributes) => new Address( $attributes['address_line_one'], $attributes['address_line_two'], ), )->withoutObjectCaching(); } ``` ## ミューテタの定義 ミューテタは `Attribute::make()` の `set` 引数として定義します。アクセサと同じメソッドにまとめられます。 ```php theme={null} protected function firstName(): Attribute { return Attribute::make( get: fn (string $value) => ucfirst($value), set: fn (string $value) => strtolower($value), ); } ``` モデルに値をセットすると `set` クロージャが呼ばれます。 ```php theme={null} $user = User::find(1); $user->first_name = 'SALLY'; // strtolower() が適用されて 'sally' が保存される ``` ### 複数の属性に書き込む `set` クロージャから配列を返すと、複数のカラムをまとめて更新できます。 ```php theme={null} use App\Support\Address; use Illuminate\Database\Eloquent\Casts\Attribute; protected function address(): Attribute { return Attribute::make( get: fn (mixed $value, array $attributes) => new Address( $attributes['address_line_one'], $attributes['address_line_two'], ), set: fn (Address $value) => [ 'address_line_one' => $value->lineOne, 'address_line_two' => $value->lineTwo, ], ); } ``` ## 属性キャスト **キャスト**は、アクセサ・ミューテタを書かなくても属性の型変換を宣言できる簡便な方法です。モデルの `casts()` メソッドで配列を返します。 ```php theme={null} 'boolean', 'score' => 'float', 'settings' => 'array', 'created_at' => 'datetime', ]; } } ``` ### 組み込みキャスト一覧 | キャスト | 説明 | | - | - | | `integer` / `int` | 整数 | | `float` / `double` / `real` | 浮動小数点数 | | `decimal:<桁数>` | 指定桁数の小数 | | `string` | 文字列 | | `boolean` / `bool` | 真偽値(`0`/`1` を含む) | | `array` | JSONを配列に相互変換 | | `object` | JSONをオブジェクトに相互変換 | | `collection` | JSONをコレクションに変換 | | `date` | 日付(Carbon) | | `datetime` | 日時(Carbon) | | `immutable_date` | イミュータブルな日付 | | `immutable_datetime` | イミュータブルな日時 | | `timestamp` | UNIXタイムスタンプ | | `hashed` | 保存時にハッシュ化 | | `encrypted` | 保存時に暗号化 | | `AsVector::class` | データベースのベクトルを浮動小数点数の配列に変換 | `null` の属性はキャストされません。また、リレーション名と同じ名前のキャストや、主キーへのキャストは定義しないでください。 ### Stringable キャスト `AsStringable` を使うと、属性を `Illuminate\Support\Stringable` オブジェクトとして扱えます。 ```php theme={null} use Illuminate\Database\Eloquent\Casts\AsStringable; protected function casts(): array { return [ 'bio' => AsStringable::class, ]; } ``` ## 配列・JSON キャスト JSON/TEXT カラムを PHP 配列として透過的に扱えます。 ```php theme={null} protected function casts(): array { return [ 'options' => 'array', ]; } ``` ```php theme={null} $user = User::find(1); // 自動的にPHP配列として取得 $options = $user->options; // 変更して保存すると自動的にJSONシリアライズされる $user->options = array_merge($options, ['theme' => 'dark']); $user->save(); ``` `->` 演算子で JSON の特定キーだけを更新することもできます。 ```php theme={null} $user->update(['options->theme' => 'dark']); ``` ### AsArrayObject / AsCollection キャスト 標準の `array` キャストは、配列の特定オフセットを直接変更しようとするとエラーになります。`AsArrayObject` や `AsCollection` を使うとこの問題を回避できます。 ```php theme={null} use Illuminate\Database\Eloquent\Casts\AsArrayObject; use Illuminate\Database\Eloquent\Casts\AsCollection; protected function casts(): array { return [ 'options' => AsArrayObject::class, // ArrayObjectとして扱う 'tags' => AsCollection::class, // Collectionとして扱う ]; } ``` カスタムコレクションクラスを使いたい場合は `using()` を指定します。 ```php theme={null} use App\Collections\TagCollection; use Illuminate\Database\Eloquent\Casts\AsCollection; protected function casts(): array { return [ 'tags' => AsCollection::using(TagCollection::class), ]; } ``` ## ベクトルキャスト `Illuminate\Database\Eloquent\Casts\AsVector` キャストを使うと、データベースのベクトルカラムを PHP 配列との間で変換できます。 ```php theme={null} use Illuminate\Database\Eloquent\Casts\AsVector; protected function casts(): array { return [ 'embedding' => AsVector::class, ]; } ``` 属性を設定するときは、PHP 配列または Laravel のコレクションなどの `Arrayable` インスタンスを指定できます。属性を取得すると、キャストは浮動小数点数の配列を返します。 ## 日時キャスト `created_at` / `updated_at` はデフォルトで Carbon にキャストされます。追加の日時カラムも同様に定義できます。 ```php theme={null} protected function casts(): array { return [ 'published_at' => 'datetime', 'expires_at' => 'immutable_datetime', ]; } ``` フォーマットを指定すると、JSON シリアライズ時にそのフォーマットが使われます。 ```php theme={null} protected function casts(): array { return [ 'published_at' => 'datetime:Y-m-d', ]; } ``` すべての日付のデフォルトシリアライズフォーマットを変えたい場合は `serializeDate()` をオーバーライドします(DB保存フォーマットには影響しません)。 ```php theme={null} use DateTimeInterface; protected function serializeDate(DateTimeInterface $date): string { return $date->format('Y-m-d'); } ``` `immutable_datetime` を使うと Carbon の代わりに CarbonImmutable が返ります。元のインスタンスを変更せずに日時操作できるため、副作用のないコードを書きやすくなります。 ## Enum キャスト PHP 8.1 以降の Backed Enum をキャストとして指定できます。 ```php theme={null} ServerStatus::class, ]; } ``` DBには Enum のバッキング値(`string` または `int`)が保存され、取得時には Enum インスタンスに変換されます。 ```php theme={null} $server = Server::find(1); if ($server->status === ServerStatus::Provisioned) { $server->status = ServerStatus::Ready; $server->save(); } ``` ### Enum の配列キャスト 複数の Enum 値を1カラムに配列として保存したい場合は `AsEnumCollection` を使います。 ```php theme={null} use App\Enums\ServerStatus; use Illuminate\Database\Eloquent\Casts\AsEnumCollection; protected function casts(): array { return [ 'statuses' => AsEnumCollection::of(ServerStatus::class), ]; } ``` ## クエリ時のキャスト クエリ実行時に動的にキャストを適用するには `withCasts()` を使います。 ```php theme={null} use App\Models\Post; use App\Models\User; $users = User::select([ 'users.*', 'last_posted_at' => Post::selectRaw('MAX(created_at)') ->whereColumn('user_id', 'users.id'), ])->withCasts([ 'last_posted_at' => 'datetime', ])->get(); ``` ## カスタムキャスト 独自のキャストクラスを作成することも可能です。`CastsAttributes` インターフェースを実装し、`get` と `set` メソッドを定義します。 ```shell theme={null} php artisan make:cast AsJson ``` 詳細な実装方法(Value Object パターン、インバウンドキャスト、Castables など)は以下の上級ページを参照してください。 CastsAttributes インターフェースの実装方法や、Value Object パターン・Castables など上級のカスタムキャストを解説します。 ## 関連ページ モデルを一貫したJSON APIレスポンスに変換するリソースクラスの使い方を確認します。 # Eloquentリレーション入門 Source: https://kawax.biz/jp/eloquent-relationships Eloquentを使ってテーブル間のリレーション(1対1・1対多・多対多)を定義し、関連データを効率よく取得する方法を解説します。 ## リレーションとは データベースのテーブルは互いに関連し合っていることがよくあります。たとえば、ブログ投稿には多数のコメントがあったり、注文はそれを行ったユーザーに関連していたりします。 Eloquentはこうしたテーブル間の関連(リレーション)を簡単に定義・操作する仕組みを提供しています。リレーションはEloquentモデルのメソッドとして定義します。 このページでは `User`、`Post`、`Comment`、`Tag` などの具体的なモデルを例として使います。 これらのモデルとテーブルはあらかじめ作成されている前提で進めます。 Eloquentが対応する主なリレーションは以下のとおりです。 | リレーション | 説明 | | - | - | | `hasOne` | 1対1(親が1つの子を持つ) | | `belongsTo` | 1対1の逆(子が親に属する) | | `hasMany` | 1対多(親が複数の子を持つ) | | `belongsToMany` | 多対多(中間テーブルを使う) | ```mermaid theme={null} erDiagram users ||--o| profiles : "hasOne / belongsTo (profiles.user_id)" posts ||--o{ comments : "hasMany / belongsTo (comments.post_id)" posts }o--o{ tags : "belongsToMany (post_tag 中間テーブル)" ``` ## hasOne(1対1) `hasOne` は、あるモデルが別のモデルを1つだけ持つ関係を表します。たとえば `User` が1つの `Profile` を持つ場合です。 ### リレーションの定義 `User` モデルに `profile` メソッドを定義します。 ```php theme={null} hasOne(Profile::class); } } ``` Eloquentは親モデル名をもとに外部キーを自動的に推測します。この場合、`profiles` テーブルに `user_id` カラムがあると想定します。 ### 関連データの取得 定義したリレーションはプロパティとしてアクセスできます。 ```php theme={null} $user = User::find(1); $profile = $user->profile; ``` リレーションメソッドをプロパティとして呼び出すと、Eloquentが自動的にクエリを発行して関連データを返します。これを「ダイナミックリレーションプロパティ」といいます。 ## belongsTo(1対1の逆) `belongsTo` は `hasOne` の逆で、外部キーを持つ側のモデルに定義します。`Profile` が `User` に属する関係を表します。 ### リレーションの定義 ```php theme={null} belongsTo(User::class); } } ``` Eloquentはリレーションメソッド名に `_id` を付けたカラム名(この場合 `user_id`)を外部キーとして使います。 ### 関連データの取得 ```php theme={null} $profile = Profile::find(1); $user = $profile->user; echo $user->name; ``` ## hasMany(1対多) `hasMany` は、1つの親モデルが複数の子モデルを持つ最もよく使われるリレーションです。たとえば、1つの `Post` が複数の `Comment` を持つ場合です。 ### リレーションの定義 ```php theme={null} hasMany(Comment::class); } } ``` Eloquentは `comments` テーブルに `post_id` カラムがあると想定します。 ### 関連データの取得 `hasMany` のリレーションはコレクションを返します。 ```php theme={null} $post = Post::find(1); foreach ($post->comments as $comment) { echo $comment->body; } ``` クエリに条件を追加することもできます。 ```php theme={null} $recentComments = Post::find(1) ->comments() ->latest() ->take(5) ->get(); ``` ### 逆のリレーション(belongsTo) コメントから投稿を参照するには、`Comment` モデルに `belongsTo` を定義します。 ```php theme={null} belongsTo(Post::class); } } ``` ```php theme={null} $comment = Comment::find(1); echo $comment->post->title; ``` ### `whereBelongsTo()`で親モデルから絞り込む `belongsTo` の子モデルを取得する際、`where('post_id', $post->id)` のように外部キーを手動で指定することもできますが、`whereBelongsTo` メソッドを使うと、渡したモデルからリレーションと外部キーを自動的に判定してくれます。 ```php theme={null} $comments = Comment::whereBelongsTo($post)->get(); ``` [コレクション](/jp/eloquent-collections)を渡すこともできます。この場合、コレクション内のいずれかの親モデルに属するモデルをまとめて取得します。 ```php theme={null} $posts = Post::where('featured', true)->get(); $comments = Comment::whereBelongsTo($posts)->get(); ``` デフォルトでは渡したモデルのクラス名からリレーションを判定しますが、第2引数でリレーション名を明示的に指定することもできます。 ```php theme={null} $comments = Comment::whereBelongsTo($post, 'post')->get(); ``` ### `chaperone()`による親モデルの自動ハイドレート `Post::with('comments')` のように子モデルをEagerロードしても、各 `Comment` から `$comment->post` にアクセスすると追加のクエリが発生し、N+1問題につながります。`hasMany` の定義に `chaperone()` を付けると、Eloquentが各子モデルへ親モデルを自動的に紐付けてくれるため、この追加クエリを防げます。 ```php theme={null} hasMany(Comment::class)->chaperone(); } } ``` リレーション定義を変更せずにEagerロード時だけ有効にしたい場合は、`with()` の中で `chaperone()` を呼び出します。 ```php theme={null} $posts = Post::with([ 'comments' => fn ($comments) => $comments->chaperone(), ])->get(); ``` `chaperone()` は多態リレーションの `morphMany` でも同様に使えます。ピボットモデル経由の `belongsToMany` に対する `chaperone()` は [カスタムピボットモデルとchaperone](/jp/advanced/eloquent-custom-pivot) で解説しています。 ## belongsToMany(多対多) 多対多リレーションは、両方のモデルが互いに複数の関連を持つ場合に使います。たとえば、`Post` が複数の `Tag` を持ち、`Tag` も複数の `Post` に属する場合です。 ### テーブル構造 多対多には中間テーブルが必要です。`Post` と `Tag` の場合、`post_tag` という中間テーブルを用意します。 ```text theme={null} posts id - integer title - string tags id - integer name - string post_tag post_id - integer tag_id - integer ``` 中間テーブル名はEloquentが関連する2つのモデル名をアルファベット順に結合して自動的に推測します(`post` + `tag` → `post_tag`)。 ### リレーションの定義 ```php theme={null} belongsToMany(Tag::class); } } ``` `Tag` 側にも同様に定義することで、逆方向からも参照できます。 ```php theme={null} belongsToMany(Post::class); } } ``` ### 関連データの取得 ```php theme={null} $post = Post::find(1); foreach ($post->tags as $tag) { echo $tag->name; } ``` ### 関連データの追加・削除 `attach` で中間テーブルにレコードを追加、`detach` で削除します。 ```php theme={null} $post = Post::find(1); // タグを追加 $post->tags()->attach($tagId); // タグを削除 $post->tags()->detach($tagId); // 現在の関連をすべて置き換える $post->tags()->sync([$tagId1, $tagId2]); ``` `sync` を使うと、指定したID以外の中間テーブルレコードを自動的に削除して、渡したIDだけが関連として残るように更新します。 中間テーブルに追加カラムを持たせたり、`using()`でカスタムピボットモデルを指定する高度な使い方は [カスタムピボットモデルとchaperone](/jp/advanced/eloquent-custom-pivot) で解説しています。 ## Eagerローディング ### N+1問題とは リレーションをプロパティとしてアクセスすると、Eloquentはその都度クエリを発行します。これを「遅延ロード(Lazy Loading)」といいます。ループ内でリレーションにアクセスすると、N+1問題が発生します。 ```php theme={null} // 全投稿を取得する1回のクエリ $posts = Post::all(); foreach ($posts as $post) { // 各投稿ごとにユーザーを取得するクエリが発行される(N回) echo $post->user->name; } ``` 投稿が100件あれば、合計101回のクエリが発行されます。これはパフォーマンスに大きな影響を与えます。 ### with()によるEagerローディング `with()` メソッドを使うと、関連データをまとめて取得できます。これにより、クエリが2回だけに抑えられます。 ```php theme={null} // 投稿とユーザーを2回のクエリで取得 $posts = Post::with('user')->get(); foreach ($posts as $post) { // 追加のクエリは発行されない echo $post->user->name; } ``` 実行されるSQLは以下の2つだけです。 ```sql theme={null} select * from posts select * from users where id in (1, 2, 3, ...) ``` ### 複数のリレーションを同時にEagerロード 配列で複数のリレーションを指定できます。 ```php theme={null} $posts = Post::with(['user', 'comments', 'tags'])->get(); ``` ### ネストしたEagerロード ドット記法でネストしたリレーションもEagerロードできます。 ```php theme={null} // 投稿のコメントと、各コメントのユーザーをEagerロード $posts = Post::with('comments.user')->get(); ``` EagerローディングはN+1問題を防ぐために非常に重要です。ループ内でリレーションにアクセスするときは、常に `with()` を使う習慣をつけましょう。 ## 次のステップ Eloquentの基本的なCRUD操作に戻って復習します。 # Eloquent APIリソース Source: https://kawax.biz/jp/eloquent-resources Eloquentモデルを一貫したJSON APIレスポンスに変換する方法を解説します。リソースクラスの作成から条件付きフィールド、ページネーション連携まで実践的に紹介します。 ## APIリソースとは APIを構築するとき、EloquentモデルをそのままJSONとして返すと、隠したいカラムが漏れたり、クライアントが必要としていない大量のデータを送り付けてしまうことがあります。 **Eloquent APIリソース**は、モデルとJSONレスポンスの間に変換レイヤーを挟む仕組みです。`toArray()` メソッドで「何をどの形式でレスポンスに含めるか」を明示的に定義できます。 ```mermaid theme={null} flowchart LR A["Eloquentモデル
    (User)"] --> B["リソースクラス
    (UserResource)"] B --> C["JSONレスポンス
    { id, name, email }"] ``` 主なメリットは次のとおりです。 * レスポンスに含めるフィールドを完全にコントロールできる * フィールド名の変換や値の加工をひとつの場所にまとめられる * 条件によってフィールドを出し分けられる * リレーションをネストして一貫した構造を保てる ## リソースの作成 `make:resource` Artisanコマンドでリソースクラスを生成します。 ```shell theme={null} php artisan make:resource UserResource ``` 生成されたクラスは `app/Http/Resources` ディレクトリに置かれます。 ```php theme={null} $this->id, 'name' => $this->name, 'email' => $this->email, 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ]; } } ``` `$this` でモデルのプロパティに直接アクセスできます。これはリソースクラスが内部でモデルへのアクセスをプロキシしているためです。 ## コントローラーでの使用 定義したリソースはコントローラーやルートから返せます。 ```php theme={null} use App\Http\Resources\UserResource; use App\Models\User; Route::get('/users/{user}', function (User $user) { return new UserResource($user); }); ``` または、モデルの `toResource()` メソッドを使う方法もあります。 ```php theme={null} Route::get('/users/{user}', function (User $user) { return $user->toResource(); }); ``` `toResource()` はモデル名に基づいて対応するリソースクラス(`UserResource`)を自動的に探します。 デフォルトでは、レスポンスは `data` キーでラップされます。 ```json theme={null} { "data": { "id": 1, "name": "山田 太郎", "email": "yamada@example.com", "created_at": "2024-01-15T10:00:00.000000Z", "updated_at": "2024-01-15T10:00:00.000000Z" } } ``` ## リソースコレクション 複数のモデルを返す場合は `collection()` メソッドを使います。 ```php theme={null} use App\Http\Resources\UserResource; use App\Models\User; Route::get('/users', function () { return UserResource::collection(User::all()); }); ``` または、Eloquentコレクションの `toResourceCollection()` を使います。 ```php theme={null} return User::all()->toResourceCollection(); ``` ### カスタムコレクションリソース コレクション全体にメタデータを付加したい場合は、専用のコレクションリソースを作成します。 ```shell theme={null} php artisan make:resource UserCollection ``` ```php theme={null} $this->collection, 'links' => [ 'self' => route('users.index'), ], ]; } } ``` ```php theme={null} Route::get('/users', function () { return new UserCollection(User::all()); }); ``` ## フィールドの加工と変換 `toArray()` 内でフィールド名の変更や値の加工ができます。 ```php theme={null} public function toArray(Request $request): array { return [ 'id' => $this->id, 'full_name' => $this->name, // フィールド名を変換 'email_address' => $this->email, // フィールド名を変換 'role' => strtoupper($this->role), // 値を加工 'registered_at' => $this->created_at->toDateString(), // 日付をフォーマット ]; } ``` ## 条件付きフィールド ### when() — 条件に応じてフィールドを追加 特定の条件を満たすときだけフィールドを含めたい場合は `when()` を使います。 ```php theme={null} public function toArray(Request $request): array { return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, // 認証ユーザーが管理者のときだけ含める 'secret_token' => $this->when( $request->user()?->isAdmin(), $this->secret_token ), ]; } ``` `when()` の条件が `false` のとき、そのキー自体がレスポンスから取り除かれます。 ### mergeWhen() — 複数フィールドをまとめて条件付き追加 同じ条件で複数のフィールドをまとめて出し分けるには `mergeWhen()` を使います。 ```php theme={null} public function toArray(Request $request): array { return [ 'id' => $this->id, 'name' => $this->name, $this->mergeWhen($request->user()?->isAdmin(), [ 'admin_note' => $this->admin_note, 'internal_id' => $this->internal_id, ]), ]; } ``` ### whenLoaded() — ロード済みリレーションのみ含める リレーションがEagerロードされているときだけ含めることで、N+1問題を防ぎながら柔軟なレスポンスを作れます。 ```php theme={null} use App\Http\Resources\PostResource; public function toArray(Request $request): array { return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, // postsがwith()でロードされているときだけ含める 'posts' => PostResource::collection($this->whenLoaded('posts')), ]; } ``` コントローラー側でリレーションをロードするかどうかを制御できます。 ```php theme={null} // postsを含めて返す return new UserResource($user->load('posts')); // postsなしで返す return new UserResource($user); ``` ### whenCounted() — カウントを条件付きで含める `loadCount()` で取得したリレーションのカウントを条件付きで含めます。 ```php theme={null} public function toArray(Request $request): array { return [ 'id' => $this->id, 'name' => $this->name, 'posts_count' => $this->whenCounted('posts'), ]; } ``` ```php theme={null} return new UserResource($user->loadCount('posts')); ``` ## ネストしたリソース リレーションを別のリソースクラスでネストすることで、一貫した構造を保てます。 ```php theme={null} // app/Http/Resources/PostResource.php class PostResource extends JsonResource { public function toArray(Request $request): array { return [ 'id' => $this->id, 'title' => $this->title, 'body' => $this->body, 'author' => new UserResource($this->whenLoaded('user')), 'comments' => CommentResource::collection($this->whenLoaded('comments')), 'published_at' => $this->published_at?->toDateString(), ]; } } ``` ```php theme={null} // コントローラー $post = Post::with(['user', 'comments'])->findOrFail($id); return new PostResource($post); ``` ```json theme={null} { "data": { "id": 1, "title": "Laravelで学ぶAPIリソース", "author": { "id": 5, "name": "山田 太郎", "email": "yamada@example.com" }, "comments": [ { "id": 10, "body": "参考になりました!" } ] } } ``` ## メタデータの追加 ### with() — トップレベルのメタデータ コレクション全体にメタデータを付加するには `with()` メソッドをオーバーライドします。 ```php theme={null} class UserCollection extends ResourceCollection { public function toArray(Request $request): array { return parent::toArray($request); } public function with(Request $request): array { return [ 'meta' => [ 'version' => '1.0', 'generated_at' => now()->toIso8601String(), ], ]; } } ``` レスポンス例: ```json theme={null} { "data": [...], "meta": { "version": "1.0", "generated_at": "2024-01-15T10:00:00+00:00" } } ``` ### additional() — 動的にメタデータを追加 コントローラー側で動的にメタデータを追加したい場合は `additional()` を使います。 ```php theme={null} return User::all() ->load('roles') ->toResourceCollection() ->additional(['meta' => [ 'total_admins' => User::where('role', 'admin')->count(), ]]); ``` ## ページネーションとの組み合わせ ページネーション結果をリソースに渡すだけで、`meta` と `links` が自動的に付加されます。 ```php theme={null} Route::get('/users', function () { return UserResource::collection(User::paginate(15)); }); ``` または: ```php theme={null} return User::paginate(15)->toResourceCollection(); ``` レスポンス例: ```json theme={null} { "data": [ { "id": 1, "name": "山田 太郎" }, { "id": 2, "name": "鈴木 花子" } ], "links": { "first": "https://example.com/users?page=1", "last": "https://example.com/users?page=5", "prev": null, "next": "https://example.com/users?page=2" }, "meta": { "current_page": 1, "from": 1, "last_page": 5, "per_page": 15, "to": 15, "total": 72 } } ``` ページネーションレスポンスでは、`withoutWrapping()` を呼んでいても `data` キーは必ず付きます。ページネーションの `meta` や `links` キーと共存させるためです。 ## データラッピングの無効化 デフォルトでは最外層のリソースが `data` キーにラップされます。これを無効にするには `AppServiceProvider` の `boot()` 内で `withoutWrapping()` を呼びます。 ```php theme={null} use Illuminate\Http\Resources\Json\JsonResource; public function boot(): void { JsonResource::withoutWrapping(); } ``` `withoutWrapping()` は最外層のラッピングのみに影響します。自分で定義した `data` キーは削除されません。 ## 実践例:ユーザーAPIの実装 ユーザー管理APIを例に、リソースを使った一貫したレスポンス設計を示します。 ```mermaid theme={null} flowchart TD A["GET /api/users"] --> B["UserController@index"] C["GET /api/users/:id"] --> D["UserController@show"] B --> E["UserResource::collection(paginate)"] D --> F["new UserResource(user)"] E --> G["{ data: [...], meta, links }"] F --> H["{ data: { id, name, email, ... } }"] ``` ### UserResource ```php theme={null} $this->id, 'name' => $this->name, 'email' => $this->email, 'avatar_url' => $this->avatar_url, 'role' => $this->role, // 管理者にのみ表示 'created_at' => $this->when( $request->user()?->isAdmin(), $this->created_at->toDateString() ), // ロードされているときだけ含める 'posts' => PostResource::collection($this->whenLoaded('posts')), 'posts_count' => $this->whenCounted('posts'), ]; } } ``` ### UserController ```php theme={null} paginate(20); return UserResource::collection($users); } public function show(User $user): UserResource { $user->loadCount('posts')->load('posts'); return new UserResource($user); } } ``` ## 関連ページ リレーションの定義方法とEagerローディングを確認します。 ページネーション結果をAPIリソースと組み合わせる方法を確認します。 # Eloquentシリアライゼーション Source: https://kawax.biz/jp/eloquent-serialization EloquentモデルをJSON・配列に変換する方法、属性の表示・非表示制御、追加属性の設定、日付フォーマットのカスタマイズを解説します。 ## はじめに LaravelでAPIを構築する際、モデルとそのリレーションを配列やJSONに変換する必要がよくあります。 Eloquentにはこれらの変換を行うための便利なメソッドと、シリアライズされたモデルの表現にどの属性を含めるかを制御する機能が含まれています。 Eloquentモデルとコレクションのより堅牢なJSONシリアライゼーションの方法については、[Eloquent APIリソース](./eloquent-resources)のドキュメントを参照してください。 ## モデルと配列 ### 配列へのシリアライゼーション モデルと読み込まれた[リレーション](/jp/eloquent-relationships)を配列に変換するには `toArray` メソッドを使います。 このメソッドは再帰的に動作し、すべての属性とすべてのリレーション(リレーションのリレーションも含む)が配列に変換されます。 ```php theme={null} use App\Models\User; $user = User::with('roles')->first(); return $user->toArray(); ``` `attributesToArray` メソッドを使うと、リレーションを除いたモデルの属性だけを配列に変換できます。 ```php theme={null} $user = User::first(); return $user->attributesToArray(); ``` モデルのコレクション全体を配列に変換するには、コレクションインスタンスの `toArray` メソッドを呼び出します。 ```php theme={null} $users = User::all(); return $users->toArray(); ``` ### JSONへのシリアライゼーション モデルをJSONに変換するには `toJson` メソッドを使います。 `toArray` と同様に `toJson` メソッドも再帰的なので、すべての属性とリレーションがJSONに変換されます。 [PHPがサポートするJSONエンコードオプション](https://www.php.net/manual/ja/function.json-encode.php)を指定することもできます。 ```php theme={null} use App\Models\User; $user = User::find(1); return $user->toJson(); return $user->toJson(JSON_PRETTY_PRINT); ``` モデルまたはコレクションを文字列にキャストすることもでき、その場合はモデルまたはコレクションの `toJson` メソッドが自動的に呼び出されます。 ```php theme={null} return (string) User::find(1); ``` モデルとコレクションは文字列にキャストされるとJSONに変換されるので、アプリケーションのルートやコントローラーからEloquentオブジェクトを直接返せます。 Laravelはルートやコントローラーから返されるEloquentモデルとコレクションを自動的にJSONにシリアライズします。 ```php theme={null} Route::get('/users', function () { return User::all(); }); ``` #### リレーション EloquentモデルがJSONに変換される際、読み込まれたリレーションはJSONオブジェクトの属性として自動的に含まれます。 また、Eloquentのリレーションメソッドは「キャメルケース」のメソッド名で定義されますが、リレーションのJSON属性は「スネークケース」になります。 ## 属性の表示制御 ### 属性の非表示 モデルの配列またはJSON表現に含まれるパスワードなどの属性を制限したい場合があります。 そのためにはモデルに `Hidden` 属性を使います。 `Hidden` 属性にリストされた属性はモデルのシリアライズされた表現に含まれません。 ```php theme={null} リレーションを非表示にするには、EloquentモデルのHidden属性にリレーションのメソッド名を追加します。 ### 属性の公開 `Visible` 属性を使って、モデルの配列およびJSON表現に含まれるべき属性の「許可リスト」を定義することもできます。 `Visible` 属性に存在しないすべての属性は、モデルが配列やJSONに変換される際に非表示になります。 ```php theme={null} makeVisible('attribute')->toArray(); return $user->mergeVisible(['name', 'email'])->toArray(); ``` 逆に、通常は表示される属性を非表示にするには、`makeHidden` または `mergeHidden` メソッドを使います。 ```php theme={null} return $user->makeHidden('attribute')->toArray(); return $user->mergeHidden(['name', 'email'])->toArray(); ``` 表示または非表示の属性をすべて一時的にオーバーライドするには、`setVisible` および `setHidden` メソッドをそれぞれ使います。 ```php theme={null} return $user->setVisible(['id', 'name'])->toArray(); return $user->setHidden(['email', 'password', 'remember_token'])->toArray(); ``` ## 追加属性 モデルを配列やJSONに変換する際、データベースに対応するカラムを持たない属性を追加したい場合があります。 そのためにはまず値の[アクセサ](/jp/eloquent-mutators)を定義します。 ```php theme={null} 'yes', ); } } ``` アクセサをモデルの配列およびJSON表現に常に追加したい場合は、モデルに `Appends` 属性を使います。 アクセサのPHPメソッドは「キャメルケース」で定義されていますが、属性名は通常「スネークケース」でのシリアライズ表現を使います。 ```php theme={null} append('is_admin')->toArray(); return $user->mergeAppends(['is_admin', 'status'])->toArray(); return $user->setAppends(['is_admin'])->toArray(); ``` 追加されたすべてのプロパティをモデルから削除するには `withoutAppends` メソッドを使います。 ```php theme={null} return $user->withoutAppends()->toArray(); ``` ## 日付のシリアライゼーション ### デフォルトの日付フォーマットのカスタマイズ `serializeDate` メソッドをオーバーライドすることで、デフォルトのシリアライゼーションフォーマットをカスタマイズできます。 このメソッドはデータベースへの保存のための日付フォーマットには影響しません。 ```php theme={null} /** * 配列・JSONシリアライゼーションのために日付を準備する。 */ protected function serializeDate(DateTimeInterface $date): string { return $date->format('Y-m-d'); } ``` ### 属性ごとの日付フォーマットのカスタマイズ モデルの[キャスト宣言](/jp/eloquent-mutators#属性キャスト)で日付フォーマットを指定することで、個々のEloquent日付属性のシリアライゼーションフォーマットをカスタマイズできます。 ```php theme={null} protected function casts(): array { return [ 'birthday' => 'date:Y-m-d', 'joined_at' => 'datetime:Y-m-d H:00', ]; } ``` # 暗号化(Encryption) Source: https://kawax.biz/jp/encryption LaravelのCryptファサードを使ったAES-256-CBCによる値の暗号化・復号方法を解説します。APP_KEYの生成、モデルキャスト、個人情報の安全な保存まで幅広くカバーします。 ## Cryptファサードとは Laravelの暗号化サービスは、**OpenSSL** の AES-256-CBC 暗号化(または AES-128-CBC)を使って、値の暗号化・復号をシンプルなインターフェースで提供します。 Laravelで暗号化されたすべての値は、**メッセージ認証コード(MAC)** で署名されています。 これにより、暗号化後の値が改ざんされた場合は復号できなくなります。 ```mermaid theme={null} flowchart LR A["平文データ"] -->|"Crypt::encryptString()"| B["暗号化された値
    (MACで署名済み)"] B -->|"Crypt::decryptString()"| A B -->|改ざん検知| C["DecryptException"] ``` *** ## 設定 ### APP\_KEYの生成 暗号化を使う前に、`config/app.php` の `key` 設定が必要です。 この値は `APP_KEY` 環境変数から読み込まれます。 `php artisan key:generate` コマンドを使って安全なキーを生成してください。 PHPのセキュアな乱数生成器を使って暗号学的に安全なキーが生成されます。 ```shell theme={null} php artisan key:generate ``` Laravelのインストール時に通常は自動生成されます。生成されたキーは `.env` ファイルに保存されます。 ```ini theme={null} APP_KEY=base64:J63qRTDLub5NuZvP+kb8YIorGS6qFYHKVo6u7179stY= ``` `APP_KEY` は絶対に公開しないでください。このキーが漏洩すると、暗号化されたデータがすべて復号されてしまいます。 ### 暗号化キーのローテーション 暗号化キーを変更すると、すべての認証済みユーザーセッションがログアウトされます。 Laravelはセッションクッキーを含むすべてのクッキーを暗号化しているためです。 また、以前のキーで暗号化されたデータは復号できなくなります。 この問題を軽減するため、`APP_PREVIOUS_KEYS` に古いキーをコンマ区切りで列挙できます。 ```ini theme={null} APP_KEY="base64:J63qRTDLub5NuZvP+kb8YIorGS6qFYHKVo6u7179stY=" APP_PREVIOUS_KEYS="base64:2nLsGFGzyoae2ax3EF2Lyq/hH6QghBGLIq5uL+Gp8/w=" ``` Laravelは暗号化には常に現在のキーを使用しますが、復号時は現在のキーで失敗した場合に古いキーを順番に試します。 これにより、キーのローテーション中もユーザーはアプリケーションを継続して使用できます。 *** ## 暗号化 `Crypt` ファサードの `encryptString` メソッドを使って値を暗号化します。 暗号化された値はOpenSSLとAES-256-CBC暗号を使用し、MACで署名されます。 ```php theme={null} user()->fill([ 'token' => Crypt::encryptString($request->token), ])->save(); return redirect('/secrets'); } } ``` `encryptString` はシリアライズ**なし**で文字列を暗号化します。オブジェクトや配列を暗号化する場合は `encrypt` を使用してください。 | メソッド | 用途 | | - | - | | `Crypt::encryptString($value)` | 文字列をそのまま暗号化 | | `Crypt::encrypt($value)` | 値をシリアライズしてから暗号化(配列・オブジェクト対応) | *** ## 復号 `Crypt` ファサードの `decryptString` メソッドで暗号化された値を復号します。 MACが無効な場合など、正常に復号できないときは `DecryptException` がスローされます。 ```php theme={null} use Illuminate\Contracts\Encryption\DecryptException; use Illuminate\Support\Facades\Crypt; try { $decrypted = Crypt::decryptString($encryptedValue); } catch (DecryptException $e) { // 復号に失敗した場合の処理 abort(400, '無効なデータです。'); } ``` | メソッド | 用途 | | - | - | | `Crypt::decryptString($value)` | 文字列として復号 | | `Crypt::decrypt($value)` | 復号してアンシリアライズ(配列・オブジェクト対応) | *** ## モデルのキャスト Eloquentモデルで `encrypted` キャストを使うと、属性の暗号化・復号を自動で行えます。 ```php theme={null} 'encrypted', // 文字列の暗号化 'profile_data' => 'encrypted:array', // 配列を暗号化して保存 'preferences' => 'encrypted:collection',// コレクションを暗号化して保存 'metadata' => 'encrypted:object', // オブジェクトを暗号化して保存 'settings' => 'encrypted:json', // JSONとして暗号化して保存 ]; } } ``` キャストを設定すると、モデルへの値の代入・取得時に自動で暗号化・復号されます。 ```php theme={null} // 自動的に暗号化されてDBに保存される $user->secret_note = '秘密のメモ'; $user->save(); // 自動的に復号されて取得される echo $user->secret_note; // '秘密のメモ' ``` `encrypted:*` キャストは内部で `Crypt::encrypt` と `Crypt::decrypt` を使用します。 データベースのカラム型は `text` または `longText` を推奨します。 *** ## 実用例:個人情報の暗号化保存 個人情報(マイナンバー、クレジットカード番号など)をデータベースに安全に保存する典型的なパターンです。 ### マイグレーション ```php theme={null} Schema::create('profiles', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->constrained(); $table->text('my_number')->nullable(); // 暗号化された値はtextで保存 $table->text('bank_account')->nullable(); $table->timestamps(); }); ``` ### モデル ```php theme={null} 'encrypted', 'bank_account' => 'encrypted', ]; } } ``` ### コントローラー ```php theme={null} use App\Models\Profile; use Illuminate\Http\Request; class ProfileController extends Controller { public function store(Request $request) { $request->validate([ 'my_number' => ['required', 'string'], 'bank_account' => ['required', 'string'], ]); // モデルが自動的に暗号化して保存する Profile::create([ 'user_id' => $request->user()->id, 'my_number' => $request->my_number, 'bank_account' => $request->bank_account, ]); return redirect('/profile'); } public function show(Request $request) { $profile = $request->user()->profile; // モデルが自動的に復号して返す return view('profile.show', ['profile' => $profile]); } } ``` *** ## まとめ | やりたいこと | 方法 | | - | - | | APP\_KEYを生成 | `php artisan key:generate` | | 文字列を暗号化 | `Crypt::encryptString($value)` | | 文字列を復号 | `Crypt::decryptString($value)` | | 配列・オブジェクトを暗号化 | `Crypt::encrypt($value)` | | モデル属性を自動暗号化 | `'column' => 'encrypted'` キャスト | | キーローテーション | `APP_PREVIOUS_KEYS` に旧キーを列挙 | ## 次のステップ パスワードの安全なハッシュ化と検証方法を学びます。 ポリシーとゲートを使ったアクセス制御を解説します。 # ファサード Source: https://kawax.biz/jp/facades Laravelのファサードの仕組みと使い方、リアルタイムファサード、テスト方法、依存注入との使い分けを解説します。 ## ファサードとは ファサードは、アプリケーションの[サービスコンテナ](/jp/service-container)で利用可能なクラスへの「静的」なインターフェースを提供します。Laravelにはほぼすべての機能にアクセスできる多くのファサードが同梱されています。 Laravelのファサードはサービスコンテナ内の実体クラスへの「静的プロキシ」として機能し、従来の静的メソッドよりもテストしやすく柔軟性を保ちながら、簡潔で表現力豊かな構文を提供します。 すべてのLaravelファサードは `Illuminate\Support\Facades` 名前空間に定義されています。 ```php theme={null} use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Route; Route::get('/cache', function () { return Cache::get('key'); }); ``` ファサードの仕組みを完全に理解していなくても問題ありません。まずは使い方を覚えて、Laravelを学び続けましょう。 ## ファサードの仕組み Laravelアプリケーションでは、ファサードはコンテナからオブジェクトへのアクセスを提供するクラスです。この仕組みは `Facade` クラスによって実現されています。Laravelのすべてのファサード、およびカスタムファサードは、基底クラス `Illuminate\Support\Facades\Facade` を継承します。 `Facade` 基底クラスは `__callStatic()` マジックメソッドを使って、ファサードへの呼び出しをコンテナから解決されたオブジェクトへ委譲します。 ```mermaid theme={null} flowchart LR A["Cache::get('key')"] --> B["Facade::__callStatic()"] B --> C["getFacadeAccessor()
    → 'cache'"] C --> D["サービスコンテナ
    make('cache')"] D --> E["実クラスのインスタンス
    CacheManager"] E --> F["get('key')
    を実行"] ``` ```php theme={null} $user]); } } ``` ファイルの先頭で `Cache` ファサードをインポートしています。このファサードは `Illuminate\Contracts\Cache\Factory` インターフェースの実装へのアクセスをプロキシします。ファサードを使った呼び出しはすべて、Laravelのキャッシュサービスの内部インスタンスに渡されます。 `Illuminate\Support\Facades\Cache` クラスを見ると、静的メソッド `get` は存在しません。 ```php theme={null} class Cache extends Facade { /** * コンポーネントの登録名を取得する */ protected static function getFacadeAccessor(): string { return 'cache'; } } ``` `Cache` ファサードは基底 `Facade` クラスを継承し、`getFacadeAccessor()` メソッドを定義しています。このメソッドはサービスコンテナのバインディング名を返します。ユーザーが `Cache` ファサードの静的メソッドを呼び出すと、Laravelはサービスコンテナから `cache` バインディングを解決し、そのオブジェクトに対してリクエストされたメソッド(この場合 `get`)を実行します。 ## ファサードを使う場面・使わない場面 ### ファサードのメリット ファサードには多くのメリットがあります。手動で注入・設定しなければならない長いクラス名を覚えることなく、Laravelの機能を使える簡潔で覚えやすい構文を提供します。また、PHPのダイナミックメソッドの独自の使い方により、テストが容易です。 ### スコープクリープに注意する ファサードを使う際の主な危険性はクラスの「スコープクリープ」です。ファサードは使いやすく注入を必要としないため、クラスが肥大化して多くのファサードを使い続けてしまいがちです。依存注入を使えば、大きなコンストラクターが視覚的なフィードバックを与えてくれます。ファサードを使う場合は、クラスのサイズに注意して責務の範囲を狭く保ちましょう。 クラスが大きくなりすぎたと感じたら、複数の小さなクラスに分割することを検討してください。 ### ファサード vs. 依存注入 依存注入の主なメリットの一つは、注入されたクラスの実装を差し替えられることです。これはテスト時に役立ちます。モックやスタブを注入して、スタブに対してさまざまなメソッドが呼ばれたことをアサートできます。 真に静的なクラスメソッドはモックやスタブにできないのが通常ですが、ファサードはダイナミックメソッドを使ってサービスコンテナから解決されたオブジェクトへのメソッド呼び出しをプロキシするため、注入されたクラスインスタンスをテストするのと同じようにファサードをテストできます。 ```php theme={null} use Illuminate\Support\Facades\Cache; Route::get('/cache', function () { return Cache::get('key'); }); ``` このルートに対して、次のテストを書いて `Cache::get` が期待する引数で呼び出されたことを検証できます。 ```php theme={null} use Illuminate\Support\Facades\Cache; test('basic example', function () { Cache::shouldReceive('get') ->with('key') ->andReturn('value'); $response = $this->get('/cache'); $response->assertSee('value'); }); ``` ### ファサード vs. ヘルパー関数 ファサードに加えて、Laravelはビューの生成、イベントの発火、ジョブのディスパッチ、HTTPレスポンスの送信など、一般的なタスクを実行できる「ヘルパー」関数を提供しています。多くのヘルパー関数は対応するファサードと同じ機能を実行します。 ```php theme={null} // ファサードを使った呼び出し return Illuminate\Support\Facades\View::make('profile'); // ヘルパー関数を使った同等の呼び出し return view('profile'); ``` ファサードとヘルパー関数の間には実質的な違いはありません。ヘルパー関数を使っている場合でも、対応するファサードと同じようにテストできます。 ```php theme={null} use Illuminate\Support\Facades\Cache; test('cache helper test', function () { Cache::shouldReceive('get') ->with('key') ->andReturn('value'); $response = $this->get('/cache'); $response->assertSee('value'); }); ``` ## リアルタイムファサード リアルタイムファサードを使うと、アプリケーション内の任意のクラスをファサードとして扱えます。使い方を説明するために、まずリアルタイムファサードを使わないコードを見てみましょう。 例えば、`Podcast` モデルに `publish` メソッドがあるとします。ただし、ポッドキャストを公開するには `Publisher` インスタンスを注入する必要があります。 ```php theme={null} update(['publishing' => now()]); $publisher->publish($this); } } ``` リアルタイムファサードを使うと、同じテスト容易性を維持しながら `Publisher` インスタンスを明示的に渡す必要がなくなります。リアルタイムファサードを生成するには、インポートするクラスの名前空間に `Facades` というプレフィックスを付けます。 ```php theme={null} update(['publishing' => now()]); Publisher::publish($this); } } ``` リアルタイムファサードが使われると、`Facades` プレフィックスの後に現れるインターフェースまたはクラス名の部分を使ってサービスコンテナからパブリッシャーの実装が解決されます。 ```php theme={null} use(RefreshDatabase::class); test('podcast can be published', function () { $podcast = Podcast::factory()->create(); Publisher::shouldReceive('publish')->once()->with($podcast); $podcast->publish(); }); ``` リアルタイムファサードは、テスト時のモックを簡単にしながら、引数として渡す必要をなくしたい場合に便利です。 ## ファサードのテスト ファサードをテストするには `shouldReceive` メソッドを使います。これにより、Mockeryのモックインスタンスが返されます。ファサードは実際にサービスコンテナによって解決・管理されるため、通常の静的クラスよりもはるかにテストしやすいです。 ```php theme={null} use Illuminate\Support\Facades\Cache; test('ユーザープロフィールを表示する', function () { Cache::shouldReceive('get') ->once() ->with('user:1') ->andReturn(['name' => 'Taylor']); $response = $this->get('/users/1'); $response->assertSee('Taylor'); }); ``` よく使うモックメソッドは次のとおりです。 | メソッド | 説明 | | - | - | | `shouldReceive('method')` | メソッドが呼ばれることを期待する | | `once()` | 1回だけ呼ばれることを期待する | | `times(n)` | n回呼ばれることを期待する | | `with(args)` | 特定の引数で呼ばれることを期待する | | `andReturn(value)` | 指定した値を返す | | `andReturnNull()` | nullを返す | ## 主要なファサード一覧 よく使うファサードとその実体クラス、サービスコンテナのバインディング名の対応表です。 | ファサード | クラス | バインディング | | - | - | - | | `App` | `Illuminate\Foundation\Application` | `app` | | `Auth` | `Illuminate\Auth\AuthManager` | `auth` | | `Cache` | `Illuminate\Cache\CacheManager` | `cache` | | `Config` | `Illuminate\Config\Repository` | `config` | | `Cookie` | `Illuminate\Cookie\CookieJar` | `cookie` | | `Crypt` | `Illuminate\Encryption\Encrypter` | `encrypter` | | `DB` | `Illuminate\Database\DatabaseManager` | `db` | | `Event` | `Illuminate\Events\Dispatcher` | `events` | | `File` | `Illuminate\Filesystem\Filesystem` | `files` | | `Gate` | `Illuminate\Contracts\Auth\Access\Gate` | — | | `Hash` | `Illuminate\Contracts\Hashing\Hasher` | `hash` | | `Http` | `Illuminate\Http\Client\Factory` | — | | `Log` | `Illuminate\Log\LogManager` | `log` | | `Mail` | `Illuminate\Mail\Mailer` | `mailer` | | `Notification` | `Illuminate\Notifications\ChannelManager` | — | | `Queue` | `Illuminate\Queue\QueueManager` | `queue` | | `RateLimiter` | `Illuminate\Cache\RateLimiter` | — | | `Redirect` | `Illuminate\Routing\Redirector` | `redirect` | | `Request` | `Illuminate\Http\Request` | `request` | | `Route` | `Illuminate\Routing\Router` | `router` | | `Schema` | `Illuminate\Database\Schema\Builder` | — | | `Session` | `Illuminate\Session\SessionManager` | `session` | | `Storage` | `Illuminate\Filesystem\FilesystemManager` | `filesystem` | | `URL` | `Illuminate\Routing\UrlGenerator` | `url` | | `Validator` | `Illuminate\Validation\Factory` | `validator` | | `View` | `Illuminate\View\Factory` | `view` | ## 次のステップ ファサードと対をなすContractsの概念と使い分けを学びます。 # ハッシュ(Hashing) Source: https://kawax.biz/jp/hashing Laravel の Hash ファサードを使ったパスワードの安全なハッシュ化・検証方法を解説します。bcrypt と Argon2 アルゴリズムの設定や使い方を説明します。 ## ハッシュとは ハッシュ(Hashing)とは、パスワードなどの平文データを一方向の変換で固定長の文字列に変換する処理です。 同じ入力からは常に同じハッシュが生成されますが、ハッシュから元の平文を復元することはできません。 Laravel の `Hash` ファサードは、安全なパスワード保存のために **bcrypt** と **Argon2** のハッシュアルゴリズムをサポートしています。 ### アルゴリズムの比較 | アルゴリズム | 特徴 | 推奨用途 | | - | - | - | | **bcrypt** | ワークファクター(rounds)で計算コストを調整できる。デフォルト | 一般的な Web アプリケーション | | **argon2i** | メモリ・時間・スレッド数を調整できる。サイドチャネル攻撃に強い | 高セキュリティが求められる場面 | | **argon2id** | argon2i と argon2d のハイブリッド。PHC 推奨 | 新規プロジェクトで Argon2 を使う場合 | bcrypt の「ワークファクター」は、ハッシュ生成にかかる時間を制御します。ハッシュが遅いほどブルートフォース攻撃への耐性が上がります。ハードウェアが高速になるにつれてワークファクターを上げることで、セキュリティを維持できます。 ### ハッシュ化と検証のフロー ```mermaid theme={null} flowchart LR A[平文パスワード] -->|Hash::make| B["ハッシュ値
    DBに保存"] C[ログイン時の入力] -->|Hash::check| D{一致?} B --> D D -->|true| E[認証成功] D -->|false| F[認証失敗] ``` *** ## 設定 デフォルトでは Laravel は `bcrypt` ドライバーを使用します。`HASH_DRIVER` 環境変数で変更できます。 ```ini theme={null} # .env HASH_DRIVER=bcrypt # bcrypt / argon / argon2id ``` ハッシュ設定をカスタマイズする場合は、`config:publish` で設定ファイルを公開します。 ```shell theme={null} php artisan config:publish hashing ``` 公開後、`config/hashing.php` でデフォルトのワークファクターなどを変更できます。 *** ## 基本的な使い方 ### パスワードをハッシュ化する `Hash::make()` にパスワードの平文を渡すとハッシュ値が返ります。このハッシュ値をデータベースに保存します。 ```php theme={null} validate([ 'password' => ['required', 'min:8', 'confirmed'], ]); $request->user()->fill([ 'password' => Hash::make($request->password), ])->save(); return redirect('/profile'); } } ``` ハッシュ値はデータベースに保存しますが、平文パスワードは絶対に保存しないでください。 ### bcrypt のワークファクターを調整する `rounds` オプションでハッシュ生成の計算コストを調整できます。値が大きいほど安全ですが、処理時間も増えます。 デフォルト値(12)はほとんどのアプリケーションで適切です。 ```php theme={null} $hashed = Hash::make('plain-text-password', [ 'rounds' => 14, ]); ``` ### Argon2 のワークファクターを調整する Argon2 を使う場合は `memory`・`time`・`threads` オプションで計算コストを調整できます。 ```php theme={null} $hashed = Hash::make('plain-text-password', [ 'memory' => 65536, // KiB 単位 'time' => 4, 'threads' => 2, ]); ``` | オプション | 説明 | デフォルト | | - | - | - | | `memory` | 使用するメモリ量(KiB) | 65536 | | `time` | 反復回数 | 4 | | `threads` | 使用するスレッド数 | 1 | Argon2 のオプションの詳細は [PHP 公式ドキュメント](https://secure.php.net/manual/ja/function.password-hash.php) を参照してください。 *** ## パスワードの検証 `Hash::check()` を使うと、平文パスワードが保存済みのハッシュと一致するか確認できます。 ログイン処理の中で使用する典型的な例です。 ```php theme={null} use Illuminate\Support\Facades\Hash; if (Hash::check($request->password, $user->password)) { // パスワードが一致した } else { // パスワードが一致しない } ``` `Auth::attempt()` を使う場合はこの処理が自動で行われるため、直接呼び出す必要はありません。 `Hash::check()` が役立つのは、現在のパスワードの確認を手動で行うときです。 ```php theme={null} // パスワード変更フォームで現在のパスワードを確認する例 if (! Hash::check($request->current_password, $request->user()->password)) { return back()->withErrors(['current_password' => '現在のパスワードが正しくありません。']); } ``` *** ## 再ハッシュの判定 `Hash::needsRehash()` を使うと、ハッシュ生成時のワークファクターが現在の設定と異なるかどうかを確認できます。 ワークファクターを変更した際に、既存のハッシュを新しい設定で更新するために使います。 ```php theme={null} use Illuminate\Support\Facades\Hash; if (Hash::needsRehash($user->password)) { $user->update([ 'password' => Hash::make($plainTextPassword), ]); } ``` ログイン成功時に再ハッシュを行う例です。 ```mermaid theme={null} flowchart TD A[ログイン成功] --> B{needsRehash?} B -->|true| C[新しいワークファクターで
    再ハッシュして保存] B -->|false| D[そのまま続行] C --> D ``` ```php theme={null} // ログイン処理の中で再ハッシュを行う if (Auth::attempt($credentials)) { if (Hash::needsRehash(Auth::user()->password)) { Auth::user()->update([ 'password' => Hash::make($credentials['password']), ]); } return redirect()->intended('/dashboard'); } ``` ハードウェアの性能向上に合わせてワークファクターを定期的に見直し、セキュリティを維持しましょう。`needsRehash()` を活用することで、ユーザーのログイン時に自然なタイミングでパスワードを更新できます。 *** ## ハッシュアルゴリズム検証 デフォルトでは、`Hash::check()` はハッシュが現在設定されているアルゴリズムで生成されたものかを検証します。 アルゴリズムが異なる場合は `RuntimeException` がスローされます。 これにより、ハッシュアルゴリズムの改ざんによる攻撃を防げます。 アルゴリズムを移行中など、複数のアルゴリズムを同時にサポートする必要がある場合は、`HASH_VERIFY` を `false` に設定してこの検証を無効化できます。 ```ini theme={null} # .env HASH_VERIFY=false ``` `HASH_VERIFY=false` にするとアルゴリズム検証が無効になります。移行期間中のみ使用し、移行完了後は `true`(デフォルト)に戻してください。 *** ## まとめ | やりたいこと | 方法 | | - | - | | パスワードをハッシュ化する | `Hash::make($password)` | | ハッシュを検証する | `Hash::check($plain, $hash)` | | 再ハッシュが必要か確認する | `Hash::needsRehash($hash)` | | ハッシュドライバーを変更する | `HASH_DRIVER` 環境変数 | | アルゴリズム検証を無効にする | `HASH_VERIFY=false` | ## 次のステップ `Hash::check()` を使ったログイン処理の実装など、認証の全体像を学びます。 # ヘルパー関数 Source: https://kawax.biz/jp/helpers Laravelが提供するグローバルヘルパー関数と Arr・Number クラスを実用例とともに解説します。 ## ヘルパー関数とは Laravelは多数のグローバルPHP関数(ヘルパー)を提供しています。フレームワーク自体が内部で使用しているものがほとんどですが、アプリケーション内でも自由に利用できます。 ヘルパーは大きく以下のカテゴリに分かれています。 * **配列・オブジェクト** — `Arr::` クラスと `data_get()` などの関数 * **数値** — `Number::` クラス * **パス** — `app_path()`, `storage_path()` などのパス取得関数 * **URL** — `route()`, `url()`, `asset()` などのURL生成関数 * **その他** — `config()`, `collect()`, `auth()` などよく使うユーティリティ ヘルパー関数は `use` 宣言不要でどこからでも呼び出せます。`Arr` や `Number` クラスを使う場合は `use Illuminate\Support\Arr;` などのインポートが必要です。 ## 配列ヘルパー(Arr クラス) ### `Arr::get()` — ドット記法でネストした値を取得 多次元配列からドット記法(`foo.bar.baz`)でネストした値を安全に取得します。キーが存在しない場合はデフォルト値を返します。 ```php theme={null} use Illuminate\Support\Arr; $config = [ 'database' => [ 'connections' => [ 'mysql' => ['host' => '127.0.0.1', 'port' => 3306], ], ], ]; $host = Arr::get($config, 'database.connections.mysql.host'); // '127.0.0.1' // キーが存在しない場合のデフォルト値 $charset = Arr::get($config, 'database.connections.mysql.charset', 'utf8mb4'); // 'utf8mb4' ``` `data_get()` グローバル関数も同じ機能を持ちます。Eloquentのリレーションのような`Arrayable`なオブジェクトにも対応しているため、より汎用的な場面で使えます。 ### `Arr::set()` — ネストした配列に値を設定 ```php theme={null} use Illuminate\Support\Arr; $array = ['products' => ['desk' => ['price' => 100]]]; Arr::set($array, 'products.desk.price', 200); // ['products' => ['desk' => ['price' => 200]]] ``` ### `Arr::has()` — キーの存在を確認 ```php theme={null} use Illuminate\Support\Arr; $array = ['product' => ['name' => 'Desk', 'price' => 100]]; $hasName = Arr::has($array, 'product.name'); // true // 複数キーをすべて持つか確認 $hasAll = Arr::has($array, ['product.name', 'product.price']); // true ``` ### `Arr::only()` / `Arr::except()` — キーを絞り込む・除外する リクエストデータや設定配列から必要なキーだけを取り出したり、不要なキーを除外したりするときに便利です。 ```php theme={null} use Illuminate\Support\Arr; $user = [ 'id' => 1, 'name' => '山田太郎', 'email' => 'yamada@example.com', 'password' => 'secret', 'role' => 'admin', ]; // 必要なキーだけ取り出す $safe = Arr::only($user, ['id', 'name', 'email']); // ['id' => 1, 'name' => '山田太郎', 'email' => 'yamada@example.com'] // password だけ除外する $public = Arr::except($user, ['password']); // ['id' => 1, 'name' => '山田太郎', 'email' => '...', 'role' => 'admin'] ``` ### `Arr::pluck()` — ネストした配列から特定キーを抜き出す ```php theme={null} use Illuminate\Support\Arr; $records = [ ['user' => ['id' => 1, 'name' => '山田太郎']], ['user' => ['id' => 2, 'name' => '鈴木花子']], ]; $names = Arr::pluck($records, 'user.name'); // ['山田太郎', '鈴木花子'] // 第3引数でキーを指定 $nameById = Arr::pluck($records, 'user.name', 'user.id'); // [1 => '山田太郎', 2 => '鈴木花子'] ``` ### `Arr::first()` / `Arr::last()` — 条件を満たす最初・最後の要素 ```php theme={null} use Illuminate\Support\Arr; $prices = [150, 80, 200, 50, 120]; // 100以上の最初の価格 $first = Arr::first($prices, fn ($price) => $price >= 100); // 150 // 条件を満たさない場合のデフォルト値 $first = Arr::first($prices, fn ($price) => $price >= 500, 0); // 0 // 最後の要素(条件なし) $last = Arr::last($prices); // 120 ``` ### `Arr::flatten()` — 多次元配列を1次元に平坦化 ```php theme={null} use Illuminate\Support\Arr; $tags = [ 'frontend' => ['html', 'css', 'javascript'], 'backend' => ['php', 'laravel', 'mysql'], ]; $allTags = Arr::flatten($tags); // ['html', 'css', 'javascript', 'php', 'laravel', 'mysql'] ``` ### `Arr::wrap()` — 値を確実に配列にする 値が配列でない場合に配列でラップします。`null` は空配列になります。関数の引数を柔軟に受け付ける際に役立ちます。 ```php theme={null} use Illuminate\Support\Arr; Arr::wrap('Laravel'); // ['Laravel'] Arr::wrap(['Laravel', 'PHP']); // ['Laravel', 'PHP'] Arr::wrap(null); // [] ``` ### `Arr::sort()` — 値でソート ```php theme={null} use Illuminate\Support\Arr; $fruits = ['banana', 'apple', 'cherry']; $sorted = Arr::sort($fruits); // ['apple', 'banana', 'cherry'] // クロージャでソートキーを指定 $products = [ ['name' => 'ノートPC', 'price' => 120000], ['name' => 'マウス', 'price' => 3500], ['name' => 'キーボード', 'price' => 8000], ]; $byPrice = Arr::sort($products, fn ($product) => $product['price']); // マウス, キーボード, ノートPC の順 ``` ### `Arr::dot()` / `Arr::undot()` — ドット記法に変換・復元 ```php theme={null} use Illuminate\Support\Arr; $nested = [ 'user' => [ 'profile' => ['name' => '山田太郎', 'age' => 28], ], ]; $dotted = Arr::dot($nested); // ['user.profile.name' => '山田太郎', 'user.profile.age' => 28] // 元の構造に戻す $restored = Arr::undot($dotted); // ['user' => ['profile' => ['name' => '山田太郎', 'age' => 28]]] ``` ### `Arr::join()` — 配列を文字列に結合 ```php theme={null} use Illuminate\Support\Arr; $items = ['PHP', 'Laravel', 'MySQL']; Arr::join($items, ', '); // 'PHP, Laravel, MySQL' // 最後の要素の前だけ別の文字列を使う Arr::join($items, ', ', ' と '); // 'PHP, Laravel と MySQL' ``` ## `data_get()` — ネストしたデータへのアクセス `Arr::get()` の汎用版です。配列だけでなく、オブジェクトやEloquentモデル、コレクションにも対応しています。 ```php theme={null} $users = [ ['name' => '山田太郎', 'address' => ['city' => '東京']], ['name' => '鈴木花子', 'address' => ['city' => '大阪']], ]; // ドット記法でアクセス $city = data_get($users, '0.address.city'); // '東京' // ワイルドカードで全要素から取得 $cities = data_get($users, '*.address.city'); // ['東京', '大阪'] ``` ## 数値ヘルパー(Number クラス) ### `Number::format()` — 数値を読みやすい形式に ```php theme={null} use Illuminate\Support\Number; Number::format(1234567.89); // '1,234,567.89' Number::format(1234567.89, precision: 0, locale: 'ja'); // '1,234,568' ``` ### `Number::currency()` — 通貨フォーマット ```php theme={null} use Illuminate\Support\Number; Number::currency(10000, 'JPY', locale: 'ja'); // '¥10,000' Number::currency(29.99, 'USD'); // '$29.99' ``` ### `Number::fileSize()` — ファイルサイズを人間が読める形式に ```php theme={null} use Illuminate\Support\Number; Number::fileSize(1024); // '1 KB' Number::fileSize(1024 * 1024 * 2.5); // '2.5 MB' ``` ### `Number::abbreviate()` — 大きな数を省略表記に ```php theme={null} use Illuminate\Support\Number; Number::abbreviate(1000); // '1K' Number::abbreviate(1500000); // '1.5M' ``` ### `Number::percentage()` — パーセント表示 ```php theme={null} use Illuminate\Support\Number; Number::percentage(75.5, precision: 1); // '75.5%' ``` ## パスヘルパー アプリケーション内のディレクトリパスを取得します。環境やデプロイ先が変わっても正しいパスを返します。 ```php theme={null} // app ディレクトリの絶対パス app_path(); // /var/www/html/app // app/Http/Controllers/UserController.php のパス app_path('Http/Controllers/UserController.php'); // プロジェクトルートからの相対パス base_path('composer.json'); // config ディレクトリ config_path('database.php'); // database ディレクトリ database_path('migrations'); // storage ディレクトリ storage_path('app/uploads'); // public ディレクトリ public_path('css/app.css'); // resources ディレクトリ resource_path('views/welcome.blade.php'); ``` ## URLヘルパー ### `route()` — 名前付きルートのURL生成 ```php theme={null} // ルート定義 // Route::get('/users/{user}', [UserController::class, 'show'])->name('users.show'); // 絶対URL(デフォルト) $url = route('users.show', ['user' => 1]); // 'https://example.com/users/1' // 相対URL $url = route('users.show', ['user' => 1], false); // '/users/1' ``` ### `url()` — 任意パスの絶対URL生成 ```php theme={null} $url = url('user/profile'); // 'https://example.com/user/profile' // URLジェネレーターインスタンスを取得 $current = url()->current(); // 現在のURL $full = url()->full(); // クエリ文字列を含む現在のURL $previous = url()->previous(); // 直前のURL ``` ### `asset()` — 静的アセットのURL生成 ```php theme={null} $url = asset('img/logo.png'); // 'https://example.com/img/logo.png' ``` ### `to_route()` — 名前付きルートへのリダイレクト ```php theme={null} return to_route('users.show', ['user' => 1]); // HTTPステータスとヘッダーも指定可能 return to_route('users.show', ['user' => 1], 302, ['X-Custom' => 'value']); ``` ## その他のよく使うヘルパー ### `config()` — 設定値の取得・設定 ```php theme={null} // ドット記法で設定値を取得 $timezone = config('app.timezone'); // 'Asia/Tokyo' // デフォルト値付き $debug = config('app.debug', false); // 実行時に設定を変更(その処理内限定) config(['app.locale' => 'ja']); ``` ### `collect()` — コレクションの作成 ```php theme={null} $collection = collect([1, 2, 3, 4, 5]); $sum = collect([1, 2, 3])->sum(); // 6 ``` `collect()` はコレクションへの入り口となる最重要ヘルパーです。詳しくは[コレクション](/jp/collections)を参照してください。 ### `auth()` — 認証ユーザーの取得 ```php theme={null} // 現在のユーザーを取得 $user = auth()->user(); // ログイン中か確認 if (auth()->check()) { // ログイン済み処理 } // 特定のガードを使う $admin = auth('admin')->user(); ``` ### `blank()` / `filled()` — 空チェック `blank()` は `null`, 空文字, 空白のみ, 空のコレクション・配列を `true` とみなします。`filled()` はその逆です。 ```php theme={null} blank(''); // true blank(' '); // true blank(null); // true blank(collect()); // true blank(0); // false(0は「空」ではない) blank(false); // false filled('hello'); // true filled(0); // true ``` ### `abort()` / `abort_if()` / `abort_unless()` — HTTP例外 ```php theme={null} // 直接HTTPエラーを投げる abort(404); abort(403, 'このページにアクセスする権限がありません。'); // 条件が true のときエラー abort_if(! auth()->user()->isAdmin(), 403); // 条件が false のときエラー abort_unless(auth()->check(), 401); ``` ### `dd()` / `dump()` — デバッグ ```php theme={null} // 変数を出力してスクリプトを終了 dd($user); dd($user, $orders, $config); // スクリプトを終了せずに出力 dump($query); ``` ### `dispatch()` — ジョブをキューに投入 ```php theme={null} dispatch(new App\Jobs\SendWelcomeEmail($user)); // 即時実行(同期) dispatch_sync(new App\Jobs\GenerateReport($data)); ``` ### `encrypt()` / `decrypt()` — 暗号化 ```php theme={null} $encrypted = encrypt('sensitive-value'); $decrypted = decrypt($encrypted); ``` ### `env()` — 環境変数の取得 ```php theme={null} $debug = env('APP_DEBUG', false); $dbHost = env('DB_HOST', '127.0.0.1'); ``` `env()` はコントローラーやサービスクラスで直接呼ぶのではなく、`config/` ファイル内で呼び出して `config()` 経由でアクセスするのがベストプラクティスです。設定値のキャッシュ(`php artisan config:cache`)が有効な場合、`env()` が意図した値を返さないことがあります。 ## collect() との使い分け `Arr::` クラスは通常のPHP配列を対象にした操作、`collect()` はコレクションオブジェクトを返す操作です。 ```php theme={null} use Illuminate\Support\Arr; // 配列のままで操作したい場合 → Arr クラス $filtered = Arr::where($items, fn ($v) => $v > 0); // 結果は配列 // メソッドチェーンで複数の操作をしたい場合 → collect() $result = collect($items) ->filter(fn ($v) => $v > 0) ->map(fn ($v) => $v * 2) ->values() ->all(); // 結果は配列(all() で取り出す) ``` Eloquentの結果は自動的にコレクションとして返るため、`collect()` で包む必要はありません。単純な配列操作で十分な場合は `Arr::` を使うとシンプルに書けます。 ## まとめ | ヘルパー | 用途 | | - | - | | `Arr::get($array, 'a.b.c', $default)` | ネストした配列から安全に取得 | | `Arr::only($array, $keys)` | 指定キーだけ残す | | `Arr::except($array, $keys)` | 指定キーを除外する | | `Arr::pluck($array, 'key')` | 特定キーの値一覧を取得 | | `Arr::flatten($array)` | 多次元配列を1次元に | | `Arr::wrap($value)` | 確実に配列にする | | `data_get($target, 'a.*.b')` | ワイルドカードでの取得 | | `Number::format($n)` | 数値を読みやすくフォーマット | | `Number::currency($n, 'JPY')` | 通貨表示 | | `Number::fileSize($bytes)` | ファイルサイズ表示 | | `route('name', $params)` | 名前付きルートのURL | | `url('path')` | 絶対URL生成 | | `asset('path')` | 静的アセットURL | | `config('key', $default)` | 設定値の取得 | | `collect($array)` | コレクション作成 | | `auth()->user()` | 現在のユーザー取得 | | `blank($value)` | 空チェック | | `filled($value)` | 空でないか確認 | | `abort(403)` | HTTP例外を投げる | | `dispatch($job)` | ジョブをキューに投入 | | `env('KEY', $default)` | 環境変数の取得 | **`Arr::` クラスを使う場面**: * 単純な配列操作(ネストしたキーへのアクセス、キーの絞り込みなど) * コレクションオブジェクトのオーバーヘッドが不要な場合 * 結果を配列のまま扱いたい場合 **`collect()` を使う場面**: * 複数の操作をメソッドチェーンで表現したい場合 * Eloquentの結果をさらに加工する場合(既にコレクション) * `map`, `filter`, `groupBy` などコレクション固有のメソッドを使いたい場合 # HTTPクライアント Source: https://kawax.biz/jp/http-client LaravelのHTTPクライアントを使って外部APIにリクエストを送る方法を解説します。認証・タイムアウト・リトライ・テストまで網羅します。 ## HTTPクライアントとは LaravelのHTTPクライアントは、[Guzzle](https://docs.guzzlephp.org/en/stable/) をラップした使いやすいAPIです。 `Http` ファサードを通じて、外部のWebサービスやAPIへのHTTPリクエストを簡潔に記述できます。 ```php theme={null} use Illuminate\Support\Facades\Http; $response = Http::get('https://api.example.com/users'); ``` Guzzleは事前にインストールされているため、追加の設定なしにすぐ使い始められます。 ## 基本的なリクエスト ### GETリクエスト ```php theme={null} use Illuminate\Support\Facades\Http; $response = Http::get('https://api.example.com/users'); ``` クエリパラメータは配列で渡せます。 ```php theme={null} $response = Http::get('https://api.example.com/users', [ 'page' => 1, 'per_page' => 20, ]); ``` ### POSTリクエスト データはデフォルトで `application/json` として送信されます。 ```php theme={null} $response = Http::post('https://api.example.com/users', [ 'name' => '山田 太郎', 'email' => 'taro@example.com', ]); ``` ### PUT / PATCH / DELETE ```php theme={null} // PUTリクエスト $response = Http::put('https://api.example.com/users/1', [ 'name' => '山田 花子', ]); // PATCHリクエスト $response = Http::patch('https://api.example.com/users/1', [ 'email' => 'hanako@example.com', ]); // DELETEリクエスト $response = Http::delete('https://api.example.com/users/1'); ``` ## レスポンスの処理 `Http::get()` などのメソッドは `Illuminate\Http\Client\Response` インスタンスを返します。 このオブジェクトにはレスポンスを検査するための多数のメソッドが用意されています。 ```php theme={null} $response = Http::get('https://api.example.com/users/1'); // レスポンスボディ $response->body(); // 文字列で取得 $response->json(); // 配列に変換 $response->json('name'); // JSONの特定キーを取得 $response->object(); // stdClassオブジェクトとして取得 $response->collect(); // Collectionとして取得 // ステータス $response->status(); // ステータスコード(例: 200) $response->successful(); // 200番台なら true $response->failed(); // 400番台以上なら true $response->clientError(); // 400番台なら true $response->serverError(); // 500番台なら true // よく使うステータスコードの判定 $response->ok(); // 200 $response->created(); // 201 $response->noContent(); // 204 $response->notFound(); // 404 $response->unauthorized(); // 401 $response->forbidden(); // 403 $response->unprocessableEntity(); // 422 $response->tooManyRequests(); // 429 ``` JSONレスポンスは配列アクセスでも取得できます。 ```php theme={null} $name = Http::get('https://api.example.com/users/1')['name']; ``` ### JSONデコードのオプション `json()`の第2引数にJSONデコードフラグを渡せます。無効なJSONを例外として扱う場合は、PHPの`JSON_THROW_ON_ERROR`を指定します。 ```php theme={null} use JsonException; use Illuminate\Support\Facades\Http; try { $data = Http::get('https://api.example.com/users/1') ->json(flags: JSON_THROW_ON_ERROR); } catch (JsonException $e) { report($e); } ``` 第1引数にキーを指定する場合も、第2引数でフラグを渡せます。 ```php theme={null} $name = Http::get('https://api.example.com/users/1') ->json('name', JSON_THROW_ON_ERROR); ``` ## リクエストオプション ### ヘッダーの設定 ```php theme={null} $response = Http::withHeaders([ 'X-Api-Version' => '2', 'Accept-Language' => 'ja', ])->get('https://api.example.com/users'); ``` `application/json` を受け入れることを示す場合は `acceptJson()` が便利です。 ```php theme={null} $response = Http::acceptJson()->get('https://api.example.com/users'); ``` ブラウザーからLaravelへ送るAJAXリクエストのCSRFヘッダー(`X-CSRF-TOKEN` / `X-XSRF-TOKEN`)については [CSRFプロテクション](/jp/csrf) を参照してください。 ### 認証 **Bearer トークン認証**(最も一般的): ```php theme={null} $response = Http::withToken($token)->get('https://api.example.com/me'); ``` **Basic認証**: ```php theme={null} $response = Http::withBasicAuth('user@example.com', 'password') ->get('https://api.example.com/private'); ``` ### ベースURLの設定 同じホストへのリクエストが多い場合、`baseUrl()` でまとめられます。 ```php theme={null} $response = Http::baseUrl('https://api.example.com') ->withToken($token) ->get('/users/1'); ``` ### フォームデータの送信 `application/x-www-form-urlencoded` で送信したい場合は `asForm()` を使います。 ```php theme={null} $response = Http::asForm()->post('https://api.example.com/login', [ 'username' => 'taro', 'password' => 'secret', ]); ``` ### タイムアウト ```php theme={null} // レスポンスタイムアウト(デフォルトは30秒) $response = Http::timeout(10)->get('https://api.example.com/slow-endpoint'); // 接続タイムアウト(デフォルトは10秒) $response = Http::connectTimeout(5)->get('https://api.example.com/endpoint'); ``` タイムアウトを超えた場合、`Illuminate\Http\Client\ConnectionException` がスローされます。 外部APIの呼び出しには必ずタイムアウトを設定することを推奨します。 ### リトライ 一時的なネットワーク障害やサーバーエラーに対して、自動リトライを設定できます。 ```php theme={null} // 最大3回、100ミリ秒間隔でリトライ $response = Http::retry(3, 100)->post('https://api.example.com/orders', $data); ``` 条件付きリトライ(接続エラーのときだけリトライするなど): ```php theme={null} use Illuminate\Http\Client\PendingRequest; use Throwable; use Illuminate\Http\Client\ConnectionException; $response = Http::retry(3, 100, function (Throwable $exception, PendingRequest $request) { return $exception instanceof ConnectionException; })->post('https://api.example.com/orders', $data); ``` ## エラーハンドリング ### 手動でエラーを確認する LaravelのHTTPクライアントは、デフォルトでは400・500番台のレスポンスに対して例外をスローしません。 `failed()` や `clientError()` などで明示的に確認します。 ```php theme={null} $response = Http::get('https://api.example.com/users/999'); if ($response->notFound()) { // 404 の処理 } if ($response->failed()) { // エラーログを記録するなど logger()->error('API request failed', ['status' => $response->status()]); } ``` ### 例外をスローする `throw()` を使うと、エラー時に `Illuminate\Http\Client\RequestException` をスローします。 ```php theme={null} // エラーレスポンス(4xx/5xx)なら例外をスロー $response = Http::post('https://api.example.com/users', $data)->throw(); // throwIf(): 条件が true のときにスロー(下記は独立した使用例) $response = Http::get('https://api.example.com/users/1'); $response->throwIf($response->status() === 422); // throwUnlessStatus(): 指定ステータスコード以外でスロー(下記も独立した使用例) $response = Http::post('https://api.example.com/orders', $data); $response->throwUnlessStatus(201); ``` `throw()` はレスポンスインスタンスを返すため、メソッドチェーンで書けます。 ```php theme={null} $user = Http::post('https://api.example.com/users', $data) ->throw() ->json(); ``` 例外をキャッチして処理する場合: ```php theme={null} use Illuminate\Http\Client\RequestException; use Illuminate\Http\Client\ConnectionException; try { $response = Http::timeout(5) ->post('https://api.example.com/users', $data) ->throw(); } catch (ConnectionException $e) { // タイムアウトや接続エラー logger()->error('Connection failed: ' . $e->getMessage()); } catch (RequestException $e) { // 4xx / 5xx エラー logger()->error('API error', ['status' => $e->response->status()]); } ``` ## 並行リクエスト 複数のAPIを同時に呼び出したい場合、`pool()` で並行実行できます。 ```php theme={null} use Illuminate\Http\Client\Pool; use Illuminate\Support\Facades\Http; $responses = Http::pool(fn (Pool $pool) => [ $pool->as('users')->get('https://api.example.com/users'), $pool->as('posts')->get('https://api.example.com/posts'), $pool->as('comments')->get('https://api.example.com/comments'), ]); $users = $responses['users']->json(); $posts = $responses['posts']->json(); $comments = $responses['comments']->json(); ``` プール内のリクエストがタイムアウトやDNSエラーなどの接続レベルで失敗した場合、`$responses` の該当する要素は `Response` ではなく `Illuminate\Http\Client\ConnectionException` のインスタンスになります。 ```php theme={null} foreach ($responses as $response) { if ($response instanceof Throwable) { // 接続に失敗した... } elseif ($response->failed()) { // 接続は成功したが、エラーレスポンスを受信した... } } ``` 順番に実行するより大幅に高速化できます。外部APIを複数呼び出すダッシュボードなどに有効です。 ## テスト ### Http::fake() によるモック テストでは `Http::fake()` を使って実際のHTTPリクエストを送らずにレスポンスをシミュレートします。 ```php theme={null} use Illuminate\Support\Facades\Http; Http::fake(); // すべてのリクエストに200を返す $response = Http::get('https://api.example.com/users'); $response->successful(); // true ``` 特定のURLに対してレスポンスを設定する場合: ```php theme={null} Http::fake([ 'api.example.com/users/*' => Http::response(['id' => 1, 'name' => '山田 太郎'], 200), 'api.example.com/posts/*' => Http::response(['error' => 'Not Found'], 404), '*' => Http::response('OK', 200), // それ以外のすべて ]); ``` レスポンスのシーケンス(複数回呼ばれたときに順番にレスポンスを返す): ```php theme={null} Http::fake([ // 1回目: 200、2回目: 200、3回目: 429 を返す // シーケンスが尽きると以降のリクエストで例外がスローされる 'api.example.com/*' => Http::sequence() ->push(['id' => 1], 200) ->push(['id' => 2], 200) ->pushStatus(429), ]); ``` ### リクエストの検証 `Http::assertSent()` でリクエストの内容を検証できます。 ```php theme={null} use Illuminate\Http\Client\Request; use Illuminate\Support\Facades\Http; Http::fake(); Http::withToken('my-token')->post('https://api.example.com/users', [ 'name' => '山田 太郎', ]); Http::assertSent(function (Request $request) { return $request->url() === 'https://api.example.com/users' && $request->hasHeader('Authorization', 'Bearer my-token') && $request['name'] === '山田 太郎'; }); // 送信されていないことを確認 Http::assertNotSent(function (Request $request) { return $request->url() === 'https://api.example.com/admin'; }); // リクエストが1件だけ送られたことを確認 Http::assertSentCount(1); ``` テストでは必ず `Http::fake()` を先頭で呼び出しましょう。 呼び出し忘れると実際の外部APIにリクエストが飛んでしまいます。 `Http::preventStrayRequests()` を使うと、フェイクしていないURLへのリクエストで例外をスローさせることができます。 ### Strayリクエストの防止 ```php theme={null} Http::preventStrayRequests(); Http::fake([ 'api.example.com/*' => Http::response(['ok' => true]), ]); // フェイクされていないURLへのリクエストは例外になる Http::get('https://other.example.com/endpoint'); // 例外スロー ``` ## 実践例: 外部APIを呼び出すサービスクラス 実際のプロジェクトでは、HTTPクライアントのロジックをサービスクラスにまとめるのがベストプラクティスです。 ```php theme={null} baseUrl) ->withToken($this->token) ->acceptJson() ->timeout(10) ->get("/users/{$username}") ->throw() ->json(); } /** * リポジトリの一覧を取得する * * @throws ConnectionException * @throws RequestException */ public function getRepositories(string $username, int $page = 1): array { return Http::baseUrl($this->baseUrl) ->withToken($this->token) ->acceptJson() ->timeout(10) ->retry(2, 500) ->get("/users/{$username}/repos", [ 'page' => $page, 'per_page' => 30, 'sort' => 'updated', ]) ->throw() ->json(); } } ``` ```php theme={null} // app/Providers/AppServiceProvider.php use App\Services\GitHubService; public function register(): void { $this->app->singleton(GitHubService::class, function () { return new GitHubService( token: config('services.github.token'), ); }); } ``` ```php theme={null} // config/services.php 'github' => [ 'token' => env('GITHUB_TOKEN'), ], ``` ```php theme={null} github->getUser($username); return response()->json($user); } catch (ConnectionException) { return response()->json(['error' => 'GitHub APIに接続できませんでした。'], 503); } catch (RequestException $e) { $status = $e->response->status(); if ($status === 404) { return response()->json(['error' => 'ユーザーが見つかりません。'], 404); } return response()->json(['error' => 'GitHub APIでエラーが発生しました。'], 502); } } } ``` ```php theme={null} Http::response([ 'login' => 'octocat', 'name' => 'The Octocat', 'public_repos' => 8, ], 200), 'api.github.com/users/notfound' => Http::response( ['message' => 'Not Found'], 404 ), ]); $this->service = new GitHubService(token: 'test-token'); } public function test_ユーザー情報を取得できる(): void { $user = $this->service->getUser('octocat'); $this->assertEquals('octocat', $user['login']); $this->assertEquals('The Octocat', $user['name']); Http::assertSent(function ($request) { return $request->url() === 'https://api.github.com/users/octocat' && $request->hasHeader('Authorization', 'Bearer test-token'); }); } public function test_存在しないユーザーは例外がスローされる(): void { $this->expectException(RequestException::class); $this->service->getUser('notfound'); } } ``` ## まとめ | メソッド | 用途 | | - | - | | `Http::get($url, $query)` | GETリクエスト | | `Http::post($url, $data)` | POSTリクエスト(JSON) | | `Http::put($url, $data)` | PUTリクエスト | | `Http::patch($url, $data)` | PATCHリクエスト | | `Http::delete($url)` | DELETEリクエスト | | `->withToken($token)` | Bearer認証 | | `->withHeaders($headers)` | カスタムヘッダー | | `->timeout($seconds)` | タイムアウト設定 | | `->retry($times, $sleep)` | 自動リトライ | | `->throw()` | エラー時に例外をスロー | | `Http::fake()` | テスト用モック | | `Http::pool($callback)` | 並行リクエスト | | メソッド | 説明 | | - | - | | `successful()` | 200番台 | | `failed()` | 400番台以上 | | `clientError()` | 400番台 | | `serverError()` | 500番台 | | `ok()` | 200 | | `created()` | 201 | | `notFound()` | 404 | | `unauthorized()` | 401 | | `forbidden()` | 403 | | `unprocessableEntity()` | 422 | | `tooManyRequests()` | 429 | * HTTPクライアントのロジックはサービスクラスにまとめる * 必ずタイムアウトを設定する(`timeout()` と `connectTimeout()`) * 一時的な障害に対してリトライを設定する(`retry()`) * テストでは `Http::fake()` を必ず使用し、外部APIを実際に叩かない * `Http::preventStrayRequests()` をテストのセットアップに加えると安心 * APIのトークンや認証情報は環境変数と `config/services.php` で管理する # HTTP テスト Source: https://kawax.biz/jp/http-tests LaravelのHTTPテスト機能を使ったリクエストのシミュレーション、アサーション、認証テスト、ファイルアップロードテストの解説 ## はじめに Laravelは、HTTPリクエストをシミュレートしてレスポンスを検証するための、豊富なAPIを提供しています。 実際のHTTPサーバーを立てることなく、アプリケーションへのリクエストをテスト内で再現できます。 ```php Pest theme={null} get('/'); $response->assertStatus(200); }); ``` ```php PHPUnit theme={null} get('/'); $response->assertStatus(200); } } ``` `get()` メソッドはアプリケーションに `GET` リクエストを送り、`assertStatus()` は返ってきたレスポンスのHTTPステータスコードを検証します。 テスト実行中はCSRFミドルウェアが自動的に無効になります。テストで明示的に無効化する必要はありません。 ## リクエストの作成 テスト内では `get`、`post`、`put`、`patch`、`delete` メソッドを使ってリクエストを送れます。 これらのメソッドはネットワークリクエストを実際には発行せず、アプリケーション内部でシミュレートします。 返り値は `Illuminate\Testing\TestResponse` のインスタンスで、様々なアサーションメソッドを提供します。 ```php Pest theme={null} get('/'); $response->assertStatus(200); }); ``` ```php PHPUnit theme={null} get('/'); $response->assertStatus(200); } } ``` 1つのテストメソッド内では、基本的に1つのリクエストのみ送ることを推奨します。複数のリクエストを同一テスト内で実行すると予期しない動作が発生することがあります。 ### リクエストヘッダーのカスタマイズ `withHeaders()` メソッドを使ってリクエストのヘッダーをカスタマイズできます。 ```php Pest theme={null} withHeaders([ 'X-Header' => 'Value', ])->post('/user', ['name' => 'Sally']); $response->assertStatus(201); }); ``` ```php PHPUnit theme={null} withHeaders([ 'X-Header' => 'Value', ])->post('/user', ['name' => 'Sally']); $response->assertStatus(201); } } ``` ### クッキー `withCookie()` または `withCookies()` メソッドでリクエスト前にクッキー値を設定できます。 ```php Pest theme={null} withCookie('color', 'blue')->get('/'); $response = $this->withCookies([ 'color' => 'blue', 'name' => 'Taylor', ])->get('/'); }); ``` ```php PHPUnit theme={null} withCookie('color', 'blue')->get('/'); $response = $this->withCookies([ 'color' => 'blue', 'name' => 'Taylor', ])->get('/'); } } ``` ### セッションと認証 `withSession()` メソッドを使ってリクエスト前にセッションデータを設定できます。 ```php Pest theme={null} withSession(['banned' => false])->get('/'); }); ``` ```php PHPUnit theme={null} withSession(['banned' => false])->get('/'); } } ``` `actingAs()` メソッドで認証ユーザーとしてリクエストを送れます。モデルファクトリーと組み合わせて使います。 ```php Pest theme={null} create(); $response = $this->actingAs($user) ->withSession(['banned' => false]) ->get('/'); }); ``` ```php PHPUnit theme={null} create(); $response = $this->actingAs($user) ->withSession(['banned' => false]) ->get('/'); } } ``` `actingAs()` の第2引数にガード名を渡すと、そのガードで認証されます。テストの間はそのガードがデフォルトになります。 ```php theme={null} $this->actingAs($user, 'web'); ``` 未認証状態でリクエストしたい場合は `actingAsGuest()` を使います。 ```php theme={null} $this->actingAsGuest(); ``` ### レスポンスのデバッグ テスト中にレスポンスの内容を確認したい場合は `dump`、`dumpHeaders`、`dumpSession` メソッドを使います。 ```php Pest theme={null} get('/'); $response->dump(); $response->dumpHeaders(); $response->dumpSession(); }); ``` ```php PHPUnit theme={null} get('/'); $response->dump(); $response->dumpHeaders(); $response->dumpSession(); } } ``` 実行を停止したい場合は `dd`、`ddHeaders`、`ddBody`、`ddJson`、`ddSession` メソッドを使います。 ```php theme={null} $response->dd(); $response->ddHeaders(); $response->ddBody(); $response->ddJson(); $response->ddSession(); ``` ### 例外のテスト 特定の例外がスローされることをテストするには、`Exceptions` ファサードを使います。 ```php Pest theme={null} get('/order/1'); // 例外がスローされたことを確認 Exceptions::assertReported(InvalidOrderException::class); // 例外の内容を確認 Exceptions::assertReported(function (InvalidOrderException $e) { return $e->getMessage() === 'The order was invalid.'; }); }); ``` ```php PHPUnit theme={null} get('/'); Exceptions::assertReported(InvalidOrderException::class); Exceptions::assertReported(function (InvalidOrderException $e) { return $e->getMessage() === 'The order was invalid.'; }); } } ``` 例外がスローされなかったことを確認するには `assertNotReported` や `assertNothingReported` を使います。 ```php theme={null} Exceptions::assertNotReported(InvalidOrderException::class); Exceptions::assertNothingReported(); ``` 例外ハンドリングを無効にしてリクエストを送るには `withoutExceptionHandling()` を使います。 ```php theme={null} $response = $this->withoutExceptionHandling()->get('/'); ``` クロージャ内のコードが例外をスローするかテストするには `assertThrows()` を使います。 ```php theme={null} $this->assertThrows( fn () => (new ProcessOrder)->execute(), OrderInvalid::class ); // 例外の内容を確認する場合 $this->assertThrows( fn () => (new ProcessOrder)->execute(), fn (OrderInvalid $e) => $e->orderId() === 123 ); ``` 例外がスローされないことを確認するには `assertDoesntThrow()` を使います。 ```php theme={null} $this->assertDoesntThrow(fn () => (new ProcessOrder)->execute()); ``` ## JSON APIのテスト LaravelはJSON APIのテストのためのヘルパーを多数提供しています。 `json`、`getJson`、`postJson`、`putJson`、`patchJson`、`deleteJson`、`optionsJson` メソッドを使ってJSONリクエストを送れます。 ```php Pest theme={null} postJson('/api/user', ['name' => 'Sally']); $response ->assertStatus(201) ->assertJson([ 'created' => true, ]); }); ``` ```php PHPUnit theme={null} postJson('/api/user', ['name' => 'Sally']); $response ->assertStatus(201) ->assertJson([ 'created' => true, ]); } } ``` JSONレスポンスのデータには配列変数としてアクセスできます。 ```php Pest theme={null} expect($response['created'])->toBeTrue(); ``` ```php PHPUnit theme={null} $this->assertTrue($response['created']); ``` `assertJson()` はレスポンスを配列に変換し、指定した配列がJSONレスポンスの中に含まれているかを検証します。JSONに他のプロパティが存在していても、指定したフラグメントが含まれていればテストは通過します。 ### 完全一致のアサーション `assertExactJson()` を使うと、返却されたJSONと指定した配列が**完全に一致する**ことを検証できます。 ```php Pest theme={null} postJson('/user', ['name' => 'Sally']); $response ->assertStatus(201) ->assertExactJson([ 'created' => true, ]); }); ``` ```php PHPUnit theme={null} postJson('/user', ['name' => 'Sally']); $response ->assertStatus(201) ->assertExactJson([ 'created' => true, ]); } } ``` ### JSONパスのアサーション `assertJsonPath()` を使って、指定したパスにあるデータを検証できます。 ```php Pest theme={null} postJson('/user', ['name' => 'Sally']); $response ->assertStatus(201) ->assertJsonPath('team.owner.name', 'Darian'); }); ``` ```php PHPUnit theme={null} postJson('/user', ['name' => 'Sally']); $response ->assertStatus(201) ->assertJsonPath('team.owner.name', 'Darian'); } } ``` クロージャを渡してより柔軟に検証することもできます。 ```php theme={null} $response->assertJsonPath('team.owner.name', fn (string $name) => strlen($name) >= 3); ``` ### フルーエントなJSONテスト `assertJson()` にクロージャを渡すと、`AssertableJson` インスタンスを使ってフルーエントにアサーションを記述できます。 ```php Pest theme={null} use Illuminate\Testing\Fluent\AssertableJson; test('フルーエントなJSONテスト', function () { $response = $this->getJson('/users/1'); $response ->assertJson(fn (AssertableJson $json) => $json->where('id', 1) ->where('name', 'Victoria Faith') ->where('email', fn (string $email) => str($email)->is('victoria@gmail.com')) ->whereNot('status', 'pending') ->missing('password') ->etc() ); }); ``` ```php PHPUnit theme={null} use Illuminate\Testing\Fluent\AssertableJson; public function test_fluent_json(): void { $response = $this->getJson('/users/1'); $response ->assertJson(fn (AssertableJson $json) => $json->where('id', 1) ->where('name', 'Victoria Faith') ->where('email', fn (string $email) => str($email)->is('victoria@gmail.com')) ->whereNot('status', 'pending') ->missing('password') ->etc() ); } ``` `etc()` メソッドはアサーション対象以外のプロパティが存在することを許可します。`etc()` を使わない場合、アサーションしていないプロパティが存在するとテストが失敗します。これにより意図せず機密情報をレスポンスに含めてしまうことを防ぎます。 属性の存在・不在を確認するには `has()` と `missing()` を使います。 ```php theme={null} $response->assertJson(fn (AssertableJson $json) => $json->has('data') ->missing('message') ); ``` 複数の属性をまとめて確認するには `hasAll()` や `missingAll()` を使います。 ```php theme={null} $response->assertJson(fn (AssertableJson $json) => $json->hasAll(['status', 'data']) ->missingAll(['message', 'code']) ); ``` #### JSONコレクションのアサーション ルートが複数のアイテムを含むJSONレスポンスを返す場合、`has()` メソッドでアイテム数やコレクションの中身を検証できます。 ```php theme={null} $response ->assertJson(fn (AssertableJson $json) => $json->has(3) ->first(fn (AssertableJson $json) => $json->where('id', 1) ->where('name', 'Victoria Faith') ->missing('password') ->etc() ) ); ``` すべてのアイテムに同じアサーションを適用するには `each()` を使います。 ```php theme={null} $response ->assertJson(fn (AssertableJson $json) => $json->has(3) ->each(fn (AssertableJson $json) => $json->whereType('id', 'integer') ->whereType('name', 'string') ->whereType('email', 'string') ->missing('password') ->etc() ) ); ``` #### JSONの型アサーション `whereType()` や `whereAllType()` を使ってプロパティの型を検証できます。 ```php theme={null} $response->assertJson(fn (AssertableJson $json) => $json->whereType('id', 'integer') ->whereAllType([ 'users.0.name' => 'string', 'meta' => 'array' ]) ); ``` `|` 文字で複数の型を指定することもできます。いずれかの型に一致すればアサーションは通過します。 ```php theme={null} $response->assertJson(fn (AssertableJson $json) => $json->whereType('name', 'string|null') ->whereType('id', ['string', 'integer']) ); ``` 利用可能な型は `string`、`integer`、`double`、`boolean`、`array`、`null` です。 ## 認証テスト `actingAs()` を使って認証されたユーザーとしてリクエストを送れます。 ```php Pest theme={null} create(); $response = $this->actingAs($user)->get('/dashboard'); $response->assertStatus(200); }); test('未認証ユーザーはリダイレクトされる', function () { $response = $this->get('/dashboard'); $response->assertRedirect('/login'); }); ``` ```php PHPUnit theme={null} create(); $response = $this->actingAs($user)->get('/dashboard'); $response->assertStatus(200); } public function test_unauthenticated_users_are_redirected(): void { $response = $this->get('/dashboard'); $response->assertRedirect('/login'); } } ``` 特定のガードで認証するには第2引数にガード名を指定します。 ```php theme={null} $this->actingAs($user, 'api'); ``` ### ユーザー登録フローのテスト例 実際のユーザー登録エンドポイントをテストする例です。 ```php Pest theme={null} post('/register', [ 'name' => 'Test User', 'email' => 'test@example.com', 'password' => 'password', 'password_confirmation' => 'password', ]); $response->assertRedirect('/dashboard'); $this->assertDatabaseHas('users', ['email' => 'test@example.com']); }); test('重複したメールアドレスでは登録できない', function () { User::factory()->create(['email' => 'test@example.com']); $response = $this->post('/register', [ 'name' => 'Test User', 'email' => 'test@example.com', 'password' => 'password', 'password_confirmation' => 'password', ]); $response->assertSessionHasErrors('email'); }); ``` ```php PHPUnit theme={null} post('/register', [ 'name' => 'Test User', 'email' => 'test@example.com', 'password' => 'password', 'password_confirmation' => 'password', ]); $response->assertRedirect('/dashboard'); $this->assertDatabaseHas('users', ['email' => 'test@example.com']); } public function test_duplicate_email_cannot_register(): void { User::factory()->create(['email' => 'test@example.com']); $response = $this->post('/register', [ 'name' => 'Test User', 'email' => 'test@example.com', 'password' => 'password', 'password_confirmation' => 'password', ]); $response->assertSessionHasErrors('email'); } } ``` ## セッションのテスト `withSession()` でセッションデータを事前にセットしてリクエストを送れます。 `assertSessionHas()` でセッションに値が存在するかを検証できます。 ```php Pest theme={null} withSession(['locale' => 'ja'])->get('/'); $response->assertSessionHas('locale', 'ja'); }); ``` ```php PHPUnit theme={null} withSession(['locale' => 'ja'])->get('/'); $response->assertSessionHas('locale', 'ja'); } } ``` ### セッションアサーションの一覧 | メソッド | 説明 | | - | - | | `assertSessionHas($key, $value)` | セッションに指定したキーと値が存在する | | `assertSessionHasAll([...])` | セッションに複数のキーと値が存在する | | `assertSessionHasErrors($keys)` | セッションにバリデーションエラーが存在する | | `assertSessionHasErrorsIn($bag, $keys)` | 指定したエラーバッグにエラーが存在する | | `assertSessionHasNoErrors()` | セッションにバリデーションエラーが存在しない | | `assertSessionDoesntHaveErrors()` | セッションにバリデーションエラーが存在しない | | `assertSessionMissing($key)` | セッションに指定したキーが存在しない | ## ファイルアップロードのテスト `Illuminate\Http\UploadedFile` クラスの `fake()` メソッドを使って、ダミーのファイルや画像を生成できます。 `Storage` ファサードの `fake()` メソッドと組み合わせることで、ファイルアップロードを簡単にテストできます。 ```php Pest theme={null} image('avatar.jpg'); $response = $this->post('/avatar', [ 'avatar' => $file, ]); Storage::disk('avatars')->assertExists($file->hashName()); }); ``` ```php PHPUnit theme={null} image('avatar.jpg'); $response = $this->post('/avatar', [ 'avatar' => $file, ]); Storage::disk('avatars')->assertExists($file->hashName()); } } ``` ファイルが存在しないことを確認するには `assertMissing()` を使います。 ```php theme={null} Storage::disk('avatars')->assertMissing('missing.jpg'); ``` ### ダミーファイルのカスタマイズ 画像のサイズやファイルサイズを指定できます。バリデーションルールのテストに便利です。 ```php theme={null} // 幅・高さ・ファイルサイズを指定する(サイズはKB単位) UploadedFile::fake()->image('avatar.jpg', $width, $height)->size(100); // 任意のファイルタイプを作成する UploadedFile::fake()->create('document.pdf', $sizeInKilobytes); // MIMEタイプを明示的に指定する UploadedFile::fake()->create( 'document.pdf', $sizeInKilobytes, 'application/pdf' ); ``` ## ビューのテスト HTTPリクエストをシミュレートせずにビューを直接レンダリングしてテストできます。 `view()` メソッドはビュー名とオプションでデータの配列を受け取り、`Illuminate\Testing\TestView` のインスタンスを返します。 ```php Pest theme={null} view('welcome', ['name' => 'Taylor']); $view->assertSee('Taylor'); }); ``` ```php PHPUnit theme={null} view('welcome', ['name' => 'Taylor']); $view->assertSee('Taylor'); } } ``` `TestView` クラスでは以下のアサーションメソッドが使えます。 | メソッド | 説明 | | - | - | | `assertSee($value)` | ビューに指定した文字列が含まれる | | `assertSeeInOrder([...])` | ビューに指定した文字列が順番に含まれる | | `assertSeeText($value)` | ビューのテキストに指定した文字列が含まれる | | `assertSeeTextInOrder([...])` | ビューのテキストに指定した文字列が順番に含まれる | | `assertDontSee($value)` | ビューに指定した文字列が含まれない | | `assertDontSeeText($value)` | ビューのテキストに指定した文字列が含まれない | レンダリングされたビューの内容を文字列として取得するには、`TestView` インスタンスを文字列にキャストします。 ```php theme={null} $contents = (string) $this->view('welcome'); ``` バリデーションエラーをビューに渡すには `withViewErrors()` を使います。 ```php theme={null} $view = $this->withViewErrors([ 'name' => ['有効な名前を入力してください。'] ])->view('form'); $view->assertSee('有効な名前を入力してください。'); ``` ### コンポーネントのテスト `blade()` メソッドを使って生のBladeテンプレート文字列をレンダリングできます。 ```php theme={null} $view = $this->blade( '', ['name' => 'Taylor'] ); $view->assertSee('Taylor'); ``` `component()` メソッドを使ってBladeコンポーネントをレンダリングできます。`Illuminate\Testing\TestComponent` のインスタンスを返します。 ```php theme={null} $view = $this->component(Profile::class, ['name' => 'Taylor']); $view->assertSee('Taylor'); ``` ## レスポンスアサーション一覧 `Illuminate\Testing\TestResponse` クラスが提供する主要なアサーションメソッドです。 ### HTTPステータス | メソッド | 説明 | | - | - | | `assertOk()` | 200 OK | | `assertCreated()` | 201 Created | | `assertAccepted()` | 202 Accepted | | `assertNoContent($status = 204)` | 204 No Content | | `assertBadRequest()` | 400 Bad Request | | `assertUnauthorized()` | 401 Unauthorized | | `assertForbidden()` | 403 Forbidden | | `assertNotFound()` | 404 Not Found | | `assertUnprocessable()` | 422 Unprocessable Entity | | `assertTooManyRequests()` | 429 Too Many Requests | | `assertInternalServerError()` | 500 Internal Server Error | | `assertStatus($code)` | 指定したステータスコード | | `assertSuccessful()` | 2xx 系のステータスコード | | `assertClientError()` | 4xx 系のステータスコード | | `assertServerError()` | 5xx 系のステータスコード | ### リダイレクト | メソッド | 説明 | | - | - | | `assertRedirect($uri)` | 指定したURIへリダイレクト | | `assertRedirectContains($string)` | リダイレクトURIに指定した文字列が含まれる | | `assertRedirectToRoute($name, $params)` | 指定したルートへリダイレクト | | `assertRedirectToSignedRoute($name, $params)` | 指定した署名付きルートへリダイレクト | | `assertRedirectBack()` | 直前のURLへリダイレクト | ### コンテンツ | メソッド | 説明 | | - | - | | `assertSee($value, $escaped = true)` | 指定した値がレスポンスに含まれる | | `assertSeeInOrder(array $values)` | 指定した値が順番に含まれる | | `assertSeeText($value)` | 指定したテキストが含まれる | | `assertDontSee($value)` | 指定した値が含まれない | | `assertDontSeeText($value)` | 指定したテキストが含まれない | | `assertContent($value)` | レスポンスボディが指定した値に一致する | | `assertDownload($filename)` | ダウンロードレスポンスである | ### JSON | メソッド | 説明 | | - | - | | `assertJson(array $data)` | JSONレスポンスが指定したデータを含む | | `assertExactJson(array $data)` | JSONレスポンスが指定したデータに完全一致する | | `assertJsonPath($path, $value)` | 指定したパスの値が一致する | | `assertJsonMissingPath($path)` | 指定したパスが存在しない | | `assertJsonStructure(array $structure)` | JSONが指定した構造を持つ | | `assertJsonCount($count, $key)` | 指定したキーのJSONが指定数の要素を持つ | | `assertJsonFragment(array $data)` | JSONに指定したフラグメントが含まれる | | `assertJsonMissing(array $data)` | JSONに指定したデータが含まれない | | `assertJsonIsArray()` | JSONがアレイである | | `assertJsonIsObject()` | JSONがオブジェクトである | | `assertJsonValidationErrors($keys)` | 指定したキーのバリデーションエラーが含まれる | | `assertJsonMissingValidationErrors($keys)` | 指定したキーのバリデーションエラーが含まれない | ### ヘッダーとクッキー | メソッド | 説明 | | - | - | | `assertHeader($headerName, $value)` | 指定したヘッダーが存在する | | `assertHeaderMissing($headerName)` | 指定したヘッダーが存在しない | | `assertCookie($name, $value)` | 指定したクッキーが存在する | | `assertCookieExpired($name)` | 指定したクッキーが期限切れである | | `assertCookieMissing($name)` | 指定したクッキーが存在しない | | `assertPlainCookie($name, $value)` | 指定した暗号化されていないクッキーが存在する | ### ビュー | メソッド | 説明 | | - | - | | `assertViewIs($value)` | 指定したビューが返された | | `assertViewHas($key, $value)` | ビューに指定したデータが存在する | | `assertViewHasAll(array $data)` | ビューに複数のデータが存在する | | `assertViewMissing($key)` | ビューに指定したデータが存在しない | ### バリデーション | メソッド | 説明 | | - | - | | `assertValid($keys)` | 指定したキーにバリデーションエラーがない | | `assertInvalid($keys)` | 指定したキーにバリデーションエラーがある | ## TDDを実践するためのポイント HTTPテストはTDD(テスト駆動開発)と相性が抜群です。以下のポイントを意識するとより効果的です。 HTTPテストは `tests/Feature/` ディレクトリに作成します。アプリケーションの外から見た動作(リクエスト→レスポンス)を最初に定義することで、実装すべき機能が明確になります。 データベースを使うテストでは `RefreshDatabase` トレイトを使いましょう。各テスト後にデータベースがリセットされ、テスト間でのデータ干渉を防げます。テストの独立性を保つことで、実行順序に依存しない安定したテストスイートが構築できます。 `User::factory()->create()` のようなモデルファクトリーを使うと、テストデータの作成が簡単になります。特定の状態を持つモデルを作成するためにファクトリーの状態(state)を定義しておくと、テストの可読性が向上します。 1つのテストメソッドではできるだけ1つのことを検証しましょう。テストが失敗したときに原因が特定しやすくなります。「Arrange(準備)→ Act(実行)→ Assert(検証)」のAAAパターンを意識するとテストが読みやすくなります。 認証が必要なルートには必ず「認証済みの場合」と「未認証の場合」の両方のテストを書きましょう。セキュリティ上の問題を早期に発見できます。 ## 関連ページ Laravelでのテストの基本的な書き方と `php artisan test` の使い方を確認します。 # インストール Source: https://kawax.biz/jp/installation LaravelのインストールとPHP環境のセットアップ方法を解説します。 ## 必要な環境 Laravelを始める前に、以下の環境が必要です。 * **PHP 8.3以上** * **Composer**(PHPのパッケージマネージャー) * **Laravelインストーラー**(任意だが推奨) Node.js/npmまたはBunも、フロントエンドアセットをコンパイルするために必要です。 ## PHPとLaravelインストーラーのインストール PHPとComposerとLaravelインストーラーをまとめてインストールする最も簡単な方法は、OSに合わせた次のコマンドを実行することです。 ```shell macOS theme={null} /bin/bash -c "$(curl -fsSL https://php.new/install/mac/8.4)" ``` ```powershell Windows (管理者として実行) theme={null} Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://php.new/install/windows/8.4')) ``` ```shell Linux theme={null} /bin/bash -c "$(curl -fsSL https://php.new/install/linux/8.4)" ``` コマンドを実行後、ターミナルセッションを再起動してください。 PHPやComposerをすでにインストール済みの場合は、Composerを使ってLaravelインストーラーだけをインストールできます。 ```shell theme={null} composer global require laravel/installer ``` グラフィカルなPHP管理ツールを求めるなら、[Laravel Herd](#laravel-herd-を使ったインストール)も確認してください。 ## 新しいLaravelアプリケーションの作成 PHPとComposerとLaravelインストーラーの準備ができたら、新しいLaravelアプリケーションを作成します。 インストーラーがテストフレームワーク・データベース・スターターキットの選択を促します。 ```shell theme={null} laravel new example-app ``` アプリケーションが作成されたら、`dev` Composerスクリプトで開発サーバーを起動します。 ```shell theme={null} cd example-app npm install && npm run build composer run dev ``` 開発サーバーが起動したら、ブラウザで [http://localhost:8000](http://localhost:8000) にアクセスします。 スターターキットを使うと、認証機能を含むベースの足場が自動生成されます。Laravel 13では、React・Vue・Svelte・Livewireの4種類から選択できます。 ### スターターキットの選択肢 `laravel new` コマンドを実行すると、以下のスターターキットからいずれかを選択できます。 [Inertia](https://inertiajs.com) を使ったReactフロントエンドのスターターキットです。React 19・TypeScript・Tailwind・[shadcn/ui](https://ui.shadcn.com) を採用しており、SPAとサーバーサイドルーティングを組み合わせたモダンな構成を提供します。 [Inertia](https://inertiajs.com) を使ったVueフロントエンドのスターターキットです。Vue Composition API・TypeScript・Tailwind・[shadcn-vue](https://www.shadcn-vue.com/) を採用しています。 [Inertia](https://inertiajs.com) を使ったSvelteフロントエンドのスターターキットです。Svelte 5・TypeScript・Tailwind・[shadcn-svelte](https://www.shadcn-svelte.com/) を採用しています。 [Laravel Livewire](https://livewire.laravel.com) を使ったスターターキットです。PHPだけで動的なUIを構築でき、Bladeテンプレートを主に使うチームに最適です。Tailwind・[Flux UI](https://fluxui.dev) を採用しています。 どの構成を選ぶべきか迷ったら、[フロントエンド](/jp/frontend) で Blade・Livewire・Inertia・SPA の違いを先に整理しておくと判断しやすくなります。 ## Laravel Herd を使ったインストール [Laravel Herd](https://herd.laravel.com) は、macOSとWindows向けのネイティブなLaravel/PHP開発環境です。 PHP・Nginx・Laravel CLIをまとめてインストールできます。 ### macOS 1. [Herdのウェブサイト](https://herd.laravel.com)からインストーラーをダウンロードします。 2. インストーラーを実行するとPHPが自動的に設定されます。 3. `~/Herd` ディレクトリがパークディレクトリとして設定されます。 ```shell theme={null} cd ~/Herd laravel new my-app cd my-app herd open ``` ### Windows 1. [Herdのウェブサイト](https://herd.laravel.com/windows)からWindowsインストーラーをダウンロードします。 2. インストール後、Herdを起動してオンボーディングを完了させます。 3. `%USERPROFILE%\Herd` がパークディレクトリになります。 ```shell theme={null} cd ~\Herd laravel new my-app cd my-app herd open ``` ## 初期設定 ### 環境設定 Laravelの設定値の多くは、環境によって異なる場合があります。 そのため、アプリケーションのルートにある `.env` ファイルで重要な設定値を管理します。 `.env` ファイルはソースコントロールにコミットしないでください。 機密情報が漏れるリスクがあります。 ### データベースの設定 デフォルトでは、LaravelはSQLiteを使用するように設定されています。 MySQLやPostgreSQLを使う場合は、`.env` ファイルを更新してください。 ```ini theme={null} DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=laravel DB_USERNAME=root DB_PASSWORD= ``` SQLite以外のデータベースを使う場合は、マイグレーションを実行してテーブルを作成します。 ```shell theme={null} php artisan migrate ``` ## IDEサポート 公式の[Laravel VS Code Extension](https://marketplace.visualstudio.com/items?itemName=laravel.vscode-laravel)をインストールすると、シンタックスハイライト・スニペット・Artisanコマンド統合・EloquentモデルのスマートオートコンプリートなどのLaravelサポートが利用できます。 [PhpStorm](https://www.jetbrains.com/phpstorm/laravel/)はLaravelフレームワークの組み込みサポートを持ち、Bladeテンプレート・Eloquentモデル・ルート・ビュー・翻訳のスマートオートコンプリートを提供します。 ## 次のステップ Laravelプロジェクトの主要なディレクトリとファイルを理解します。 # Laravelとは Source: https://kawax.biz/jp/introduction Laravelの概要と特徴、なぜLaravelを選ぶべきかを解説します。 ## Laravelとは Laravelは、表現力豊かでエレガントな構文を持つPHP製のWebアプリケーションフレームワークです。 フレームワークはアプリケーションを作るための構造と出発点を提供し、あなたは細部の実装に悩まずに素晴らしいものを作ることに集中できます。 Laravelは強力な機能を提供しながら、優れた開発体験を実現することを目指しています。 依存性注入、表現力豊かなデータベース抽象化レイヤー、キューとスケジュールジョブ、ユニットテストと統合テストなど、多くの機能が含まれています。 PHPのWebフレームワークが初めてでも、長年の経験があっても、Laravelはあなたとともに成長するフレームワークです。 ## なぜLaravelを使うのか ### プログレッシブなフレームワーク Laravelは「プログレッシブ」なフレームワークです。 Web開発の第一歩を踏み出すときは、豊富なドキュメント・ガイド・[動画チュートリアル](https://laracasts.com)があなたを圧倒されることなく学習をサポートします。 シニア開発者には、[依存性注入](/jp/service-container)・[ユニットテスト](/jp/testing)・[キュー](/jp/queues)・[リアルタイムイベント](/jp/broadcasting)などの堅牢なツールを提供します。 ### スケーラブルなフレームワーク Laravelは非常にスケーラブルです。 PHPのスケーリング特性と、Redisのような高速な分散キャッシュシステムのサポートにより、Laravelでの水平スケールは簡単です。 実際にLaravelアプリケーションは月間数億リクエストを処理するまで簡単にスケールされています。 ### AIと相性のよいフレームワーク Laravelの規約とよく定義された構造は、CursorやClaude Codeなどのツールを使ったAI支援開発に最適です。 コントローラーやマイグレーションを追加するよう指示すると、AIエージェントはどこに配置すべきか正確に把握しています。 ### コミュニティによるフレームワーク Laravelは世界中の何千人もの才能ある開発者が[フレームワークに貢献](https://github.com/laravel/framework)しています。 PHPエコシステムの優れたパッケージを組み合わせ、最も堅牢で開発者に優しいフレームワークを提供しています。 ## Laravelの主な特徴 データベース操作を直感的なオブジェクト指向インターフェースで行えます。 テーブルごとにモデルクラスを定義し、そのモデルを使ってデータを操作します。 シンプルで強力なテンプレートエンジンです。 PHPコードを制限なく使いながら、便利なディレクティブで生産性を高めます。 `php artisan` コマンドで、コントローラーやモデルの生成・マイグレーション実行・キャッシュのクリアなど、多くの操作を自動化できます。 ユーザー認証の仕組みがフレームワークに組み込まれており、スターターキットを使えば数分でログイン機能を構築できます。 [Pest](https://pestphp.com)または[PHPUnit](https://phpunit.de/)を使ったユニットテストと機能テストが標準でサポートされています。 ## 次のステップ PHPとLaravelをインストールして最初のプロジェクトを作成します。 # リクエストライフサイクル Source: https://kawax.biz/jp/lifecycle LaravelアプリケーションへのHTTPリクエストとConsoleコマンドがどのように処理されるかを解説します。 ## はじめに ツールを「現実の世界」で使う場合、そのツールがどのように動作するかを理解していると、より自信を持って使えます。アプリケーション開発でも同じです。開発ツールがどのように機能するかを理解すると、より快適に、自信を持ってアプリケーションを構築できます。 このページでは、Laravelフレームワークがどのように動作するかについて、高レベルな概要を説明します。フレームワーク全体をよく理解することで、すべてが「魔法」のように感じられなくなり、アプリケーション構築への自信が高まります。 ## HTTPリクエストのライフサイクル ### 全体の流れ ```mermaid theme={null} flowchart TD A[ブラウザ/HTTPリクエスト] --> B[public/index.php] B --> C[Composerオートローダー読み込み] C --> D[bootstrap/app.php] D --> E[アプリケーションインスタンス生成
    サービスコンテナ] E --> F[HTTPカーネル] F --> G[ブートストラッパー実行
    エラーハンドリング/ロギング/環境検出] G --> H[サービスプロバイダー登録・起動
    register → boot] H --> I[ミドルウェアスタック通過] I --> J[ルーティング] J --> K[コントローラー/クロージャ実行] K --> L[レスポンス生成] L --> I I --> M[ブラウザにレスポンス送信] ``` ### 最初のステップ Laravelアプリケーションへのすべてのリクエストのエントリーポイントは `public/index.php` ファイルです。すべてのリクエストは、Webサーバー(Apache / Nginx)の設定によってこのファイルに転送されます。`index.php` ファイル自体はほとんどコードを含んでいません。フレームワークの残りの部分をロードするための出発点です。 `index.php` ファイルはComposerが生成したオートローダー定義を読み込み、`bootstrap/app.php` からLaravelアプリケーションのインスタンスを取得します。Laravelが最初に行う処理は、アプリケーション/[サービスコンテナ](/jp/service-container)のインスタンスを生成することです。 ### HTTPカーネル 次に、受け取ったリクエストはHTTPカーネル(`Illuminate\Foundation\Http\Kernel`)に送られます。アプリケーションインスタンスの `handleRequest` メソッドを使って処理されます。 HTTPカーネルは、リクエストが実行される前に実行される**ブートストラッパー**の配列を定義しています。これらのブートストラッパーは次の処理を行います。 * エラーハンドリングの設定 * ロギングの設定 * [アプリケーション環境の検出](/jp/installation) * その他リクエスト処理前に必要なタスク HTTPカーネルは、リクエストをアプリケーションのミドルウェアスタックに通す責任も持ちます。これらのミドルウェアは、[HTTPセッション](/jp/session)の読み書き、アプリケーションがメンテナンスモードかどうかの確認、[CSRFトークンの検証](/jp/middleware)などを処理します。 HTTPカーネルの `handle` メソッドのシグネチャはシンプルです。`Request` を受け取り、`Response` を返します。カーネルをアプリケーション全体を表す大きなブラックボックスと考えてください。HTTPリクエストを渡すとHTTPレスポンスが返ってきます。 ### サービスプロバイダー カーネルのブートストラッピングで最も重要な処理の一つが、アプリケーションの[サービスプロバイダー](/jp/service-providers)をロードすることです。サービスプロバイダーは、データベース、キュー、バリデーション、ルーティングなど、フレームワークのさまざまなコンポーネントをブートストラップする責任を持ちます。 Laravelはプロバイダーのリストをイテレートし、各プロバイダーをインスタンス化します。インスタンス化後、すべてのプロバイダーの `register` メソッドが呼び出されます。次に、すべてのプロバイダーが登録されると、各プロバイダーの `boot` メソッドが呼び出されます。これにより、サービスプロバイダーは `boot` メソッドが実行される時点で、すべてのコンテナバインディングが登録・利用可能な状態になっています。 ユーザー定義またはサードパーティのサービスプロバイダーは `bootstrap/providers.php` ファイルで登録します。 ### ルーティング アプリケーションがブートストラップされ、すべてのサービスプロバイダーが登録されると、`Request` はルーターに渡されてディスパッチされます。ルーターはリクエストをルートまたはコントローラーにディスパッチし、ルート固有のミドルウェアも実行します。 ミドルウェアは、アプリケーションに入るHTTPリクエストをフィルタリングまたは検査する便利な仕組みを提供します。たとえば、Laravelにはユーザーが認証されているかどうかを確認するミドルウェアが含まれています。ユーザーが認証されていない場合、ミドルウェアはログイン画面にリダイレクトします。認証されている場合は、リクエストをアプリケーション内部に進めます。 リクエストがマッチしたルートに割り当てられたすべてのミドルウェアを通過すると、ルートまたはコントローラーメソッドが実行され、レスポンスが返されます。 ### レスポンスの返却 ルートまたはコントローラーメソッドがレスポンスを返すと、そのレスポンスはルートのミドルウェアを通って外側に戻り、アプリケーションが送信するレスポンスを変更または検査する機会を得ます。 最終的に、レスポンスがミドルウェアを通り抜けると、HTTPカーネルの `handle` メソッドはレスポンスオブジェクトをアプリケーションインスタンスの `handleRequest` に返し、このメソッドが返されたレスポンスの `send` メソッドを呼び出します。`send` メソッドはレスポンスの内容をユーザーのWebブラウザに送信します。これでLaravelのリクエストライフサイクル全体の旅が完了します。 ## Consoleコマンドのライフサイクル ### 全体の流れ ```mermaid theme={null} flowchart TD A[artisanコマンド実行] --> B[artisan] B --> C[Composerオートローダー読み込み] C --> D[bootstrap/app.php] D --> E[アプリケーションインスタンス生成
    サービスコンテナ] E --> F[コンソールカーネル
    handleCommand] F --> G[サービスプロバイダー登録・起動
    register → boot] G --> H[コマンドのロード
    app/Console/Commands] H --> I[コマンド実行] I --> J[終了コードを返す] ``` ### artisanエントリーポイント コンソールコマンドのエントリーポイントはプロジェクトルートの `artisan` ファイルです。HTTPリクエストと同様に、ComposerのオートローダーとLaravelアプリケーションのインスタンスが生成されます。 次に、アプリケーションインスタンスの `handleCommand` メソッドを通じてコンソールカーネルに処理が渡されます。 ### コンソールカーネルとコマンド実行 コンソールカーネルもHTTPカーネルと同様にサービスプロバイダーをロードします。すべてのプロバイダーが登録・起動された後、`app/Console/Commands` ディレクトリのコマンドがロードされ、指定されたコマンドが実行されます。 ```shell theme={null} php artisan make:controller UserController ``` このコマンドは次のように処理されます。 1. `artisan` ファイルがComposerオートローダーを読み込む 2. `bootstrap/app.php` からアプリケーションインスタンスを生成 3. コンソールカーネルがサービスプロバイダーをロード 4. `make:controller` コマンドを検索して実行 5. 終了コードを返す ## サービスプロバイダーに注目する サービスプロバイダーは、Laravelアプリケーションをブートストラップするための真の鍵です。アプリケーションインスタンスが生成され、サービスプロバイダーが登録され、リクエストがブートストラップされたアプリケーションに渡される。それだけです。 サービスプロバイダーを通じてLaravelアプリケーションがどのように構築・ブートストラップされるかをしっかり把握することは非常に価値があります。アプリケーションのユーザー定義サービスプロバイダーは `app/Providers` ディレクトリに保存されます。 デフォルトの `AppServiceProvider` はほぼ空の状態です。このプロバイダーは、アプリケーション独自のブートストラッピングやサービスコンテナのバインディングを追加するのに最適な場所です。大規模なアプリケーションでは、アプリケーションで使用する特定のサービスのブートストラッピングを細かく分けた複数のサービスプロバイダーを作成することをお勧めします。 ### register と boot の違い サービスプロバイダーには2つの主要なメソッドがあります。 | メソッド | タイミング | 用途 | | - | - | - | | `register` | 全プロバイダーのインスタンス化後に全て呼び出される | サービスコンテナへのバインディング登録のみ | | `boot` | 全プロバイダーの `register` 完了後に呼び出される | ビューコンポーザー、イベントリスナー、その他の初期化 | `register` メソッド内でイベントリスナー、ルート、その他の機能を登録しないでください。まだロードされていないサービスプロバイダーが提供するサービスを誤って使用してしまう可能性があります。 ## 次のステップ 依存性注入とサービスコンテナの仕組みを理解します。 サービスプロバイダーを使ってアプリケーションをブートストラップする方法を学びます。 # ミドルウェア Source: https://kawax.biz/jp/middleware Laravelのミドルウェアを使ってHTTPリクエストのフィルタリングと前処理を行う方法を解説します。 ## ミドルウェアとは ミドルウェアは、アプリケーションに届くHTTPリクエストを検査・フィルタリングする仕組みです。 リクエストがコントローラーに渡される前後に処理を挟み込めます。 例えば、Laravelには認証ミドルウェアが含まれています。 ユーザーが認証されていない場合はログイン画面にリダイレクトし、認証済みの場合はリクエストをそのままアプリケーション内部に通します。 認証以外にも、ログ記録・CSRFプロテクション・レート制限など、さまざまな目的でミドルウェアを作成できます。Laravel 13のCSRF保護の詳細は [CSRFプロテクション](/jp/csrf) を参照してください。 ユーザーが定義したミドルウェアは、通常 `app/Http/Middleware` ディレクトリに置きます。 ```mermaid theme={null} flowchart TD A["HTTPリクエスト"] --> B["グローバルミドルウェア
    (すべてのリクエストに適用)"] B --> C["ルートミドルウェア
    (特定のルートに適用)"] C --> D["コントローラー / クロージャ"] D --> E["レスポンス生成"] E --> F["ルートミドルウェア
    (後処理)"] F --> G["グローバルミドルウェア
    (後処理)"] G --> H["HTTPレスポンス"] ``` ## 組み込みミドルウェア Laravelはデフォルトで `web` と `api` の2つのミドルウェアグループを用意しています。 `routes/web.php` には `web` グループが、`routes/api.php` には `api` グループが自動的に適用されます。 | `web` ミドルウェアグループ | | - | | `EncryptCookies` | | `AddQueuedCookiesToResponse` | | `StartSession` | | `ShareErrorsFromSession` | | `PreventRequestForgery` (CSRF保護) | | `SubstituteBindings` | また、よく使うミドルウェアにはエイリアスが設定されており、短い名前で参照できます。 | エイリアス | ミドルウェア | | - | - | | `auth` | `Illuminate\Auth\Middleware\Authenticate` | | `guest` | `Illuminate\Auth\Middleware\RedirectIfAuthenticated` | | `verified` | `Illuminate\Auth\Middleware\EnsureEmailIsVerified` | | `throttle` | `Illuminate\Routing\Middleware\ThrottleRequests` | ## ミドルウェアの作成 `make:middleware` Artisanコマンドで新しいミドルウェアを作成します。 ```shell theme={null} php artisan make:middleware EnsureTokenIsValid ``` `app/Http/Middleware/EnsureTokenIsValid.php` が生成されます。 `handle` メソッドにリクエストを処理するロジックを記述します。 ```php theme={null} input('token') !== 'my-secret-token') { return redirect('/home'); } return $next($request); } } ``` `$next($request)` を呼び出すと、リクエストがアプリケーションの次の処理に渡されます。 条件を満たさない場合はリダイレクトやレスポンスを返してリクエストを止めます。 ### リクエスト前後の処理 ミドルウェアはリクエストの**前**または**後**に処理を実行できます。 ```php theme={null} // リクエスト前に処理する public function handle(Request $request, Closure $next): Response { // ここに前処理を書く return $next($request); } ``` ```php theme={null} // レスポンス後に処理する public function handle(Request $request, Closure $next): Response { $response = $next($request); // ここに後処理を書く return $response; } ``` ## ミドルウェアの登録 ### グローバルミドルウェア すべてのリクエストに対してミドルウェアを実行したい場合は、`bootstrap/app.php` の `withMiddleware` でグローバルスタックに追加します。 ```php theme={null} // bootstrap/app.php use App\Http\Middleware\EnsureTokenIsValid; return Application::configure(basePath: dirname(__DIR__)) ->withMiddleware(function (Middleware $middleware): void { $middleware->append(EnsureTokenIsValid::class); }); ``` `append` はスタックの末尾に追加します。先頭に追加するには `prepend` を使います。 ### ルートへのミドルウェア適用 特定のルートだけにミドルウェアを適用するには、ルート定義で `middleware` メソッドを呼び出します。 ```php theme={null} use App\Http\Middleware\EnsureTokenIsValid; Route::get('/profile', function () { // ... })->middleware(EnsureTokenIsValid::class); ``` 複数のミドルウェアを適用する場合は配列で渡します。 ```php theme={null} Route::get('/', function () { // ... })->middleware([First::class, Second::class]); ``` 特定のルートだけミドルウェアを除外したい場合は `withoutMiddleware` を使います。 ```php theme={null} use App\Http\Middleware\EnsureTokenIsValid; Route::middleware([EnsureTokenIsValid::class])->group(function () { Route::get('/', function () { // このルートにはミドルウェアが適用される }); Route::get('/profile', function () { // このルートはミドルウェアを除外 })->withoutMiddleware([EnsureTokenIsValid::class]); }); ``` ### ミドルウェアエイリアス 長いクラス名に短いエイリアスを定義できます。`bootstrap/app.php` で設定します。 ```php theme={null} // bootstrap/app.php use App\Http\Middleware\EnsureUserIsSubscribed; return Application::configure(basePath: dirname(__DIR__)) ->withMiddleware(function (Middleware $middleware): void { $middleware->alias([ 'subscribed' => EnsureUserIsSubscribed::class, ]); }); ``` エイリアスを使ってルートに適用できます。 ```php theme={null} Route::get('/profile', function () { // ... })->middleware('subscribed'); ``` ## ミドルウェアグループ 複数のミドルウェアを1つのキーでまとめると、ルートへの適用が簡単になります。 `bootstrap/app.php` で `appendToGroup` または `prependToGroup` を使います。 ```php theme={null} // bootstrap/app.php use App\Http\Middleware\First; use App\Http\Middleware\Second; return Application::configure(basePath: dirname(__DIR__)) ->withMiddleware(function (Middleware $middleware): void { $middleware->appendToGroup('group-name', [ First::class, Second::class, ]); }); ``` グループはルートに通常のミドルウェアと同じ方法で適用できます。 ```php theme={null} Route::get('/', function () { // ... })->middleware('group-name'); Route::middleware(['group-name'])->group(function () { // ... }); ``` 既存の `web` グループや `api` グループにミドルウェアを追加する場合は専用のメソッドが使えます。 ```php theme={null} // bootstrap/app.php use App\Http\Middleware\EnsureUserIsSubscribed; return Application::configure(basePath: dirname(__DIR__)) ->withMiddleware(function (Middleware $middleware): void { $middleware->web(append: [ EnsureUserIsSubscribed::class, ]); }); ``` ## ミドルウェアパラメーター ミドルウェアは追加のパラメーターを受け取れます。 `handle` メソッドの `$next` 引数の後にパラメーターを追加します。 ```php theme={null} user()->hasRole($role)) { return redirect('/home'); } return $next($request); } } ``` ルート定義でミドルウェア名とパラメーターを `:` で区切って指定します。 ```php theme={null} use App\Http\Middleware\EnsureUserHasRole; Route::put('/post/{id}', function (string $id) { // ... })->middleware(EnsureUserHasRole::class.':editor'); ``` 複数のパラメーターはカンマで区切ります。 ```php theme={null} Route::put('/post/{id}', function (string $id) { // ... })->middleware(EnsureUserHasRole::class.':editor,publisher'); ``` ## 実例:認証チェックミドルウェア ログインしていないユーザーをリダイレクトするシンプルな例です。 ```shell theme={null} php artisan make:middleware RedirectIfNotAuthenticated ``` ```php theme={null} user()) { return redirect('/login'); } return $next($request); } } ``` ルートに適用します。 ```php theme={null} Route::get('/dashboard', function () { return view('dashboard'); })->middleware(RedirectIfNotAuthenticated::class); ``` Laravelには認証用の `auth` ミドルウェアが組み込まれています。独自に実装する前に、既存のミドルウェアで要件を満たせないか確認しましょう。 ## 次のステップ コントローラーでリクエストデータを受け取る方法を学びます。 # データベースマイグレーション Source: https://kawax.biz/jp/migrations Laravelのマイグレーション機能を使ってデータベーススキーマをバージョン管理する方法を解説します。 ## マイグレーションとは マイグレーションはデータベースのバージョン管理のようなものです。 チームがアプリケーションのデータベーススキーマ定義を共有・管理できます。 ソースコードを pull した後に「ローカルのデータベースにカラムを手動で追加してください」とチームメンバーに伝えなければならない場面を経験したことがあるでしょう。 マイグレーションはその問題を解決します。 マイグレーションファイルは `database/migrations` ディレクトリに格納されます。 各ファイル名にはタイムスタンプが含まれており、Laravelがマイグレーションの実行順序を決定するために使われます。 ## マイグレーションファイルの作成 `make:migration` Artisanコマンドで新しいマイグレーションファイルを生成します。 ```shell theme={null} php artisan make:migration create_posts_table ``` マイグレーション名から Laravelはテーブル名を推測し、適切なスタブを生成します。 `create_posts_table` という名前であれば、`posts` テーブルを作成するコードがあらかじめ用意されます。 ## マイグレーションの構造 マイグレーションクラスは `up` と `down` の2つのメソッドを持ちます。 * `up` メソッド:テーブル・カラム・インデックスをデータベースに追加します。 * `down` メソッド:`up` の操作を元に戻します。ロールバック時に呼ばれます。 ```php theme={null} id(); $table->string('title'); $table->text('body'); $table->boolean('published')->default(false); $table->timestamps(); }); } /** * マイグレーションを元に戻す */ public function down(): void { Schema::dropIfExists('posts'); } }; ``` ## 主なカラム定義メソッド `Blueprint` クラスには多くのカラム型が用意されています。 | メソッド | 説明 | | - | - | | `$table->id()` | 自動増分の主キー(`bigIncrements('id')` の別名) | | `$table->string('name')` | VARCHAR相当のカラム(デフォルト255文字) | | `$table->text('body')` | TEXTカラム | | `$table->integer('count')` | INTEGERカラム | | `$table->boolean('active')` | TINYINTで真偽値を格納 | | `$table->timestamp('published_at')` | TIMESTAMPカラム | | `$table->timestamps()` | `created_at` と `updated_at` をまとめて追加 | | `$table->softDeletes()` | 論理削除用の `deleted_at` カラムを追加 | | `$table->foreignId('user_id')` | 外部キー用の `BIGINT UNSIGNEDカラム` | ### カラム修飾子 カラム定義にはメソッドチェーンで修飾子を追加できます。 ```php theme={null} $table->string('email')->unique(); $table->string('name')->nullable(); $table->integer('votes')->default(0); $table->string('title')->after('id'); // 指定カラムの後に配置 ``` ### ベクターカラムとインデックス `vector` メソッドは、埋め込みベクトルを保存する `vector` カラムを作成します。`dimensions` には、使用する埋め込みプロバイダーの出力次元数を指定してください。 ```php theme={null} $table->vector('embedding', dimensions: 1536); ``` ベクターカラムは `pgvector` 拡張を使用する PostgreSQL 接続と、MariaDB 11.7以降で利用できます。PostgreSQLでは、ベクターカラムを作成する前に `pgvector` を有効化してください。 ```php theme={null} Schema::ensureVectorExtensionExists(); ``` ベクターの類似検索を高速化するには、`vector` カラムに `index` を追加します。このインデックスは cosine distance を使います。 ```php theme={null} $table->vector('embedding', dimensions: 1536)->index(); ``` 既存のベクターインデックスを削除する場合は `dropVectorIndex` を使います。 ```php theme={null} $table->dropVectorIndex('documents_embedding_vectorindex'); ``` ### オンラインでのインデックス作成 大きなテーブルでインデックスを作成すると、通常はインデックスの構築中にテーブルがロックされ、読み書きが待たされることがあります。MySQLでは、インデックスや外部キーの定義に `inplace` 修飾子を付けると、`INPLACE` アルゴリズムを使って操作できます。 ```php theme={null} Schema::table('users', function (Blueprint $table) { $table->index('email')->inplace(); $table->foreign('team_id') ->references('id') ->on('teams') ->inplace(); }); ``` `inplace` は `lock` 修飾子と組み合わせて、操作中のテーブルロックを指定できます。`none` を指定すると、MySQLが許可する操作では読み書きを継続できます。 ```php theme={null} Schema::table('users', function (Blueprint $table) { $table->index('email')->inplace()->lock('none'); }); ``` この機能はMySQL専用です。利用する操作が `INPLACE` アルゴリズムと指定したロックモードに対応しているかは、[MySQLのオンラインDDL操作のドキュメント](https://dev.mysql.com/doc/refman/8.0/en/innodb-online-ddl-operations.html)で確認してください。外部キー操作で `inplace` を使う場合は、あらかじめ外部キー制約を無効にする必要があります。 ## マイグレーションの実行 `migrate` コマンドで未実行のマイグレーションをすべて実行します。 ```shell theme={null} php artisan migrate ``` 実行済みのマイグレーションと未実行のマイグレーションを確認するには `migrate:status` を使います。 ```shell theme={null} php artisan migrate:status ``` 本番環境でマイグレーションを実行すると確認プロンプトが表示されます。 確認なしで実行したい場合は `--force` フラグを使いますが、データが失われる操作もあるため慎重に行ってください。 ## ロールバック 直近のマイグレーションバッチを取り消すには `migrate:rollback` を使います。 ```shell theme={null} php artisan migrate:rollback ``` 特定のステップ数だけロールバックするには `--step` オプションを指定します。 ```shell theme={null} # 直近5件のマイグレーションをロールバック php artisan migrate:rollback --step=5 ``` すべてのマイグレーションをロールバックしてから再実行するには `migrate:refresh` を使います。 ```shell theme={null} php artisan migrate:refresh ``` `migrate:refresh` はすべてのテーブルを作り直すため、既存のデータは失われます。 開発中のデータベースリセットに便利です。 ## 実践例:postsテーブルの作成 ブログ投稿を管理する `posts` テーブルを例に、一連の流れを確認します。 ### 1. マイグレーションファイルの生成 ```shell theme={null} php artisan make:migration create_posts_table ``` ### 2. マイグレーションの編集 `database/migrations/xxxx_xx_xx_xxxxxx_create_posts_table.php` を開いて編集します。 ```php theme={null} id(); $table->foreignId('user_id')->constrained()->cascadeOnDelete(); $table->string('title'); $table->text('body'); $table->boolean('published')->default(false); $table->timestamp('published_at')->nullable(); $table->timestamps(); }); } public function down(): void { Schema::dropIfExists('posts'); } }; ``` ### 3. マイグレーションの実行 ```shell theme={null} php artisan migrate ``` 実行後、データベースに `posts` テーブルが作成されます。 ### 4. カラムを後から追加する テーブル作成後に新しいカラムを追加したい場合は、既存のマイグレーションを編集せず、新しいマイグレーションを作成します。 ```shell theme={null} php artisan make:migration add_excerpt_to_posts_table ``` ```php theme={null} public function up(): void { Schema::table('posts', function (Blueprint $table) { $table->string('excerpt')->nullable()->after('title'); }); } public function down(): void { Schema::table('posts', function (Blueprint $table) { $table->dropColumn('excerpt'); }); } ``` 既存のマイグレーションファイルを直接編集するのは避けましょう。 他のメンバーや本番環境との整合性が崩れます。 変更は常に新しいマイグレーションとして追加します。 ## マイグレーションイベント マイグレーション操作のたびに[イベント](/jp/events)が発行されます。すべてのイベントは `Illuminate\Database\Events\MigrationEvent` を継承しています。 | クラス | 説明 | | - | - | | `Illuminate\Database\Events\DatabaseRefreshed` | `migrate:refresh` コマンドの完了後 | | `Illuminate\Database\Events\MigrationsStarted` | マイグレーションのバッチが実行される直前 | | `Illuminate\Database\Events\MigrationsEnded` | マイグレーションのバッチの完了後 | | `Illuminate\Database\Events\MigrationStarted` | 単一のマイグレーションが実行される直前 | | `Illuminate\Database\Events\MigrationEnded` | 単一のマイグレーションの完了後 | | `Illuminate\Database\Events\NoPendingMigrations` | マイグレーションコマンドが未実行のマイグレーションなしと判定した時 | | `Illuminate\Database\Events\SchemaDumped` | データベーススキーマダンプの完了後 | | `Illuminate\Database\Events\SchemaLoaded` | 既存のスキーマダンプのロード後 | たとえば `MigrationsEnded` イベントをリッスンして、マイグレーション完了後にキャッシュをクリアすることができます。 ```php theme={null} use Illuminate\Database\Events\MigrationsEnded; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Event; Event::listen(MigrationsEnded::class, function () { Cache::flush(); }); ``` ## 次のステップ マイグレーションで作成したテーブルにサンプルデータを投入する方法を学びます。 マイグレーションで作成したテーブルをEloquent ORMで操作する方法を学びます。 # モック Source: https://kawax.biz/jp/mocking Laravelのモック・テストダブル機能を使って、テストを独立した単位で実行する方法 ## なぜモックを使うのか テストを書くとき、メールの送信・キャッシュの読み書き・外部APIの呼び出しなど、実際には実行したくない処理があります。 こうした処理を「本物のふりをする偽物」で置き換えることをモックと呼びます。 モックを使うメリットは次のとおりです。 * **テストが速い** — 外部サービスや重い処理を実行しないため、テストがすぐ終わる * **テストが安定する** — 外部サービスの状態に左右されず、常に同じ結果になる * **テストの範囲が明確になる** — 1つのクラスやメソッドだけを検証できる Laravelはイベント・ジョブ・ファサードなどのモックをすぐに使えるヘルパーを提供しています。 内部では [Mockery](https://github.com/mockery/mockery) を使っており、複雑なセットアップなしに利用できます。 ```mermaid theme={null} graph LR A["テスト"] --> B["モック
    (偽物)"] A --> C["本番コード"] B --> D["実行されない
    (外部API・メールなど)"] C --> B style B fill:#f9a,stroke:#c66 style D fill:#eee,stroke:#aaa ``` ## モックオブジェクト サービスコンテナ経由で注入されるオブジェクトをモックするには、モックインスタンスをコンテナへバインドします。 こうすると、コンテナはオブジェクトを生成する代わりにモックインスタンスを使います。 ```php Pest theme={null} use App\Service; use Mockery; use Mockery\MockInterface; test('something can be mocked', function () { $this->instance( Service::class, Mockery::mock(Service::class, function (MockInterface $mock) { $mock->expects('process'); }) ); }); ``` ```php PHPUnit theme={null} use App\Service; use Mockery; use Mockery\MockInterface; public function test_something_can_be_mocked(): void { $this->instance( Service::class, Mockery::mock(Service::class, function (MockInterface $mock) { $mock->expects('process'); }) ); } ``` ### `mock()` メソッド Laravelのテストケース基底クラスが提供する `mock()` メソッドを使うと、上記と同等の処理をより簡潔に書けます。 ```php theme={null} use App\Service; use Mockery\MockInterface; $mock = $this->mock(Service::class, function (MockInterface $mock) { $mock->expects('process'); }); ``` ### `partialMock()` メソッド オブジェクトの一部のメソッドだけをモックしたい場合は `partialMock()` を使います。 モックしていないメソッドは通常通り実行されます。 ```php theme={null} use App\Service; use Mockery\MockInterface; $mock = $this->partialMock(Service::class, function (MockInterface $mock) { $mock->expects('process'); }); ``` ### `spy()` メソッド スパイはモックに似ていますが、コードの実行後にインタラクションを検証する点が異なります。 モックが「このメソッドが呼ばれるはずだ」と事前に設定するのに対し、スパイは「このメソッドが呼ばれたかどうか」をあとから確認します。 ```php theme={null} use App\Service; $spy = $this->spy(Service::class); // ...テスト対象のコードを実行... $spy->shouldHaveReceived('process'); ``` ```mermaid theme={null} graph TD subgraph "モック (Mock)" M1["事前に期待値を設定
    expects('process')"] --> M2["コードを実行"] --> M3["自動検証"] end subgraph "スパイ (Spy)" S1["スパイを設定"] --> S2["コードを実行"] --> S3["あとから検証
    shouldHaveReceived('process')"] end ``` ## ファサードのモック [ファサード](/jp/facades)は通常の静的メソッド呼び出しと異なり、モックができます。 依存性注入と同等のテスタビリティを持ちながら、簡潔な構文で書けます。 例として、キャッシュを使うコントローラーを考えます。 ```php theme={null} ```php Pest theme={null} with('key') ->andReturn('value'); $response = $this->get('/users'); // ... }); ``` ```php PHPUnit theme={null} with('key') ->andReturn('value'); $response = $this->get('/users'); // ... } } ``` `Request` ファサードはモックしないでください。その代わりに、`get` や `post` などのHTTPテストメソッドに入力値を渡してください。同様に、`Config` ファサードをモックする代わりに `Config::set()` をテスト内で呼び出してください。 ## ファサードスパイ ファサードをスパイで監視するには、対応するファサードの `spy()` メソッドを呼び出します。 スパイはコードが実行された後にインタラクションを検証したいときに便利です。 ```php Pest theme={null} get('/'); $response->assertStatus(200); Cache::shouldHaveReceived('put')->with('name', 'Taylor', 10); }); ``` ```php PHPUnit theme={null} use Illuminate\Support\Facades\Cache; public function test_values_are_stored_in_cache(): void { Cache::spy(); $response = $this->get('/'); $response->assertStatus(200); Cache::shouldHaveReceived('put')->with('name', 'Taylor', 10); } ``` ## 時間の操作 テストで時間に依存するロジックを検証するとき、`now()` や `Carbon::now()` が返す時刻を変更できると便利です。 Laravelのフィーチャーテスト基底クラスには、時間を操作するためのヘルパーが用意されています。 ### `travel()` — 時間を移動する ```php Pest theme={null} test('time can be manipulated', function () { // 未来へ進む $this->travel(5)->milliseconds(); $this->travel(5)->seconds(); $this->travel(5)->minutes(); $this->travel(5)->hours(); $this->travel(5)->days(); $this->travel(5)->weeks(); $this->travel(5)->years(); // 過去へ戻る $this->travel(-5)->hours(); // 特定の時刻へ移動する $this->travelTo(now()->subHours(6)); // 現在時刻に戻る $this->travelBack(); }); ``` ```php PHPUnit theme={null} public function test_time_can_be_manipulated(): void { // 未来へ進む $this->travel(5)->milliseconds(); $this->travel(5)->seconds(); $this->travel(5)->minutes(); $this->travel(5)->hours(); $this->travel(5)->days(); $this->travel(5)->weeks(); $this->travel(5)->years(); // 過去へ戻る $this->travel(-5)->hours(); // 特定の時刻へ移動する $this->travelTo(now()->subHours(6)); // 現在時刻に戻る $this->travelBack(); } ``` ### クロージャを使った時間移動 時間移動メソッドにクロージャを渡すと、指定した時刻で時間を止めてクロージャを実行し、完了後に元の時刻に戻ります。 ```php theme={null} $this->travel(5)->days(function () { // 5日後の状態でテストする }); $this->travelTo(now()->subDays(10), function () { // 特定の瞬間でテストする }); ``` ### `freezeTime()` — 時間を止める `freezeTime()` は現在時刻を固定します。`freezeSecond()` は現在時刻を秒の先頭で固定します。 ```php theme={null} use Illuminate\Support\Carbon; // 時間を固定してクロージャを実行し、終了後に再開する $this->freezeTime(function (Carbon $time) { // ... }); // 現在の秒の先頭で固定してクロージャを実行する $this->freezeSecond(function (Carbon $time) { // ... }); ``` ### 実用例:非アクティブなスレッドのロック 時間操作は、一定期間操作がないと投稿がロックされるフォーラムのような機能のテストに役立ちます。 ```php Pest theme={null} use App\Models\Thread; test('forum threads lock after one week of inactivity', function () { $thread = Thread::factory()->create(); $this->travel(1)->week(); expect($thread->isLockedByInactivity())->toBeTrue(); }); ``` ```php PHPUnit theme={null} use App\Models\Thread; public function test_forum_threads_lock_after_one_week_of_inactivity(): void { $thread = Thread::factory()->create(); $this->travel(1)->week(); $this->assertTrue($thread->isLockedByInactivity()); } ``` `travel()` などの時間操作メソッドは、フィーチャーテスト(`Tests\TestCase` を継承するクラス)でのみ使えます。PHPUnitの基底 `TestCase` では使えません。 ## メソッド一覧 ### モック関連 | メソッド | 説明 | | - | - | | `$this->mock(Class::class, fn)` | クラスの完全なモックを作成してコンテナに登録する | | `$this->partialMock(Class::class, fn)` | 一部のメソッドだけをモックする | | `$this->spy(Class::class)` | スパイを作成してコンテナに登録する | | `$this->instance(Class::class, $mock)` | 任意のモックインスタンスをコンテナに登録する | | `Facade::expects('method')` | ファサードのメソッドをモックする | | `Facade::spy()` | ファサードをスパイで監視する | | `$spy->shouldHaveReceived('method')` | スパイに対してメソッドが呼ばれたか検証する | ### 時間操作関連 | メソッド | 説明 | | - | - | | `$this->travel(n)->unit()` | 指定した単位だけ時間を移動する | | `$this->travelTo(Carbon)` | 特定の時刻へ移動する | | `$this->travelBack()` | 現在時刻に戻る | | `$this->freezeTime(fn)` | 時間を固定してクロージャを実行する | | `$this->freezeSecond(fn)` | 秒の先頭で時間を固定してクロージャを実行する | # MongoDB Source: https://kawax.biz/jp/mongodb LaravelアプリケーションでMongoDBを使う方法。ドライバのインストールから設定、Eloquentモデル、クエリビルダー、キャッシュ・キュードライバとしての利用まで解説します。 ## はじめに [MongoDB](https://www.mongodb.com/resources/products/fundamentals/why-use-mongodb) は最も人気のある NoSQL ドキュメント指向データベースの一つです。高い書き込み性能(分析や IoT に最適)、高可用性(レプリカセットによる自動フェイルオーバー)、水平スケーリング(シャーディング)、強力なクエリ言語(集計・全文検索・地理空間クエリ)が特徴です。 SQL データベースの行・列形式とは異なり、MongoDB の各レコードは BSON(バイナリ JSON)形式のドキュメントです。アプリケーションはこのデータを JSON 形式で取得できます。 ```mermaid theme={null} flowchart LR A["Laravel
    アプリケーション"] --> B["mongodb/laravel-mongodb
    パッケージ"] B --> C["MongoDB PHP ドライバ"] C --> D["MongoDB サーバー"] subgraph features ["主な用途"] E["Eloquent モデル"] F["クエリビルダー"] G["キャッシュドライバ"] H["キュードライバ"] I["GridFS ファイル保存"] J["ベクトル検索"] K["全文検索 (Scout)"] end B --> E B --> F B --> G B --> H B --> I B --> J B --> K ``` Laravel で MongoDB を使う場合は、MongoDB 公式がメンテナンスする `mongodb/laravel-mongodb` パッケージの使用を推奨します。このパッケージは Eloquent や Laravel の各種機能とのリッチな統合を提供します。 ## インストール ### MongoDB PHP ドライバ MongoDB に接続するには `mongodb` PHP 拡張が必要です。[Laravel Herd](https://herd.laravel.com) や `php.new` を使っている場合はすでにインストール済みです。手動でインストールするには PECL を使います。 ```shell theme={null} pecl install mongodb ``` インストール詳細は [MongoDB PHP 拡張インストールガイド](https://www.php.net/manual/en/mongodb.installation.php) を参照してください。 `mongodb` PHP 拡張が CLI と Web サーバーの両方で有効になっていることを確認してください。設定が異なる場合があります。 ### MongoDB サーバーの起動 MongoDB Community Server はローカル開発用に使用できます。Windows、macOS、Linux、Docker でのインストール方法は [公式インストールガイド](https://docs.mongodb.com/manual/administration/install-community/) を参照してください。 **Docker を使う場合:** ```yaml theme={null} # docker-compose.yml services: mongodb: image: mongo:8 ports: - "27017:27017" environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: password MONGO_INITDB_DATABASE: laravel_app volumes: - mongodb_data:/data/db volumes: mongodb_data: ``` ```shell theme={null} docker compose up -d ``` クラウドでのホスティングには [MongoDB Atlas](https://www.mongodb.com/cloud/atlas) が利用できます。Atlas クラスターにローカルからアクセスするには、プロジェクトの IP アクセスリストに自分の IP アドレスを追加する必要があります。 ### laravel-mongodb パッケージのインストール Composer で `mongodb/laravel-mongodb` パッケージをインストールします。 ```shell theme={null} composer require mongodb/laravel-mongodb ``` ## 設定 ### 環境変数 `.env` ファイルに MongoDB の接続情報を追加します。 ```ini theme={null} MONGODB_URI="mongodb://localhost:27017" MONGODB_DATABASE="laravel_app" ``` MongoDB Atlas を使う場合は接続文字列を Atlas のものに変更します。 ```ini theme={null} MONGODB_URI="mongodb+srv://:@.mongodb.net/?retryWrites=true&w=majority" MONGODB_DATABASE="laravel_app" ``` ### config/database.php `config/database.php` の `connections` 配列に `mongodb` 接続を追加します。 ```php theme={null} 'connections' => [ // ... 既存の接続設定 ... 'mongodb' => [ 'driver' => 'mongodb', 'dsn' => env('MONGODB_URI', 'mongodb://localhost:27017'), 'database' => env('MONGODB_DATABASE', 'laravel_app'), ], ], ``` MySQL などのリレーショナル DB と MongoDB を同時に使う場合は、`default` はそのままにして `mongodb` 接続を追加するだけで OK です。モデルごとに接続を切り替えられます。 ## 主要機能 ### Eloquent モデル `MongoDB\Laravel\Eloquent\Model` を継承することで、通常の Eloquent モデルとほぼ同じ感覚で MongoDB を操作できます。 ```php theme={null} MongoDB はスキーマレスなので、マイグレーションは不要です。ドキュメントを保存すると自動的にコレクションが作成されます。 #### 基本的な CRUD 操作 通常の Eloquent と同じ API で操作できます。 ```php theme={null} use App\Models\Article; // 作成 $article = Article::create([ 'title' => 'MongoDB入門', 'body' => 'MongoDBはドキュメント指向データベースです。', 'tags' => ['nosql', 'mongodb', 'laravel'], 'published_at' => now(), ]); // 取得 $article = Article::find('64f1a2b3c4d5e6f7a8b9c0d1'); $articles = Article::where('tags', 'nosql')->get(); // 更新 $article->update(['title' => '改訂版 MongoDB入門']); // 削除 $article->delete(); ``` #### 配列・埋め込みドキュメント MongoDB の強みである配列や埋め込みドキュメントをそのまま扱えます。 ```php theme={null} // 配列フィールドへの push $article->push('tags', 'database'); // 配列フィールドからの pull $article->pull('tags', 'nosql'); // 埋め込みドキュメントのクエリ $articles = Article::where('meta.author', 'Taylor')->get(); ``` ### クエリビルダー MongoDB のクエリビルダーを使って複雑なクエリを記述できます。詳細は [laravel-mongodb クエリビルダードキュメント](https://www.mongodb.com/docs/drivers/php/laravel-mongodb/current/query-builder/) を参照してください。 ```php theme={null} use Illuminate\Support\Facades\DB; // 基本的なクエリ $articles = DB::connection('mongodb') ->collection('articles') ->where('tags', 'laravel') ->orderBy('published_at', 'desc') ->limit(10) ->get(); // 集計パイプライン $stats = DB::connection('mongodb') ->collection('orders') ->raw(function ($collection) { return $collection->aggregate([ ['$group' => ['_id' => '$status', 'total' => ['$sum' => '$amount']]], ['$sort' => ['total' => -1]], ]); }); ``` ### キャッシュドライバ MongoDB のキャッシュドライバは TTL インデックスを使って期限切れエントリを自動的に削除します。詳細は [キャッシュドライバドキュメント](https://www.mongodb.com/docs/drivers/php/laravel-mongodb/current/cache/) を参照してください。 `config/cache.php` にストアを追加します。 ```php theme={null} 'stores' => [ 'mongodb' => [ 'driver' => 'mongodb', 'connection' => 'mongodb', 'collection' => 'cache', ], ], ``` `.env` でキャッシュドライバを MongoDB に変更します。 ```ini theme={null} CACHE_STORE=mongodb ``` ### キュードライバ MongoDB をキュードライバとして使うことができます。詳細は [キュードライバドキュメント](https://www.mongodb.com/docs/drivers/php/laravel-mongodb/current/queues/) を参照してください。 `config/queue.php` に接続を追加します。 ```php theme={null} 'connections' => [ 'mongodb' => [ 'driver' => 'mongodb', 'connection' => 'mongodb', 'collection' => 'jobs', 'queue' => env('MONGODB_QUEUE', 'default'), 'retry_after' => (int) env('MONGODB_QUEUE_RETRY_AFTER', 90), 'after_commit' => false, ], ], ``` `.env` でキュー接続を MongoDB に変更します。 ```ini theme={null} QUEUE_CONNECTION=mongodb ``` ### GridFS によるファイル保存 MongoDB の GridFS を使ってファイルを保存できます。[Flysystem 用 GridFS アダプター](https://flysystem.thephpleague.com/docs/adapter/gridfs/) を利用します。詳細は [GridFS ドキュメント](https://www.mongodb.com/docs/drivers/php/laravel-mongodb/current/filesystems/) を参照してください。 ```shell theme={null} composer require league/flysystem-gridfs ``` `config/filesystems.php` にディスクを追加します。 ```php theme={null} 'disks' => [ 'gridfs' => [ 'driver' => 'gridfs', 'connection' => 'mongodb', 'database' => env('MONGODB_DATABASE', 'laravel_app'), ], ], ``` ```php theme={null} use Illuminate\Support\Facades\Storage; // ファイルのアップロード Storage::disk('gridfs')->put('file.pdf', $contents); // ファイルの取得 $contents = Storage::disk('gridfs')->get('file.pdf'); ``` ### ベクトル検索(Vector Search) [MongoDB Atlas Vector Search](https://www.mongodb.com/docs/drivers/php/laravel-mongodb/current/fundamentals/vector-search/) を使って、ベクトル埋め込みによる類似検索ができます。テキスト、画像、音声などのデータをベクトル化して保存しておき、クエリベクトルに最も近いドキュメントを取得する AI / 機械学習向けの検索機能です。 ベクトル検索は **MongoDB Atlas** 上でのみ利用可能です。ローカルの MongoDB Community Server や自己ホスト型環境では使用できません。事前に Atlas コンソールで Vector Search インデックスを作成しておく必要があります。 `vectorSearch` メソッドはクエリビルダーで利用できます。 ```php theme={null} use App\Models\Article; // クエリ文字列から埋め込みを生成(例:OpenAI API 等) $queryVector = getEmbedding('MongoDB とは何か'); // float[] 配列 $results = Article::query()->vectorSearch( index: 'vector_index', // Atlas Vector Search インデックス名 path: 'embedding', // ベクトルを格納したフィールド名 queryVector: $queryVector, limit: 10, // 返すドキュメント数 numCandidates: 100, // 候補数(ANN 検索時に指定) ); // 各ドキュメントには vectorSearchScore が付与される foreach ($results as $result) { echo $result->title . ': ' . $result->vectorSearchScore; } ``` ### 全文検索(Scout エンジン) `mongodb` Scout エンジンを使って [Laravel Scout](/jp/scout) の全文検索機能を MongoDB で利用できます。内部的に **MongoDB Atlas Search** を使用し、ファジー検索やワイルドカード検索に対応しています。詳細は [Scout エンジンドキュメント](https://www.mongodb.com/docs/drivers/php/laravel-mongodb/current/scout/) を参照してください。 MongoDB Scout エンジンも **MongoDB Atlas** 上でのみ利用可能です。Scout のインデックスコレクションはモデルのコレクションとは別のコレクションを使う必要があります。`config/scout.php` の `prefix` 設定(デフォルトはアプリ名)が自動的に適用されます。 `.env` で Scout ドライバを `mongodb` に設定します。 ```ini theme={null} SCOUT_DRIVER=mongodb ``` モデルに `Searchable` トレイトを追加します。 ```php theme={null} use Laravel\Scout\Searchable; use MongoDB\Laravel\Eloquent\Model; class Article extends Model { use Searchable; // 検索インデックスに含めるフィールドを指定 public function toSearchableArray(): array { return [ 'title' => $this->title, 'body' => $this->body, 'tags' => $this->tags, ]; } } ``` Scout インデックスを作成し、既存データをインポートします。 ```shell theme={null} php artisan scout:index articles php artisan scout:import "App\Models\Article" ``` 標準の Scout API で検索できます。 ```php theme={null} // 全文検索 $results = Article::search('MongoDB Laravel')->get(); // フィルター付き検索 $results = Article::search('クエリビルダー') ->where('status', 'published') ->paginate(15); ``` ## MySQL との併用 Laravel では MySQL と MongoDB を同時に使用できます。モデルごとに `$connection` プロパティで接続を指定します。 ```mermaid theme={null} flowchart TD A["Laravel アプリケーション"] --> B["UserController"] A --> C["ArticleController"] B --> D["User モデル\n$connection = 'mysql'"] C --> E["Article モデル\n$connection = 'mongodb'"] D --> F["MySQL"] E --> G["MongoDB"] ``` ```php theme={null} // MySQL を使う通常の Eloquent モデル class User extends \Illuminate\Database\Eloquent\Model { protected $connection = 'mysql'; } // MongoDB を使うモデル class Article extends \MongoDB\Laravel\Eloquent\Model { protected $connection = 'mongodb'; } ``` MySQL モデルと MongoDB モデルの間でリレーションを持たせる場合は、[ハイブリッドリレーション](https://www.mongodb.com/docs/drivers/php/laravel-mongodb/current/eloquent-models/relationships/) を参照してください。 ## まとめ 1. `pecl install mongodb` で PHP 拡張をインストール 2. MongoDB サーバーを起動(ローカルまたは Docker、または Atlas) 3. `composer require mongodb/laravel-mongodb` でパッケージをインストール 4. `.env` に `MONGODB_URI` と `MONGODB_DATABASE` を設定 5. `config/database.php` に `mongodb` 接続を追加 | 特性 | MongoDB | MySQL | | - | - | - | | データモデル | ドキュメント(JSON/BSON) | テーブル(行・列) | | スキーマ | スキーマレス(柔軟) | 固定スキーマ | | スケーリング | 水平スケーリングが容易 | 垂直スケーリングが主流 | | トランザクション | マルチドキュメント対応(v4+) | 完全な ACID 対応 | | 適した用途 | ログ・分析・IoT・柔軟な構造のデータ | 構造化データ・複雑なリレーション | | 機能 | 設定箇所 | | - | - | | Eloquent モデル | `MongoDB\Laravel\Eloquent\Model` を継承 | | クエリビルダー | `DB::connection('mongodb')->collection(...)` | | キャッシュ | `config/cache.php` + `CACHE_STORE=mongodb` | | キュー | `config/queue.php` + `QUEUE_CONNECTION=mongodb` | | ファイル保存 | `config/filesystems.php` + GridFS アダプター | | ベクトル検索 | `Model::query()->vectorSearch(...)` (Atlas 必須) | | 全文検索 | `SCOUT_DRIVER=mongodb` + `Searchable` トレイト (Atlas 必須) | ## 次のステップ Eloquent、クエリビルダー、リレーションなど全機能の詳細リファレンス MongoDB と Laravel の基本的な使い方を素早く学ぶ Laravel のデータベース接続設定の基本 Eloquent ORM の基本的な使い方 # コアパッケージと独自ドライバー - Feedable Source: https://kawax.biz/jp/packages/feedable/core feedable-core パッケージのコアヘルパーと独自ドライバーの作り方 ## Feedable Core の概要 `feedable-core` は Feedable のコアヘルパーと内蔵ドライバーを分離した Composer パッケージです。\ 新規 Laravel プロジェクトへ導入すると、スターターキットと同等の基盤を再利用できます。 * パッケージ: [invokable/feedable-core](https://github.com/invokable/feedable-core) * スターターキット: [invokable/feedable](https://github.com/invokable/feedable) ## Response クラス `Rss2Response` と `JsonFeedResponse` で出力フォーマットを固定できます。\ ユーザーがフォーマットを選ぶ構成では `ResponseFactory` を使います。 ```php theme={null} use Revolution\Feedable\Core\Response\Rss2Response; return new Rss2Response( title: $title, items: $items, ); ``` ```php theme={null} use Revolution\Feedable\Core\Enums\Format; use Revolution\Feedable\Core\Response\ResponseFactory; Route::get('feed.{format?}', function (Format $format = Format::RSS) use ($title, $items) { return ResponseFactory::format($format)->make( title: $title, items: $items, ); }); ``` ## FeedItem / Author `FeedItem` は RSS/JSON Feed で共通化されたアイテムオブジェクトです。\ `Author` は `authors` フィールド用のオブジェクトで、`Author::make()` を使って作成します。 ```php theme={null} use Revolution\Feedable\Core\Elements\Author; use Revolution\Feedable\Core\Elements\FeedItem; $item = new FeedItem( id: $url, url: $url, title: $title, summary: $summary, authors: [Author::make(name: $authorName)->toArray()], ); ``` ## Support ヘルパー ### AbsoluteUri::resolve() 相対パスを絶対パスに変換。 ```php theme={null} use Revolution\Feedable\Core\Support\AbsoluteUri; $absolute = AbsoluteUri::resolve('https://example.com/', '/images/sample.jpg'); ``` ### RSS::filterLinks() ```php theme={null} use Revolution\Feedable\Core\Support\RSS; $xml = RSS::filterLinks($rss, $links); ``` ### RSS::each() ```php theme={null} use DOMElement; use Revolution\Feedable\Core\Support\RSS; $xml = RSS::each($rss, function (DOMElement $item) { $title = $item->getElementsByTagName('title')->item(0); if ($title && str_contains($title->textContent, 'NGワード')) { $item->parentNode?->removeChild($item); } }); ``` ## カスタムドライバーの作り方 ### 1) スターターキットへ直接追加する スターターキットは通常の Laravel アプリなので、`routes/web.php` にルート追加し、コントローラー(または invokable クラス)で実装できます。 ```php theme={null} use App\Http\Controllers\CustomFeedController; use Revolution\Feedable\Core\Enums\Format; Route::get('/custom/site.{format?}', CustomFeedController::class) ->whereIn('format', array_column(Format::cases(), 'value')); ``` ### 2) Composer パッケージとして追加する 複数プロジェクトで再利用する場合は、ServiceProvider でルートを登録する構成が扱いやすくなります。 ```php theme={null} use Illuminate\Support\ServiceProvider; use Illuminate\Support\Facades\Route; class CustomDriverServiceProvider extends ServiceProvider { public function boot(): void { Route::middleware('web')->group(__DIR__.'/../routes/web.php'); } } ``` ### 3) Driver::about() で情報を登録する(オプション) `Driver::about()` の登録は対応サイト一覧に表示するためのメタデータです。\ 未登録でもドライバー自体は動作します。 ## 参考: Laravel ブログドライバー 実装例として Laravel 公式ブログ向けドライバーを参照してください。 * [LaravelBlogDriver.php](https://github.com/invokable/feedable-core/blob/main/src/Drivers/Laravel/LaravelBlogDriver.php) # 対応サイトリスト - Feedable Source: https://kawax.biz/jp/packages/feedable/drivers Feedableが対応しているサイトの一覧 ## 内蔵ドライバー一覧 `feedable-core` の `src/Drivers/` には以下のドライバーが含まれます。 * Asahi * ComicDays * Famitsu * Fate * JsonFeed * JumpPlus * Laravel * MagazinePocket * Mirror * Nicovideo * Nintendo * Note * Yomiuri ## JsonFeed ドライバー `JsonFeed` は特殊ドライバーです。\ 既存の RSS/Atom フィード URL を受け取り、JSON Feed 形式に変換して返します。 * 例: `/jsonfeed?url=https://example.com/feed.xml` * 件数制限: `limit` クエリ(例: `&limit=20`) ## サンプルサイトのURLパターン [公開サンプル](https://feedable-rss.vercel.app/) で確認できる代表的なパターンです。 * 朝日新聞: `/asahi/news.{format}` * コミックDAYSオリジナル: `/comic-days/original.{format}` * ファミ通: `/famitsu/category/{category}.{format}` * FGO: `/fgo/{category}.{format}` * Laravel Blog: `/laravel/blog.{format}` * マガポケ: `/shonenmagazine/pocket.{format}` * ニコニコ静画: `/nicovideo/manga/{category}.{format}` * 任天堂ダイレクト: `/nintendo/direct.{format}` * 任天堂IR: `/nintendo/ir/news.{format}` * note注目記事: `/note/index.{format}` * 少年ジャンプ+: `/shonenjumpplus/daily.{format}` * 読売新聞: `/yomiuri/news.{format}` * JSON Feed 変換: `/jsonfeed?url={encoded-feed-url}` `{format}` は主に `rss` または `json` を指定します。 ## 対応サイト追加のリクエスト 追加で対応して欲しいサイトがあれば [GitHub](https://github.com/invokable/feedable/blob/main/README_jp.md) に記載の方法でリクエストを送ってください。 # Feedable Source: https://kawax.biz/jp/packages/feedable/index RSSフィードを提供していないサイトからRSSを生成するLaravelスターターキット ## Feedableとは Feedable は、RSS を提供していないサイトから RSS/JSON Feed を生成する Laravel 製のサービスです。\ [RSSHub](https://github.com/DIYgod/RSSHub) の考え方を参考にしています。 ## スターターキットとコアパッケージ * スターターキット: [invokable/feedable](https://github.com/invokable/feedable) * すぐにデプロイして使えるアプリケーション本体です。 * コアパッケージ: [invokable/feedable-core](https://github.com/invokable/feedable-core) * ドライバーとヘルパーを切り出した Composer パッケージです。 スターターキットは `feedable-core` を利用しており、独自ドライバーを追加して運用できます。 ## サンプルサイト * 公開サンプル: [https://feedable-rss.vercel.app/](https://feedable-rss.vercel.app/) ## デプロイ フォークして Vercel にデプロイする構成が推奨されています。無料プランでも動作します。 [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Finvokable%2Ffeedable\&env=APP_NAME,APP_KEY\&envDefaults=%7B%22APP_NAME%22%3A%22Feedable%22%7D\&envDescription=APP_KEY%20can%20be%20generated%20using%20the%20artisan%20command.\&envLink=https%3A%2F%2Fgithub.com%2Finvokable%2Flaravel-vercel-installer%3Ftab%3Dreadme-ov-file%23env\&demo-title=Feedable\&demo-url=https%3A%2F%2Ffeedable-rss.vercel.app%2F\&skippable-integrations=1) Playwright が必要な特殊なドライバーを使う場合は、Laravel Forge など通常のサーバー環境での運用を検討してください。 ## 利用規約 * 個人による私的利用の範囲でのみ使用してください。 * RSS リーダーで読む用途を想定しています。 ## GitHubリポジトリ * [invokable/feedable](https://github.com/invokable/feedable) * [invokable/feedable-core](https://github.com/invokable/feedable-core) # Laravel Console Starter Source: https://kawax.biz/jp/packages/laravel-console-starter/index artisanコマンドをメインにしたLaravelスターターキット。GitHub Actionsでスケジュール実行し、通知でアウトプットを送信します。 ## 概要 [laravel-console-starter](https://github.com/invokable/laravel-console-starter) は、カスタムartisanコマンドを中心とするLaravelアプリケーション開発を加速するスターターキットです。 スタンドアロンのCLIツールを作るのではなく、Laravelフレームワークのエコシステム全体(依存性注入・通知・スケジューリング・テスト)を活用したコンソールアプリケーションを素早く構築できます。GitHub Actionsでスケジュール実行し、結果をメールやSlackなどの通知で送信するというワークフローが基本的なユースケースです。 ## 要件 * PHP `^8.3` * Laravel Framework `^13.0` * Laravel Installer `^5.24` ## インストール ```bash theme={null} laravel new --using=revolution/laravel-console-starter --no-interaction my-app ``` インストール後の主なディレクトリ構造: ``` my-app/ ├── app/Console/Commands/ # カスタムartisanコマンドを配置 ├── .github/workflows/ │ └── cron.yml # GitHub Actionsスケジュール例 ├── config/ # アプリケーション設定 └── routes/console.php # コマンド登録 ``` ## 主な特徴 Web用のオーバーヘッドを排除し、artisanコマンド開発に集中できる構成です。 サーバーのcronジョブ不要。`.github/workflows/cron.yml` の設定例が最初から含まれています。 通知・HTTP クライアント・テスト・DI コンテナなど、Laravelの豊富な機能をそのまま使えます。 ## クイックスタート ### コマンドを作成する ```bash theme={null} php artisan make:command Hello --command=hello ``` `app/Console/Commands/Hello.php` が生成されます。`handle()` メソッドにロジックを実装し、コマンドを実行します。 ```bash theme={null} php artisan hello ``` ### GitHub Actionsでスケジュール実行する `.github/workflows/cron.yml` を編集して、実行するコマンドとスケジュールを設定します。 ```yaml theme={null} on: schedule: - cron: '0 0 * * *' # 毎日UTC午前0時 jobs: cron: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: shivammathur/setup-php@v2 with: php-version: 8.5 - run: composer install --no-dev -q - run: cp .env.example .env - run: php artisan key:generate - run: php artisan your:command ``` ### 通知を送信する コマンドの実行結果を通知として送るには、`make:notification` でクラスを作成し、`Notification` ファサードで送信します。 ```bash theme={null} php artisan make:notification TaskCompleted ``` ```php theme={null} Notification::route('mail', 'admin@example.com') ->notify(new TaskCompleted('処理が完了しました')); ``` 通知チャネル(メール・Slack等)の設定が必要な場合は、`php artisan config:publish mail` や `php artisan config:publish services` で設定ファイルを公開してください。 ## アプリケーションアイデア このスターターキットで構築できるアプリケーションのアイデアです。 * ウェブサイト死活監視とSlackアラート * サーバーリソース使用状況レポートをメール送信 * SSL証明書の有効期限確認と通知 * APIレスポンスタイム監視とアラート * 競合他社の価格変動追跡とDiscord通知 * Google AdSense収益をメールで送信 * AWSコストをDiscordに通知 * 日次の暗号資産ポートフォリオ更新をDiscordへ * 株価アラートをSlackチャンネルへ * 請求書の支払期限リマインダー * 月次経費レポートの生成と配信 * 古いログファイルのクリーンアップとストレージレポート * 異なるAPI間のデータ同期と結果通知 * CSVデータのインポートと処理結果通知 * 定期的なデータエクスポートとクラウドストレージへのアップロード * ウェブサイトのリンク切れ確認とレポート * SEOキーワードランキング監視と変動通知 * ブログ投稿のパフォーマンス指標週次レポート * RSSフィードのコンテンツ集計と通知 * GitHubリポジトリの依存関係セキュリティアラート * コードベースの静的解析レポート * デプロイ後のアプリケーションヘルスチェック * APIドキュメントの変更検知と通知 * [laravel-copilot-sdk](https://github.com/invokable/laravel-copilot-sdk) を使ったAIエージェントツール * [laravel/mcp](https://github.com/laravel/mcp) パッケージを使ったローカルMCPサーバー * ドメイン有効期限の監視とアラート * Google Trendsキーワード監視と通知 * パーソナルな支出カテゴリーのインサイトレポート ## ドキュメント インストールからコマンド作成、GitHub Actionsでのスケジューリング、通知送信、実践的なアプリ例まで。 ## 参考リンク * GitHub: [invokable/laravel-console-starter](https://github.com/invokable/laravel-console-starter) * DeepWiki: [invokable/laravel-console-starter](https://deepwiki.com/invokable/laravel-console-starter) # チュートリアル - Laravel Console Starter Source: https://kawax.biz/jp/packages/laravel-console-starter/tutorial Laravel Console Starterを使ったコンソールアプリの構築手順。コマンド作成、GitHub Actionsスケジューリング、通知送信、実践的なアプリ例を学びます。 ## はじめに このチュートリアルでは、`revolution/laravel-console-starter` を使ってカスタムartisanコマンドを持つLaravelアプリケーションを構築します。 このスターターキットを使うメリット: * **迅速なセットアップ** — 複雑な設定なしにコンソールアプリ開発をすぐに始められます * **Laravelエコシステム** — タスクスケジューリング・データベース・通知などLaravelの豊富な機能を利用できます * **Artisanコマンド** — Laravelの強力なArtisanシステムで独自コマンドを簡単に作成・管理できます * **テスト容易性** — Laravelのテストフレームワークでコマンドのテストを記述できます ## 前提条件 以下のソフトウェアをインストールしてください。 * PHP `^8.3` * Composer([https://getcomposer.org/](https://getcomposer.org/)) * Laravel Installer(`composer global require laravel/installer`) ## プロジェクトのセットアップ ターミナルで以下のコマンドを実行します。 ```bash theme={null} laravel new my-app --using=revolution/laravel-console-starter --no-interaction ``` `my-app` ディレクトリが作成され、コンソールアプリの基本構造がセットアップされます。 インストール時に以下が自動実行されます。 * `.env.example` から `.env` ファイルの生成 * `php artisan key:generate` によるアプリケーションキーの設定 主なディレクトリ構造: ``` my-app/ ├── app/Console/Commands/ # カスタムコマンドを配置 ├── .github/workflows/ │ └── cron.yml # GitHub Actionsスケジュール例 ├── config/ # アプリケーション設定 └── routes/console.php # コマンド登録 ``` デフォルトでは、メール送信はログ出力に設定されています(`MAIL_MAILER=log`)。 実際のメール送信が必要な場合は、`.env` ファイルでMailgunやPostmark、SESなどのサービスを設定してください。 ## コンソールコマンドの作成 `make:command` Artisanコマンドで新しいコマンドを作成します。 ```bash theme={null} php artisan make:command YourCommandName --command=your:command ``` * `YourCommandName` — 作成するコマンドのクラス名(例:`SendDailyReport`) * `your:command` — コマンドの呼び出し名(例:`report:send-daily`) `app/Console/Commands/YourCommandName.php` が生成されます。 生成されたファイルを編集してロジックを実装します。以下はHello Worldコマンドの例です。 ```php theme={null} info('Hello, World message has been logged.'); return Command::SUCCESS; } } ``` `$signature` でコマンドの呼び出し名を定義し、`handle()` メソッドにロジックを記述します。 ```bash theme={null} php artisan hello:world ``` 実行後、`storage/logs/laravel.log` とコンソールにメッセージが出力されます。 ## GitHub Actionsでのタスクスケジューリング GitHub Actionsを使えば、サーバーのcronジョブなしにコマンドを定期実行できます。 スターターキットには `.github/workflows/cron.yml` が事前設定済みで含まれています。 ```yaml theme={null} name: cron on: schedule: - cron: '0 0 * * *' # 毎日UTC午前0時 jobs: cron: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: shivammathur/setup-php@v2 with: php-version: 8.5 coverage: none - run: composer install --no-dev -q - run: cp .env.example .env - run: php artisan key:generate - run: php artisan inspire ``` `php artisan inspire` を自分のコマンドに置き換えます。 ```yaml theme={null} - name: Run Command run: php artisan your:command ``` 複数のコマンドを実行する場合: ```yaml theme={null} - name: Run Commands run: | php artisan first:command php artisan second:command ``` `cron` 式を変更して実行頻度を設定します。 | cron式 | 実行タイミング | | - | - | | `0 0 * * *` | 毎日UTC午前0時 | | `0 */6 * * *` | 6時間ごと | | `0 0 * * 1` | 毎週月曜日の午前0時 | APIキーやパスワードなどの機密情報はGitHubリポジトリシークレットを使います。 GitHubリポジトリの **Settings > Secrets and variables > Actions** でシークレットを追加し、ワークフローで参照します。 ```yaml theme={null} - name: Run Command with Secrets run: php artisan your:command env: API_KEY: ${{ secrets.API_KEY }} DB_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} ``` ## 通知機能 コマンドの実行結果やエラーをメール・Slackなどで通知できます。 ```bash theme={null} php artisan make:notification TaskCompleted ``` `app/Notifications/TaskCompleted.php` が生成されます。 必要に応じて設定ファイルを公開します。 ```bash theme={null} php artisan config:publish mail php artisan config:publish services ``` ```php theme={null} info('データを処理中...'); // 処理ロジック... Notification::route('mail', 'admin@example.com') ->notify(new TaskCompleted('データ処理が正常に完了しました')); return Command::SUCCESS; } } ``` 詳細は[Laravel通知ドキュメント](https://laravel.com/docs/notifications)を参照してください。 ## 実践的なアプリケーション例 ### 例1: ウェブサイト死活監視とSlackアラート ウェブサイトがオンラインかどうかを確認し、問題が検出された場合にSlackへアラートを送信するコマンドです。 ```bash theme={null} composer require laravel/slack-notification-channel ``` ```bash theme={null} php artisan make:command MonitorWebsites --command=monitor:websites php artisan make:notification WebsiteDown ``` ```bash theme={null} php artisan config:publish services ``` `.env` にSlack webhook URLを追加します。 ``` SLACK_WEBHOOK_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL ``` `config/services.php` を更新します。 ```php theme={null} 'slack' => [ 'webhook_url' => env('SLACK_WEBHOOK_URL'), ], ``` `app/Notifications/WebsiteDown.php` を編集します。 ```php theme={null} error() ->content('ウェブサイトダウンアラート!') ->attachment(function ($attachment) { $attachment->title($this->website) ->content("エラー: {$this->error}") ->timestamp(now()); }); } } ``` `app/Console/Commands/MonitorWebsites.php` を編集します。 ```php theme={null} option('timeout'); $webhookUrl = config('services.slack.webhook_url'); $hasErrors = false; foreach ($this->websites as $website) { try { $response = Http::timeout($timeout)->get($website); if ($response->successful()) { $this->info("{$website}: オンライン ✓"); } else { $error = "HTTPステータス: " . $response->status(); $this->error("{$website}: ダウン ({$error})"); Notification::route('slack', $webhookUrl) ->notify(new WebsiteDown($website, $error)); $hasErrors = true; } } catch (\Exception $e) { $this->error("{$website}: エラー ({$e->getMessage()})"); Log::error("{$website} の確認に失敗", ['error' => $e->getMessage()]); Notification::route('slack', $webhookUrl) ->notify(new WebsiteDown($website, $e->getMessage())); $hasErrors = true; } } return $hasErrors ? Command::FAILURE : Command::SUCCESS; } } ``` `.github/workflows/cron.yml` を更新します。 ```yaml theme={null} name: Website Monitoring on: schedule: - cron: '*/15 * * * *' # 15分ごとに実行 jobs: monitor: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: shivammathur/setup-php@v2 with: php-version: 8.5 - run: composer install --no-dev -q - run: cp .env.example .env - run: php artisan key:generate - name: Monitor Websites run: php artisan monitor:websites env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} ``` ### 例2: 暗号資産ポートフォリオ更新とDiscord通知 CoinGecko APIから暗号資産の価格を取得し、Discordウェブフックで更新を送信するコマンドです。 ```bash theme={null} composer require revolution/laravel-notification-discord-webhook ``` ```bash theme={null} php artisan make:command UpdateCryptoPortfolio --command=crypto:portfolio php artisan make:notification CryptoPortfolioUpdate ``` `.env` にDiscord webhook URLを追加します。 ``` DISCORD_WEBHOOK=https://discord.com/api/webhooks/YOUR/WEBHOOK ``` `config/services.php` を更新します。 ```php theme={null} 'discord' => [ 'webhook' => env('DISCORD_WEBHOOK'), ], ``` 詳細な実装については[GitHubリポジトリのチュートリアル](https://github.com/invokable/laravel-console-starter/blob/main/docs/tutorial_ja.md)を参照してください。 ```bash theme={null} php artisan crypto:portfolio ``` ### 例3: ウェブサイトコンテンツのスクレイピングとメール通知 ウェブサイトからコンテンツをスクレイピングし、結果をメールで送信するコマンドです。 ```bash theme={null} php artisan make:command WebScraper --command=scrape:website php artisan make:notification ScrapingCompleted ``` ```bash theme={null} php artisan config:publish mail ``` `.env` ファイルにメール設定を追加します。 ``` MAIL_MAILER=smtp MAIL_HOST=smtp.mailtrap.io MAIL_PORT=2525 MAIL_USERNAME=your_username MAIL_PASSWORD=your_password MAIL_ENCRYPTION=tls MAIL_FROM_ADDRESS=your-app@example.com MAIL_FROM_NAME="${APP_NAME}" ``` ```php theme={null} protected $signature = 'scrape:website {--url=https://example.com} {--email=admin@example.com}'; public function handle(): int { $url = $this->option('url'); $email = $this->option('email'); $response = Http::timeout(30)->get($url); // コンテンツを処理して通知を送信... Notification::route('mail', $email) ->notify(new ScrapingCompleted(true, $url, $title, $content)); return Command::SUCCESS; } ``` ```bash theme={null} php artisan scrape:website --email=admin@example.com ``` ## 次のステップ これで `revolution/laravel-console-starter` を使ったLaravelコンソールアプリケーションの基本を学びました。 さらに深く学ぶために: * **Laravelドキュメントを探索する** — [Laravel公式ドキュメント](https://laravel.com/docs)でコンソールアプリを強化できる機能を確認しましょう * **テストを実装する** — Laravelのテストフレームワークでコマンドのテストを作成し、信頼性を確保しましょう * **パッケージ開発を検討する** — 複数のプロジェクトで同様のコマンドを作成するなら、再利用可能なLaravelパッケージとして公開することを検討してください * **最新情報を把握する** — [Laravel News](https://laravel-news.com/) や公式ブログをフォローしてベストプラクティスを常に把握しましょう # Laravel Fetch Metadata Source: https://kawax.biz/jp/packages/laravel-fetch-metadata Sec-Fetch-* ヘッダーを使ったセキュリティミドルウェアで、LaravelアプリをCSRF攻撃やクロスサイトリクエストから保護します。 ## 概要 [revolution/laravel-fetch-metadata](https://github.com/invokable/laravel-fetch-metadata) は、ブラウザが送信する `Sec-Fetch-*` HTTPヘッダーを検証するセキュリティミドルウェアパッケージです。リクエストの発生元・モード・宛先・ユーザー操作の有無に基づいて、許可するリクエストを制御できます。 ブラウザに組み込まれたセキュリティ機能を活用することで、正規ユーザーのエクスペリエンスを損なわずに、不正なオリジンからの悪意あるリクエストを防止できます。 Laravel 13では、CSRFプロテクションのOrigin検証に `Sec-Fetch-Site` ヘッダーが使われるようになりました。詳細は[CSRFプロテクション](/jp/csrf)を参照してください。 詳細なFetch Metadataの仕様については、[MDN ドキュメント](https://developer.mozilla.org/ja/docs/Glossary/Fetch_metadata_request_header)を参照してください。 ## インストール ```bash theme={null} composer require revolution/laravel-fetch-metadata ``` ### ミドルウェアエイリアスの登録 `bootstrap/app.php` にミドルウェアのエイリアスを登録します。 ```php theme={null} use Illuminate\Foundation\Configuration\Middleware; use Revolution\FetchMetadata\Middleware\SecFetchSite; use Revolution\FetchMetadata\Middleware\SecFetchMode; use Revolution\FetchMetadata\Middleware\SecFetchDest; use Revolution\FetchMetadata\Middleware\SecFetchUser; ->withMiddleware(function (Middleware $middleware) { $middleware->alias([ 'sec-fetch-site' => SecFetchSite::class, 'sec-fetch-mode' => SecFetchMode::class, 'sec-fetch-dest' => SecFetchDest::class, 'sec-fetch-user' => SecFetchUser::class, ]); }) ``` 一部のミドルウェアだけを使うこともできます。エイリアス名は任意に変更できます。 ## ミドルウェアの説明 ### SecFetchSite `Sec-Fetch-Site` ヘッダーは、リクエストの発生元とターゲットのオリジンの関係を示します。デフォルトでは `same-origin` と `none`(直接アクセス)のみを許可します。 | 値 | 説明 | | - | - | | `same-origin` | 同一オリジンからのリクエスト | | `same-site` | サブドメインなど同一サイトからのリクエスト | | `cross-site` | 異なるサイトからのリクエスト | | `none` | ユーザーが直接URLを入力するなど、ナビゲーション起点のリクエスト | 詳細は[MDN ドキュメント](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Sec-Fetch-Site)を参照してください。 ### SecFetchMode `Sec-Fetch-Mode` ヘッダーは、リクエストのモードを示します。デフォルトでは `navigate` と `cors` を許可します。 | 値 | 説明 | | - | - | | `navigate` | リンククリックやフォーム送信などのナビゲーションリクエスト | | `cors` | CORSリクエスト | | `no-cors` | no-corsリクエスト | | `same-origin` | 同一オリジンリクエスト | | `websocket` | WebSocket接続リクエスト | 詳細は[MDN ドキュメント](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Sec-Fetch-Mode)を参照してください。 ### SecFetchDest `Sec-Fetch-Dest` ヘッダーは、リクエストの宛先リソースタイプを示します。 詳細は[MDN ドキュメント](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Sec-Fetch-Dest)を参照してください。 ### SecFetchUser `Sec-Fetch-User` ヘッダーは、リクエストがユーザーの操作によって開始されたかどうかを示します。値は `?1`(ユーザー操作あり)のみです。 `SecFetchUser` ミドルウェアを使用すると、検索エンジンのクローラーやAIエージェントもブロックされます。インデックスが必要な公開ページには使用しないでください。 ## ルーティングでの使用例 ### 基本的な使用 ```php theme={null} use Illuminate\Support\Facades\Route; use Illuminate\Http\Request; Route::post('user/update-password', function (Request $request) { // })->middleware('sec-fetch-site'); ``` ### パラメータで許可する値を指定する ```php theme={null} Route::post('user/update-password', function (Request $request) { // })->middleware('sec-fetch-site:cross-site'); ``` ### 複数のパラメータを指定する ```php theme={null} Route::post('user/update-password', function (Request $request) { // })->middleware('sec-fetch-site:same-origin,cross-site'); ``` ### エイリアスを使わない場合 ```php theme={null} use Revolution\FetchMetadata\Middleware\SecFetchSite; Route::post('user/update-password', function (Request $request) { // })->middleware(SecFetchSite::class.':same-origin,cross-site'); ``` ### 複数のミドルウェアを組み合わせる ```php theme={null} Route::post('user/update-profile', function (Request $request) { // })->middleware(['sec-fetch-site:same-origin', 'sec-fetch-mode:navigate']); ``` ## エラーハンドリング `Sec-Fetch-*` ヘッダーの値が不正な場合、`Symfony\Component\HttpKernel\Exception\BadRequestHttpException` がスローされます。 `bootstrap/app.php` でレスポンスをカスタマイズできます。 ```php theme={null} use Illuminate\Http\Request; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; ->withExceptions(function (Exceptions $exceptions) { $exceptions->render(function (BadRequestHttpException $e, Request $request) { if ($request->expectsJson()) { return response()->json([ 'message' => $e->getMessage(), ], 400); } }); }) ``` ## CSRFプロテクションとの関係 Laravel 13では、`PreventRequestForgery` ミドルウェアがCSRF保護の最初のステップとして `Sec-Fetch-Site` ヘッダーによるOrigin検証を行うようになりました。このパッケージはさらに細かい粒度でFetch Metadataヘッダーを活用し、アプリケーションのセキュリティを強化できます。 詳細は[CSRFプロテクション](/jp/csrf)を参照してください。 最新情報は [GitHub リポジトリ](https://github.com/invokable/laravel-fetch-metadata) を参照してください。 # Laravel FullFeed Source: https://kawax.biz/jp/packages/laravel-fullfeed フィードリーダー向けにWebページのメインコンテンツを抽出するLaravelパッケージ。LaravelのPipelineパターンを活用。 ## 概要 [revolution/laravel-fullfeed](https://github.com/invokable/laravel-fullfeed) は、フィードリーダー向けにWebページから本文を抽出するLaravelパッケージです。\ サイトごとのルール(JSON)を使って、記事ごとに必要なコンテンツを取り出します。 もともとはプライベートなフィードリーダーアプリで使われていた機能が切り出され、公開パッケージ化されています。 ## 要件 * PHP >= 8.4(`Dom\HTMLDocument` を利用) * Laravel >= 12.x ## インストール ```bash theme={null} composer require revolution/laravel-fullfeed ``` 開発版を使う場合: ```bash theme={null} composer require revolution/laravel-fullfeed:dev-main ``` ## 設定ファイルとサイト定義の公開 ```bash theme={null} php artisan vendor:publish --tag=fullfeed ``` このコマンドで、以下が作成されます。 * `config/fullfeed.php` * `resources/fullfeed` ## 定義ファイルの自動更新(composer post-update-cmd) `composer update` 時にサイト定義を自動で再公開したい場合は、`composer.json` に以下を追加します。 ```json theme={null} { "scripts": { "post-update-cmd": [ "@php artisan vendor:publish --tag=laravel-assets --ansi --force", "@php artisan vendor:publish --tag=fullfeed-site --ansi --force" ] } } ``` ## 基本的な使い方 ```php theme={null} use Revolution\Fullfeed\Facades\FullFeed; $html = FullFeed::get($url); ``` ## テスト `FullFeed::expects()` でFacadeをスタブできます。 ```php theme={null} use Revolution\Fullfeed\Facades\FullFeed; FullFeed::expects('get') ->with('https://example.com/article/1') ->andReturn('
    Main content
    '); ``` ## サイトルールファイル ### items\_all.json `items_all.json` は、LDRFullFeed (wedata) 形式のルールを取り込むためのベースです。\ Livedoor Reader は日本で有名なサービスでしたが、サービス自体は終了し、現在はルールデータが主に資産として残っています。FullFeed はこの歴史的なデータ形式を活用します。 ### plus.json `plus.json` は独自ルール追加用のサンプルです。 ```json theme={null} { "name": "note", "data": { "url": "^https://note\\.com/", "selector": "div[data-name=\"body\"]", "xpath": "//div[@data-name=\"body\"]", "enc": "UTF-8", "callable": "App\\FullFeed\\CustomExtractor" } } ``` ## ルール設定フィールド * `url`: 対象URLにマッチさせる正規表現 * `selector`: CSSセレクタ(`xpath` より優先) * `xpath`: XPath指定 * `enc`: 文字コード変換が必要な場合のエンコーディング指定 * `callable`: 前処理として実行する独自Extractorクラス(単体/配列) * `after_callable`: 後処理として最後に実行するExtractorクラス ## PipelineパターンとしてのExtractor実行順 FullFeed は Laravel の **Pipelineパターン** で、Extractor を順番に流します。 1. `callable` で指定したクラス 2. `XPathExtractor` 3. `SelectorExtractor` 4. `after_callable` で指定したクラス 単純なXPath/CSS抽出の前後に独自処理を差し込めるため、サイトごとの癖に対応しやすい設計です。 ## 組み込みExtractor ### RemoveElements 指定したセレクタの要素を削除します。 ```json theme={null} { "after_callable": ["Revolution\\Fullfeed\\Extractor\\RemoveElements"], "remove": ["svg", "button", "script"] } ``` ### ReplaceMatches 正規表現で一致した文字列を置換します(HTML文字列に対して処理)。 ```json theme={null} { "after_callable": ["Revolution\\Fullfeed\\Extractor\\ReplaceMatches"], "replace": [ { "pattern": "/ data-(h-)?index=\"[0-9]+\"/", "replace": "" } ] } ``` ### StripTags `strip_tags()` 相当でタグを除去します。 ```json theme={null} { "after_callable": ["Revolution\\Fullfeed\\Extractor\\StripTags:a,img"] } ``` ### Squish `Str::squish()` で余分な空白を削除します。 ```json theme={null} { "after_callable": ["Revolution\\Fullfeed\\Extractor\\Squish"] } ``` ## 独自ルールの追加 1. `resources/fullfeed` にJSONファイルを追加 2. `config/fullfeed.php` の `paths` に追加 `data.url` に最初にマッチしたルールが使われるため、追加ファイルは `paths` の先頭に置くのがおすすめです。 # OpenAI互換の音声合成を行う Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/openai互換/openai互換の音声合成を行う https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json post /v1/audio/speech OpenAI Audio Speech API互換のリクエストで音声合成を行い、WAV音声を返します。`voice`にはVOICEVOXのスタイルID、AI SDKの音声エイリアス、OpenAIのボイス名を指定できます。`speed`はVOICEVOXの`speedScale`に適用されます。`model`、`instructions`、`response_format`、`stream_format`は互換性のため受け付けますが、Laravel版では現在WAV出力固定です。 **Laravel実装状況**: ✅ コアで完全対応(フォールバック実装済み) # Add Preset Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/その他/add-preset https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json post /add_preset 新しいプリセットを追加します。 **Laravel実装状況**: ✅ NativePresetStoreでJSONファイルに永続化(フォールバック不要) # base64エンコードされた複数のwavデータを一つに結合する Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/その他/base64エンコードされた複数のwavデータを一つに結合する https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json post /connect_waves base64エンコードされたwavデータを一纏めにし、wavファイルで返します。 **Laravel実装状況**: ❌ コアに相当機能なし(フォールバックのみ) # Core Versions Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/その他/core-versions https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json get /core_versions 利用可能なコアのバージョン一覧を取得します。 **Laravel実装状況**: ✅ コアのVoicevoxCore::getVersion()で対応(フォールバック不要) # Delete Preset Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/その他/delete-preset https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json post /delete_preset 既存のプリセットを削除します。 **Laravel実装状況**: ✅ NativePresetStoreでJSONファイルに永続化(フォールバック不要) # Engine Manifest Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/その他/engine-manifest https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json get /engine_manifest エンジンマニフェストを取得します。 **Laravel実装状況**: ✅ Laravelパッケージ用のマニフェストを静的に返す(フォールバック不要) # Get Portal Page Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/その他/get-portal-page https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json get / ポータルページを返します。 **Laravel実装状況**: ❌ エンジン固有の機能(フォールバックのみ) # Get Presets Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/その他/get-presets https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json get /presets エンジンが保持しているプリセットの設定を返します。 **Laravel実装状況**: ✅ NativePresetStoreでJSONファイルに永続化(フォールバック不要) # Initialize Speaker Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/その他/initialize-speaker https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json post /initialize_speaker 指定されたスタイルを初期化します。 実行しなくても他のAPIは使用できますが、初回実行時に時間がかかることがあります。 **Laravel実装状況**: ❌ コアはvvmロードで初期化済み(フォールバックもなし) # Is Initialized Speaker Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/その他/is-initialized-speaker https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json get /is_initialized_speaker 指定されたスタイルが初期化されているかどうかを返します。 **Laravel実装状況**: ❌ コアはvvmロードで初期化済み(フォールバックもなし) # Singer Info Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/その他/singer-info https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json get /singer_info UUID で指定された歌えるキャラクターの情報を返します。 画像や音声はresource_formatで指定した形式で返されます。 **Laravel実装状況**: ✅ Laravelで完全対応(公式JSONと完全一致、フォールバック実装済み) # Singers Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/その他/singers https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json get /singers 歌えるキャラクターの情報の一覧を返します。 **Laravel実装状況**: ✅ コアで完全対応(公式JSONと完全一致、フォールバック実装済み) # Speaker Info Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/その他/speaker-info https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json get /speaker_info UUID で指定された喋れるキャラクターの情報を返します。 画像や音声はresource_formatで指定した形式で返されます。 **Laravel実装状況**: ✅ Laravelで完全対応(公式JSONと完全一致、フォールバック実装済み) # Speakers Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/その他/speakers https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json get /speakers 喋れるキャラクターの情報の一覧を返します。 **Laravel実装状況**: ✅ コアで完全対応(公式JSONと完全一致、フォールバック実装済み) # Supported Devices Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/その他/supported-devices https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json get /supported_devices 対応デバイスの一覧を取得します。 **Laravel実装状況**: ❌ エンジン固有の機能(フォールバックのみ) # Update Preset Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/その他/update-preset https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json post /update_preset 既存のプリセットを更新します。 **Laravel実装状況**: ✅ NativePresetStoreでJSONファイルに永続化(フォールバック不要) # Version Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/その他/version https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json get /version エンジンのバージョンを取得します。 **Laravel実装状況**: ✅ Laravelのみで対応(フォールバック不要) # テキストがAquesTalk 風記法に従っているか判定する Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/その他/テキストがaquestalk-風記法に従っているか判定する https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json post /validate_kana テキストがAquesTalk 風記法に従っているかどうかを判定します。 従っていない場合はエラーが返ります。 **Laravel実装状況**: ✅ Laravelのみで対応(フォールバック不要) # Add User Dict Word Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/ユーザー辞書/add-user-dict-word https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json post /user_dict_word ユーザー辞書に言葉を追加します。 **Laravel実装状況**: ✅ NativeUserDictでJSONファイルに永続化(フォールバック不要) # Delete User Dict Word Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/ユーザー辞書/delete-user-dict-word https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json delete /user_dict_word/{word_uuid} ユーザー辞書に登録されている言葉を削除します。 **Laravel実装状況**: ✅ NativeUserDictでJSONファイルに永続化(フォールバック不要) # Get User Dict Words Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/ユーザー辞書/get-user-dict-words https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json get /user_dict ユーザー辞書に登録されている単語の一覧を返します。 単語の表層形(surface)は正規化済みの物を返します。 **Laravel実装状況**: ✅ NativeUserDictでJSONファイルに永続化(フォールバック不要) # Import User Dict Words Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/ユーザー辞書/import-user-dict-words https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json post /import_user_dict 他のユーザー辞書をインポートします。 **Laravel実装状況**: ✅ NativeUserDictでJSONファイルに永続化(フォールバック不要) # Rewrite User Dict Word Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/ユーザー辞書/rewrite-user-dict-word https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json put /user_dict_word/{word_uuid} ユーザー辞書に登録されている言葉を更新します。 **Laravel実装状況**: ✅ NativeUserDictでJSONファイルに永続化(フォールバック不要) # Setting Get Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/設定/setting-get https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json get /setting 設定ページを返します。 **Laravel実装状況**: ❌ エンジン固有の機能(フォールバックのみ) # Setting Post Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/設定/setting-post https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json post /setting 設定を更新します。 **Laravel実装状況**: ❌ エンジン固有の機能(フォールバックのみ) # Downloadable Libraries Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/音声ライブラリ管理/downloadable-libraries https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json get /downloadable_libraries ダウンロード可能な音声ライブラリの情報を返します。 **Laravel実装状況**: ❌ エンジン固有の機能(フォールバックもなし) # Install Library Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/音声ライブラリ管理/install-library https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json post /install_library/{library_uuid} 音声ライブラリをインストールします。 音声ライブラリのZIPファイルをリクエストボディとして送信してください。 **Laravel実装状況**: ❌ エンジン固有の機能(フォールバックもなし) # Installed Libraries Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/音声ライブラリ管理/installed-libraries https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json get /installed_libraries インストールした音声ライブラリの情報を返します。 **Laravel実装状況**: ❌ エンジン固有の機能(フォールバックもなし) # Uninstall Library Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/音声ライブラリ管理/uninstall-library https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json post /uninstall_library/{library_uuid} 音声ライブラリをアンインストールします。 **Laravel実装状況**: ❌ エンジン固有の機能(フォールバックもなし) # 2種類のスタイルでモーフィングした音声を合成する Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/音声合成/2種類のスタイルでモーフィングした音声を合成する https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json post /synthesis_morphing 指定された2種類のスタイルで音声を合成、指定した割合でモーフィングした音声を得ます。 モーフィングの割合は`morph_rate`で指定でき、0.0でベースのスタイル、1.0でターゲットのスタイルに近づきます。 **Laravel実装状況**: ❌ コアに相当機能なし(フォールバックのみ) # Frame Synthesis Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/音声合成/frame-synthesis https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json post /frame_synthesis 歌唱音声合成を行います。 **Laravel実装状況**: ✅ コアで完全対応(フォールバック実装済み) # 指定したスタイルに対してエンジン内のキャラクターがモーフィングが可能か判定する Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/音声合成/指定したスタイルに対してエンジン内のキャラクターがモーフィングが可能か判定する https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json post /morphable_targets 指定されたベーススタイルに対してエンジン内の各キャラクターがモーフィング機能を利用可能か返します。 モーフィングの許可/禁止は`/speakers`の`speaker.supported_features.synthesis_morphing`に記載されています。 プロパティが存在しない場合は、モーフィングが許可されているとみなします。 返り値のスタイルIDはstring型なので注意。 **Laravel実装状況**: ❌ コアに相当機能なし(フォールバックのみ) # 複数まとめて音声合成する Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/音声合成/複数まとめて音声合成する https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json post /multi_synthesis **Laravel実装状況**: ✅ コアで対応(フォールバックあり) # 音声合成する Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/音声合成/音声合成する https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json post /synthesis **Laravel実装状況**: ✅ コアで完全対応(フォールバック実装済み) # 音声合成する(キャンセル可能) Source: https://kawax.biz/jp/packages/laravel-voicevox/engine-api/音声合成/音声合成する(キャンセル可能) https://raw.githubusercontent.com/invokable/laravel-voicevox/refs/heads/main/docs/jp/laravel-openapi.json post /cancellable_synthesis **Laravel実装状況**: ❌ コアに相当機能なし(フォールバックのみ、公式でもデフォルト無効) # ページネーション Source: https://kawax.biz/jp/pagination Laravelのページネーション機能を解説します。paginate()・simplePaginate()・cursorPaginate()の違い、Bladeでの表示、APIレスポンス、URLのカスタマイズまでカバーします。 ## ページネーションとは Laravelのページネーションは、クエリビルダーおよびEloquent ORMと統合されており、設定なしで使い始めることができます。現在のページはHTTPリクエストの `page` クエリパラメーターから自動的に取得され、生成されたリンクにも自動で付加されます。 デフォルトのHTMLはTailwind CSSに対応しており、Bootstrap CSSも選択できます。 ## 3種類のページネーション | メソッド | 返り値 | 特徴 | | - | - | - | | `paginate()` | `LengthAwarePaginator` | 総件数を取得。ページ番号リンクを生成 | | `simplePaginate()` | `Paginator` | 総件数を取得しない。「前へ」「次へ」のみ | | `cursorPaginate()` | `CursorPaginator` | カーソルベース。大量データに最適 | ## 基本的な使い方 ### クエリビルダーのページネーション ```php theme={null} use Illuminate\Support\Facades\DB; // 1ページあたり15件で取得 $users = DB::table('users')->paginate(15); ``` ### Eloquentのページネーション ```php theme={null} use App\Models\User; $users = User::paginate(15); // 条件付き $users = User::where('votes', '>', 100)->paginate(15); ``` ### simplePaginate 総件数のカウントクエリが不要な場合(「前へ」「次へ」リンクのみ表示)は `simplePaginate()` を使うと効率的です。 ```php theme={null} $users = DB::table('users')->simplePaginate(15); $users = User::where('active', true)->simplePaginate(15); ``` 「全部で何件中何件目」という表示が不要なら `simplePaginate()` を選びましょう。`paginate()` は `COUNT(*)` クエリを追加で実行するため、`simplePaginate()` の方が高速です。 ### cursorPaginate(カーソルページネーション) カーソルページネーションはOFFSET句の代わりにWHERE句を使うため、大量データに対して高いパフォーマンスを発揮します。無限スクロールのUIに特に適しています。 ```php theme={null} // orderBy が必須 $users = DB::table('users')->orderBy('id')->cursorPaginate(15); $users = User::where('active', true)->cursorPaginate(15); ``` 生成されるURLにはページ番号ではなくカーソル文字列が入ります。 ``` http://example.com/users?cursor=eyJpZCI6MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0 ``` カーソルページネーションを使うには `orderBy` が必須です。また、並び順のカラムはページネーション対象のテーブルに属している必要があります。 ### OFFSETとカーソルの比較 ```sql theme={null} -- paginate() / simplePaginate() — OFFSETを使う SELECT * FROM users ORDER BY id ASC LIMIT 15 OFFSET 15; -- cursorPaginate() — WHERE句を使う(インデックスが効く) SELECT * FROM users WHERE id > 15 ORDER BY id ASC LIMIT 15; ``` カーソルページネーションはインデックスが有効活用され、データが頻繁に追加・削除される場合もレコードの重複・欠落が起きにくいという利点があります。ただし、ページ番号リンクの生成はできず、「前へ」「次へ」のみです。 ## コントローラーでの実装 ```php theme={null} paginate(20); return view('users.index', compact('users')); } } ``` ## Bladeでのページネーションリンク表示 ```blade theme={null}
    @foreach ($users as $user)

    {{ $user->name }}

    @endforeach
    {{-- ページネーションリンクを出力(Tailwind CSS対応) --}} {{ $users->links() }} ``` `links()` メソッドが自動でページリンクのHTMLを生成します。現在のページの前後3ページ分のリンクが表示されます。 ### 表示するリンク数の調整 `onEachSide()` で現在のページの前後に表示するリンク数を変更できます。 ```blade theme={null} {{-- 現在ページの前後5ページ分を表示 --}} {{ $users->onEachSide(5)->links() }} ``` ## 1ページあたりの件数をリクエストから受け取る ```php theme={null} public function index(Request $request): View { $perPage = $request->integer('per_page', 15); $perPage = min(max($perPage, 1), 100); // 1〜100の範囲に制限 $users = User::paginate($perPage); return view('users.index', compact('users')); } ``` ## 1ページに複数のページネーターを表示する 同一画面に2つのページネーターを表示する場合、両方が `page` パラメーターを使うと競合します。第3引数でパラメーター名を変更します。 ```php theme={null} $users = User::paginate( perPage: 15, columns: ['*'], pageName: 'users' ); $posts = Post::paginate( perPage: 10, columns: ['*'], pageName: 'posts' ); ``` ## URLのカスタマイズ ### ベースURLの変更 ```php theme={null} $users = User::paginate(15); // /admin/users?page=N 形式のURLを生成 $users->withPath('/admin/users'); ``` ### クエリパラメーターの追加 ```php theme={null} // sort=votes を各ページリンクに追加 $users->appends(['sort' => 'votes']); // 現在のリクエストの全クエリパラメーターを引き継ぐ $users->withQueryString(); ``` ### ハッシュフラグメントの追加 ```php theme={null} // URLの末尾に #users を追加 $users->fragment('users'); ``` ## APIレスポンス(JSON出力) ページネーターをルートやコントローラーからそのまま返すと、自動的にJSONに変換されます。 ```php theme={null} use App\Models\User; Route::get('/api/users', function () { return User::paginate(15); }); ``` レスポンスのJSON形式: ```json theme={null} { "total": 50, "per_page": 15, "current_page": 1, "last_page": 4, "first_page_url": "http://example.com/api/users?page=1", "last_page_url": "http://example.com/api/users?page=4", "next_page_url": "http://example.com/api/users?page=2", "prev_page_url": null, "path": "http://example.com/api/users", "from": 1, "to": 15, "data": [ { "id": 1, "name": "山田太郎" }, { "id": 2, "name": "鈴木花子" } ] } ``` ### APIリソースとの組み合わせ `paginate()` の結果をAPIリソースコレクションでラップする場合は `UserResource::collection()` に渡します。 ```php theme={null} use App\Http\Resources\UserResource; use App\Models\User; Route::get('/api/users', function () { $users = User::paginate(15); return UserResource::collection($users); }); ``` `UserResource::collection()` にページネーターを渡すと、ページネーション情報がメタデータとして自動で付加されます。 `cursorPaginate()` のJSONにはページ番号ではなく `next_cursor` と `prev_cursor` が含まれます。APIクライアントはこれらの値を次のリクエストの `cursor` パラメーターとして使います。 ## カスタムページネーションビュー ### ビューファイルに直接指定 ```blade theme={null} {{-- カスタムビューでリンクを出力 --}} {{ $paginator->links('vendor.pagination.custom') }} {{-- データを追加で渡す --}} {{ $paginator->links('vendor.pagination.custom', ['theme' => 'dark']) }} ``` ### デフォルトビューをカスタムファイルに変更 まず公式のビューを公開してからカスタマイズします。 ```shell theme={null} php artisan vendor:publish --tag=laravel-pagination ``` `resources/views/vendor/pagination/` に以下のファイルが生成されます。 * `tailwind.blade.php` — デフォルト(Tailwind CSS用) * `bootstrap-5.blade.php` — Bootstrap 5用 * `simple-tailwind.blade.php` — simplePaginate用 * ... `tailwind.blade.php` を直接編集するか、新しいビューを作成して `AppServiceProvider` で指定します。 ```php theme={null} use Illuminate\Pagination\Paginator; public function boot(): void { Paginator::defaultView('vendor.pagination.custom'); Paginator::defaultSimpleView('vendor.pagination.simple-custom'); } ``` ### Bootstrap CSSを使う TailwindではなくBootstrapを使う場合は `AppServiceProvider` の `boot()` で指定します。 ```php theme={null} use Illuminate\Pagination\Paginator; public function boot(): void { Paginator::useBootstrapFive(); // Bootstrap 5 // Paginator::useBootstrapFour(); // Bootstrap 4 } ``` ## 手動でページネーターを作成する 配列などの既存データにページネーションを適用したい場合、ページネータークラスを直接インスタンス化します。 ```php theme={null} use Illuminate\Pagination\LengthAwarePaginator; $items = collect(range(1, 200))->map(fn ($i) => ['id' => $i, 'name' => "アイテム{$i}"]); $perPage = 15; $currentPage = request()->integer('page', 1); $paginator = new LengthAwarePaginator( items: $items->forPage($currentPage, $perPage), total: $items->count(), perPage: $perPage, currentPage: $currentPage, options: ['path' => request()->url()] ); ``` ## よく使うインスタンスメソッド ```php theme={null} $paginator = User::paginate(15); $paginator->currentPage(); // 現在のページ番号 $paginator->lastPage(); // 最後のページ番号(simplePaginate では使用不可) $paginator->total(); // 総件数(simplePaginate では使用不可) $paginator->perPage(); // 1ページあたりの件数 $paginator->count(); // 現在ページの件数 $paginator->firstItem(); // 現在ページの最初のアイテムの番号 $paginator->lastItem(); // 現在ページの最後のアイテムの番号 $paginator->hasPages(); // 複数ページが存在するか $paginator->hasMorePages(); // 次のページが存在するか $paginator->onFirstPage(); // 最初のページか $paginator->onLastPage(); // 最後のページか $paginator->nextPageUrl(); // 次のページURL $paginator->previousPageUrl(); // 前のページURL $paginator->url(3); // ページ3のURL $paginator->items(); // 現在ページのアイテム配列 ``` ## まとめ * **`paginate()`** — 総件数とページ番号リンクが必要な場合(一般的なリスト画面) * **`simplePaginate()`** — 「前へ」「次へ」リンクのみで十分な場合(高速) * **`cursorPaginate()`** — 大量データ・無限スクロール・頻繁な書き込みがある場合(最高パフォーマンス) ```blade theme={null} @foreach ($users as $user)

    {{ $user->name }}

    @endforeach {{ $users->links() }} ``` `paginate()` の結果をビューに渡し、`links()` でページリンクを出力するだけです。 現在のページは `page` クエリパラメーターから自動で検出されます。
    ページネーターをルートから直接返すと自動的にJSONに変換されます。 APIリソースと組み合わせるには `UserResource::collection($paginator)` を返します。 レスポンスには `data`(レコード配列)と各種メタ情報が含まれます。
    # パスワードリセット Source: https://kawax.biz/jp/passwords Laravelでパスワードリセット機能を実装する方法 ## はじめに パスワードリセットはWebアプリケーションに欠かせない認証フローです。スターターキットを使えばこの機能は自動的に構築されますが、APIのみのプロジェクトや独自UIを構築するプロジェクトでは手動実装が必要です。 **手動実装が必要なケース:** * API専用バックエンド(フロントエンドがSPA・モバイルアプリ) * スターターキットを使わずに認証UIを自前で構築する場合 * メール通知やリセットURLのデザインを完全にカスタマイズしたい場合 スターターキット(`laravel new`)を使ってプロジェクトを作成した場合、パスワードリセット機能はすでに実装済みです。このページはスターターキットなしで実装する方法を説明します。 パスワードリセットの全体フローは次のとおりです。 ```mermaid theme={null} flowchart TD A["ユーザー"] --> B["メールアドレス入力
    GET /forgot-password"] B --> C["リセットリンク送信
    POST /forgot-password"] C --> D["メール受信
    トークン付きURL"] D --> E["リセットフォーム表示
    GET /reset-password/{token}"] E --> F["パスワード変更
    POST /reset-password"] F --> G["ログイン画面へ
    リダイレクト"] ``` ## 設定 パスワードリセットの設定は `config/auth.php` の `passwords` キーで管理します。 ```php theme={null} // config/auth.php 'passwords' => [ 'users' => [ 'driver' => 'database', 'provider' => 'users', 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), 'expire' => 60, 'throttle' => 60, ], ], ``` * `driver` — データ保存方式(`database` または `cache`) * `expire` — トークンの有効期限(分)。デフォルトは60分 * `throttle` — 再送信を制限するまでの待機時間(秒) ## ドライバー ### database ドライバー デフォルトのドライバーです。パスワードリセットトークンをデータベースの `password_reset_tokens` テーブルに保存します。このテーブルはLaravelのデフォルトマイグレーション(`0001_01_01_000000_create_users_table.php`)に含まれています。 ```php theme={null} 'passwords' => [ 'users' => [ 'driver' => 'database', 'provider' => 'users', 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), 'expire' => 60, 'throttle' => 60, ], ], ``` ### cache ドライバー Laravel 11以降で利用可能な新しいオプションです。データベーステーブルが不要なため、シンプルな構成でパスワードリセットを実装できます。 `cache` ドライバーはキャッシュストアにトークンを保存します。`password_reset_tokens` テーブルのマイグレーションが不要になります。トークンはユーザーのメールアドレスをキーとして保存されるため、アプリ内の他の場所でメールアドレスをキャッシュキーとして使用しないよう注意してください。 ```php theme={null} 'passwords' => [ 'users' => [ 'driver' => 'cache', 'provider' => 'users', 'store' => 'passwords', // オプション: 専用のキャッシュストア 'expire' => 60, 'throttle' => 60, ], ], ``` `store` キーに専用のキャッシュストアを指定すると、`php artisan cache:clear` でリセットデータが消去されるのを防げます。指定する値は `config/cache.php` で設定されているストア名に対応させます。 ## モデルの準備 パスワードリセット機能を使うには、`App\Models\User` モデルに2つのトレイトが必要です。 ```php theme={null} Laravelのデフォルト `User` モデルにはすでにこれらのトレイトが含まれています。新規インストールの場合は追加不要です。 ## ルーティングの実装 パスワードリセットには4つのルートが必要です。 ### 1. リセットリンクのリクエストフォーム メールアドレスを入力するフォームを表示します。 ```php theme={null} // routes/web.php Route::get('/forgot-password', function () { return view('auth.forgot-password'); })->middleware('guest')->name('password.request'); ``` 対応するBladeビュー: ```blade theme={null} {{-- resources/views/auth/forgot-password.blade.php --}}
    @csrf
    @error('email') {{ $message }} @enderror
    @if (session('status'))
    {{ session('status') }}
    @endif
    ``` ### 2. リセットリンクの送信処理 フォームの送信を受け取り、`Password::sendResetLink()` でリセットメールを送信します。 ```php theme={null} use Illuminate\Http\Request; use Illuminate\Support\Facades\Password; Route::post('/forgot-password', function (Request $request) { $request->validate(['email' => 'required|email']); $status = Password::sendResetLink( $request->only('email') ); return $status === Password::ResetLinkSent ? back()->with(['status' => __($status)]) : back()->withErrors(['email' => __($status)]); })->middleware('guest')->name('password.email'); ``` `Password::sendResetLink()` はステータスの文字列を返します。 | ステータス定数 | 説明 | | - | - | | `Password::ResetLinkSent` | リセットリンクを送信した | | `Password::INVALID_USER` | 指定メールアドレスのユーザーが見つからない | | `Password::RESET_THROTTLED` | 送信が制限されている(スロットリング) | ### 3. パスワードリセットフォーム メール内のリンクをクリックしたユーザーに新しいパスワードを入力させるフォームを表示します。 ```php theme={null} Route::get('/reset-password/{token}', function (string $token) { return view('auth.reset-password', ['token' => $token]); })->middleware('guest')->name('password.reset'); ``` 対応するBladeビュー: ```blade theme={null} {{-- resources/views/auth/reset-password.blade.php --}}
    @csrf
    @error('email') {{ $message }} @enderror
    @error('password') {{ $message }} @enderror
    ``` ### 4. パスワードリセット実行処理 フォームを受け取り、`Password::reset()` でパスワードを更新します。 ```php theme={null} use App\Models\User; use Illuminate\Auth\Events\PasswordReset; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Password; use Illuminate\Support\Str; Route::post('/reset-password', function (Request $request) { $request->validate([ 'token' => 'required', 'email' => 'required|email', 'password' => 'required|min:8|confirmed', ]); $status = Password::reset( $request->only('email', 'password', 'password_confirmation', 'token'), function (User $user, string $password) { $user->forceFill([ 'password' => Hash::make($password), ])->setRememberToken(Str::random(60)); $user->save(); event(new PasswordReset($user)); } ); return $status === Password::PasswordReset ? redirect()->route('login')->with('status', __($status)) : back()->withErrors(['email' => [__($status)]]); })->middleware('guest')->name('password.update'); ``` `Password::reset()` のステータス定数: | ステータス定数 | 説明 | | - | - | | `Password::PasswordReset` | パスワードリセット成功 | | `Password::INVALID_TOKEN` | トークンが無効または期限切れ | | `Password::INVALID_USER` | 指定メールアドレスのユーザーが見つからない | ## トークンの有効期限 `config/auth.php` の `expire` オプションでトークンの有効期限を分単位で設定できます。デフォルトは60分です。 ```php theme={null} 'passwords' => [ 'users' => [ 'driver' => 'database', 'expire' => 60, // 60分後に期限切れ 'throttle' => 60, ], ], ``` `database` ドライバーを使用している場合、期限切れトークンはデータベースに残り続けます。定期的にクリーンアップするには次のArtisanコマンドを使います。 ```shell theme={null} php artisan auth:clear-resets ``` スケジューラーで自動化することをおすすめします。 ```php theme={null} use Illuminate\Support\Facades\Schedule; Schedule::command('auth:clear-resets')->everyFifteenMinutes(); ``` ## カスタマイズ ### カスタム通知の使用 パスワードリセットメールをカスタマイズするには、`User` モデルで `sendPasswordResetNotification` メソッドをオーバーライドします。 ```php theme={null} use App\Notifications\ResetPasswordNotification; class User extends Authenticatable { use Notifiable, CanResetPassword; /** * パスワードリセット通知を送信する */ public function sendPasswordResetNotification($token): void { $url = 'https://example.com/reset-password?token='.$token; $this->notify(new ResetPasswordNotification($url)); } } ``` ### リセットリンクURLのカスタマイズ `AppServiceProvider` の `boot` メソッドで `ResetPassword::createUrlUsing()` を使うと、リセットリンクのURLを変更できます。SPAなど別オリジンのフロントエンドにリダイレクトしたい場合に便利です。 ```php theme={null} use App\Models\User; use Illuminate\Auth\Notifications\ResetPassword; public function boot(): void { ResetPassword::createUrlUsing(function (User $user, string $token) { return 'https://example.com/reset-password?token='.$token; }); } ``` ### Trusted Hostsの設定 パスワードリセットリンクはHTTPリクエストの `Host` ヘッダーを使って生成されます。不正なホストからのリクエストを防ぐために、`bootstrap/app.php` でTrusted Hostsを設定することを推奨します。 ```php theme={null} ->withMiddleware(function (Middleware $middleware) { $middleware->trustHosts(at: ['example.com']); }) ``` パスワードリセット機能を実装する場合は、Trusted Hostsの設定を必ず確認してください。設定が不十分だとホストヘッダーインジェクション攻撃のリスクがあります。 ## まとめ | やりたいこと | 方法 | | - | - | | リセットリンクを送信する | `Password::sendResetLink(['email' => $email])` | | パスワードをリセットする | `Password::reset($credentials, $callback)` | | テーブルを使わずリセットする | `cache` ドライバーを設定 | | 期限切れトークンを削除する | `php artisan auth:clear-resets` | | メール通知をカスタマイズする | `sendPasswordResetNotification` をオーバーライド | | リセットURLをカスタマイズする | `ResetPassword::createUrlUsing()` | ## 次のステップ Laravelの認証システム全体の仕組みを学びます。 メール通知のカスタマイズ方法を詳しく学びます。 # Precognition Source: https://kawax.biz/jp/precognition フォーム送信前にサーバー側バリデーションをリアルタイムで実行するLaravelの機能 ## Precognitionとは Precognitionは、フォーム送信前にサーバー側バリデーションを実行する仕組みです。 フロントエンドでバリデーションルールを重複定義せずに、Laravelのルールをそのまま使えます。 通常リクエストと違い、Precognitionリクエストではルートのミドルウェアやフォームリクエストの検証は実行されますが、コントローラーメソッド本体は実行されません。 そのため、入力中のリアルタイム検証に向いています。 ## インストール Laravel 13ではバックエンド側の `laravel/precognition` を追加インストールする必要はありません。 必要なのはフロントエンド向けヘルパーパッケージです。 * Vue: `laravel-precognition-vue` * React: `laravel-precognition-react` * Alpine.js: `laravel-precognition-alpine` ```shell theme={null} npm install laravel-precognition-vue ``` ```shell theme={null} npm install laravel-precognition-react ``` ```shell theme={null} npm install laravel-precognition-alpine ``` Inertiaは2.3以降でPrecognitionサポートが組み込みです。Inertia 3でもそのまま利用できます。 Inertiaフォームを使う場合は、通常 `laravel-precognition-vue` / `laravel-precognition-react` の追加導入は不要です。 ## バックエンド設定 ルートに `HandlePrecognitiveRequests` ミドルウェアを追加します。 バリデーションルールはフォームリクエストに集約するのが実践的です。 フォームリクエストにまとめると、ルールの再利用と責務分離がしやすくなります。 ```php theme={null} use App\Http\Requests\StoreUserRequest; use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests; use Illuminate\Support\Facades\Route; Route::post('/users', function (StoreUserRequest $request) { // 通常送信時のみここが実行される })->middleware([HandlePrecognitiveRequests::class]); ``` 副作用のある独自ミドルウェアがある場合は、Precognition時にスキップしてください。 ```php theme={null} public function handle(Request $request, Closure $next): mixed { if (! $request->isPrecognitive()) { Interaction::incrementFor($request->user()); } return $next($request); } ``` ## フロントエンド連携 ### Alpine.js(Blade) ```html theme={null}
    @csrf
    ``` ### Vue(Inertia.js) ```vue theme={null} ``` ### React(Inertia.js) ```jsx theme={null} import { useForm } from 'laravel-precognition-react'; const form = useForm('post', '/users', { name: '', email: '', }); form.setData('name', e.target.value)} onBlur={() => form.validate('name')} /> ``` ### Axiosを使ったバニラJS PrecognitionライブラリはAxiosを使います。 既存のAxiosインスタンスを使う場合は `client.use()` で差し替えます。 ```js theme={null} import Axios from 'axios'; import { client } from 'laravel-precognition-vue'; window.axios = Axios.create(); window.axios.defaults.headers.common['Authorization'] = authToken; client.use(window.axios); ``` ## バリデーションのタイミング制御 入力ごとの検証は `validate()` を使います。 ```js theme={null} form.validate('email'); ``` デバウンス時間は `setValidationTimeout()` で調整できます。 ```js theme={null} form.setValidationTimeout(3000); ``` ファイルも毎回検証したい場合は `validateFiles()` を使います。 ```js theme={null} form.validateFiles(); ``` 配列入力はワイルドカードで検証できます。 ```js theme={null} form.validate('users.*.email'); ``` ## フォームヘルパー `useForm()` は送信状態やエラー状態をまとめて扱えます。 * `validating`: バリデーションリクエスト中 * `processing`: 送信中 * `errors`: エラー一覧 * `valid('field')` / `invalid('field')`: フィールドの検証状態 * `submit()`: 通常送信 ```js theme={null} const submit = () => form.submit() .then(() => form.reset()); ``` ## 通常リクエストとPrecognitionリクエストの比較 以下の図は、通常リクエストとPrecognitionリクエストの処理の違いを示しています。 ```mermaid theme={null} sequenceDiagram participant User as ユーザー participant Frontend as フロントエンド participant Server as Laravelサーバー User->>Frontend: フォーム入力 Frontend->>Server: Precognitionリクエスト(Precognition: trueヘッダー) Server->>Server: バリデーション実行(コントローラーロジック未実行) Server-->>Frontend: バリデーション結果 Frontend-->>User: リアルタイムエラー表示 User->>Frontend: フォーム送信 Frontend->>Server: 通常リクエスト Server-->>Frontend: レスポンス ``` ## 関連リンク * [Laravel公式: Precognition](https://laravel.com/docs/precognition) * [Laravel公式: Validation](https://laravel.com/docs/validation) # クエリビルダー Source: https://kawax.biz/jp/query-builder LaravelのDB::table()を使ったクエリビルダーの基本から応用まで解説します。Eloquentを使わず直接SQLを組み立てる方法と使い分けを学びます。 ## クエリビルダーとは Laravelのクエリビルダーは、データベースクエリを流暢なインターフェースで構築・実行する仕組みです。`DB` ファサードの `table()` メソッドから始まり、メソッドチェーンでクエリを組み立てます。 ```php theme={null} use Illuminate\Support\Facades\DB; $users = DB::table('users')->get(); ``` 内部ではPDOパラメーターバインディングを使用しているため、SQLインジェクション対策が自動的に行われます。 クエリビルダーはLaravelがサポートするすべてのデータベース(MySQL、MariaDB、PostgreSQL、SQLite、SQL Server)で動作します。データベースを切り替えても同じコードが使えます。 ## Eloquentとの使い分け | 状況 | 推奨 | | - | - | | モデルとリレーションが必要 | Eloquent | | 複雑な集計やレポート | クエリビルダー | | パフォーマンスが重要な大量データ処理 | クエリビルダー | | 既存テーブルへのシンプルな操作 | クエリビルダー | | マイグレーションやシーダー内の処理 | クエリビルダー | ## データの取得 ### 全件取得 ```php theme={null} $users = DB::table('users')->get(); foreach ($users as $user) { echo $user->name; } ``` `get()` は `Illuminate\Support\Collection` を返します。各レコードはPHPの `stdClass` オブジェクトです。 ### 1件取得 ```php theme={null} // 最初の1件を取得(見つからなければ null) $user = DB::table('users')->where('name', '山田太郎')->first(); // 見つからなければ例外を投げる(404レスポンスを自動返却) $user = DB::table('users')->where('name', '山田太郎')->firstOrFail(); // 特定カラムの値のみ取得 $email = DB::table('users')->where('name', '山田太郎')->value('email'); // IDで取得 $user = DB::table('users')->find(3); ``` ### 特定カラムの値をリストで取得 ```php theme={null} // emailカラムの値のコレクション $emails = DB::table('users')->pluck('email'); // nameをキー、emailを値とする連想コレクション $emailByName = DB::table('users')->pluck('email', 'name'); ``` ### 大量データの分割処理 ```php theme={null} use Illuminate\Support\Collection; // 100件ずつ処理 DB::table('users')->orderBy('id')->chunk(100, function (Collection $users) { foreach ($users as $user) { // 処理... } }); // 更新しながらチャンク処理する場合は chunkById を使う DB::table('users')->where('active', false) ->chunkById(100, function (Collection $users) { foreach ($users as $user) { DB::table('users') ->where('id', $user->id) ->update(['active' => true]); } }); ``` チャンク処理中にレコードを更新・削除する場合、`chunk()` ではなく `chunkById()` を使ってください。`chunk()` ではレコードのずれが生じることがあります。 ### ストリーミング(LazyCollection) ```php theme={null} DB::table('users')->orderBy('id')->lazy()->each(function (object $user) { // 1件ずつ処理 }); ``` ## 集計 ```php theme={null} $count = DB::table('users')->count(); $maxAge = DB::table('users')->max('age'); $minAge = DB::table('users')->min('age'); $avgAge = DB::table('users')->avg('age'); $total = DB::table('orders')->sum('amount'); // 条件付き集計 $avgPremium = DB::table('orders') ->where('plan', 'premium') ->avg('amount'); ``` ### レコードの存在確認 ```php theme={null} if (DB::table('orders')->where('finalized', 1)->exists()) { // レコードが存在する } if (DB::table('orders')->where('finalized', 1)->doesntExist()) { // レコードが存在しない } ``` ## SELECT句 ```php theme={null} // 取得するカラムを指定 $users = DB::table('users') ->select('name', 'email as user_email') ->get(); // 重複を除外 $users = DB::table('users')->distinct()->get(); // 後からカラムを追加 $query = DB::table('users')->select('name'); $users = $query->addSelect('age')->get(); ``` ## WHERE句 ### 基本的な条件 ```php theme={null} // 等値条件(= は省略可能) $users = DB::table('users')->where('votes', 100)->get(); // 比較演算子を指定 $users = DB::table('users')->where('votes', '>=', 100)->get(); $users = DB::table('users')->where('name', 'like', '山田%')->get(); // 複数条件(AND) $users = DB::table('users') ->where('status', 'active') ->where('age', '>', 20) ->get(); // OR条件 $users = DB::table('users') ->where('votes', '>', 100) ->orWhere('name', '山田太郎') ->get(); ``` ### 条件のグループ化 ```php theme={null} use Illuminate\Database\Query\Builder; // OR条件をグループ化してANDと組み合わせる $users = DB::table('users') ->where('active', true) ->where(function (Builder $query) { $query->where('role', 'admin') ->orWhere('role', 'moderator'); }) ->get(); // WHERE active = 1 AND (role = 'admin' OR role = 'moderator') ``` ### whereIn / whereBetween / whereNull ```php theme={null} // IN句 $users = DB::table('users') ->whereIn('id', [1, 2, 3]) ->get(); $users = DB::table('users') ->whereNotIn('id', [1, 2, 3]) ->get(); // BETWEEN句 $users = DB::table('users') ->whereBetween('age', [20, 40]) ->get(); // NULL判定 $users = DB::table('users')->whereNull('deleted_at')->get(); $users = DB::table('users')->whereNotNull('email_verified_at')->get(); ``` ### whereLike(パターンマッチ) ```php theme={null} // デフォルトは大文字小文字を区別しない $users = DB::table('users') ->whereLike('name', '%山田%') ->get(); // 大文字小文字を区別する $users = DB::table('users') ->whereLike('name', '%Yamada%', caseSensitive: true) ->get(); ``` ### whereAny / whereAll(複数カラムへの同一条件) ```php theme={null} // いずれかのカラムがLIKE条件にマッチ $users = DB::table('users') ->where('active', true) ->whereAny(['name', 'email', 'bio'], 'like', '%Laravel%') ->get(); // すべてのカラムがLIKE条件にマッチ $posts = DB::table('posts') ->whereAll(['title', 'content'], 'like', '%Laravel%') ->get(); ``` ### whereNullSafeEquals(NULL安全な等価比較) `whereNullSafeEquals` と `orWhereNullSafeEquals` は、カラムの値を指定した値と比較する際に、**2つの NULL 値を等しいとみなす**比較を行います。 通常の `=` 演算子では `NULL = NULL` は `false` になりますが、`whereNullSafeEquals` では `NULL` 同士を等しいと判定します。MySQLの `<=>` 演算子、PostgreSQLの `IS NOT DISTINCT FROM` に対応します。 ```php theme={null} $lastLoginIp = $request->input('last_login_ip'); // $lastLoginIp が null の場合でも last_login_ip IS NULL のユーザーが取得できる $users = DB::table('users') ->whereNullSafeEquals('last_login_ip', $lastLoginIp) ->get(); ``` 通常の `where('column', null)` は `WHERE column IS NULL` に変換されますが、`whereNullSafeEquals('column', $value)` はバインディングされた値が `null` であっても `null` でなくても一貫して動作します。ユーザー入力値が `null` になりうる場合に特に便利です。 ## JOIN ```php theme={null} // INNER JOIN $users = DB::table('users') ->join('orders', 'users.id', '=', 'orders.user_id') ->select('users.name', 'orders.amount') ->get(); // LEFT JOIN $users = DB::table('users') ->leftJoin('orders', 'users.id', '=', 'orders.user_id') ->get(); // 複数テーブルのJOIN $users = DB::table('users') ->join('contacts', 'users.id', '=', 'contacts.user_id') ->join('orders', 'users.id', '=', 'orders.user_id') ->select('users.*', 'contacts.phone', 'orders.amount') ->get(); ``` ### サブクエリJOIN ```php theme={null} // サブクエリを使ったJOIN $latestOrders = DB::table('orders') ->select('user_id', DB::raw('MAX(created_at) as last_order_at')) ->groupBy('user_id'); $users = DB::table('users') ->joinSub($latestOrders, 'latest_orders', function ($join) { $join->on('users.id', '=', 'latest_orders.user_id'); }) ->get(); ``` ## 並び替え・グループ化・リミット ```php theme={null} // 並び替え $users = DB::table('users') ->orderBy('name', 'asc') ->get(); // 複数カラムで並び替え $users = DB::table('users') ->orderBy('last_name') ->orderBy('first_name', 'desc') ->get(); // ランダム順 $users = DB::table('users')->inRandomOrder()->get(); // グループ化 $orders = DB::table('orders') ->select('status', DB::raw('COUNT(*) as count')) ->groupBy('status') ->get(); // HAVING句 $orders = DB::table('orders') ->select('user_id', DB::raw('SUM(amount) as total')) ->groupBy('user_id') ->having('total', '>', 10000) ->get(); // リミットとオフセット $users = DB::table('users') ->skip(10) // OFFSET ->take(5) // LIMIT ->get(); ``` ## サブクエリ ```php theme={null} // WHERE句のサブクエリ $activeUsers = DB::table('users')->select('id')->where('is_active', 1); $comments = DB::table('comments') ->whereIn('user_id', $activeUsers) ->get(); // SELECT句のサブクエリ $users = DB::table('users') ->select('name') ->selectSub(function ($query) { $query->from('orders') ->selectRaw('COUNT(*)') ->whereColumn('orders.user_id', 'users.id'); }, 'order_count') ->get(); ``` ## ベクター類似検索 埋め込みベクトルを保存したカラムに対して、`whereVectorSimilarTo` を使うと指定したベクトルに近いレコードだけを検索できます。ベクターカラムは、マイグレーションで `vector` 型として定義します([マイグレーションのベクターカラム](/jp/migrations#ベクターカラムとインデックス))。 ```php theme={null} use Illuminate\Support\Facades\DB; $embedding = [0.12, -0.34, 0.56]; $documents = DB::table('documents') ->whereVectorSimilarTo('embedding', $embedding, minSimilarity: 0.4) ->limit(10) ->get(); ``` `whereVectorSimilarTo` は類似度の高い順に並べます。最小類似度の指定が不要で、距離による並び順だけを明示したい場合は `orderByVectorDistance` を使えます。返却するカラムに距離を含めたいときは、`selectVectorDistance` で別名を指定できます。 ```php theme={null} $documents = DB::table('documents') ->select('id', 'title') ->selectVectorDistance('embedding', $embedding, as: 'distance') ->orderByVectorDistance('embedding', $embedding) ->limit(10) ->get(); ``` これらのメソッドはデータベースが提供するベクター演算子へ変換されます。利用するデータベースがベクター型と演算をサポートしていることを確認し、PostgreSQLでは `pgvector` 拡張を有効にしてください。 ## Raw式 Raw式はSQL文字列として直接クエリに挿入されます。ユーザー入力を直接渡すとSQLインジェクションの危険があります。必ずバインディングを使って安全に記述してください。 ```php theme={null} // DB::raw() — 任意のSQL式を埋め込む $users = DB::table('users') ->select(DB::raw('count(*) as user_count, status')) ->groupBy('status') ->get(); // selectRaw — SELECT句にRaw式を追加 $orders = DB::table('orders') ->selectRaw('price * ? as price_with_tax', [1.10]) ->get(); // whereRaw — WHERE句にRaw式を追加 $orders = DB::table('orders') ->whereRaw('price > IF(state = "JP", ?, 100)', [500]) ->get(); // havingRaw — HAVING句にRaw式を追加 $orders = DB::table('orders') ->select('department', DB::raw('SUM(amount) as total')) ->groupBy('department') ->havingRaw('SUM(amount) > ?', [100000]) ->get(); // orderByRaw — ORDER BY句にRaw式を追加 $orders = DB::table('orders') ->orderByRaw('updated_at - created_at DESC') ->get(); ``` ## INSERT / UPDATE / DELETE ### INSERT ```php theme={null} // 1件挿入 DB::table('users')->insert([ 'email' => 'yamada@example.com', 'name' => '山田太郎', ]); // 複数件挿入 DB::table('users')->insert([ ['email' => 'yamada@example.com', 'name' => '山田太郎'], ['email' => 'suzuki@example.com', 'name' => '鈴木花子'], ]); // 挿入後にAUTO_INCREMENTのIDを取得 $id = DB::table('users')->insertGetId([ 'email' => 'sato@example.com', 'name' => '佐藤次郎', ]); ``` ### UPSERT(INSERT OR UPDATE) ```php theme={null} // 存在すれば更新、なければ挿入 DB::table('users')->upsert( [ ['email' => 'yamada@example.com', 'name' => '山田太郎', 'votes' => 5], ['email' => 'suzuki@example.com', 'name' => '鈴木花子', 'votes' => 10], ], uniqueBy: ['email'], // 重複チェックのカラム update: ['name', 'votes'] // 更新するカラム ); ``` ### UPDATE ```php theme={null} // 条件付き更新 $affected = DB::table('users') ->where('id', 1) ->update(['name' => '山田一郎', 'updated_at' => now()]); // インクリメント・デクリメント DB::table('users')->where('id', 1)->increment('votes'); // +1 DB::table('users')->where('id', 1)->increment('votes', 5); // +5 DB::table('users')->where('id', 1)->decrement('votes'); // -1 DB::table('users')->where('id', 1)->decrement('balance', 100); // -100 ``` ### DELETE ```php theme={null} // 条件付き削除 $deleted = DB::table('users')->where('status', 'inactive')->delete(); // テーブル全件削除(AUTO_INCREMENTをリセット) DB::table('users')->truncate(); ``` ## 条件付きクエリ(when) クエリ条件を動的に適用したいときは `when()` を使うと条件分岐をすっきり書けます。 ```php theme={null} $status = request('status'); $sortBy = request('sort', 'name'); $users = DB::table('users') ->when($status, function ($query, $status) { $query->where('status', $status); }) ->when($sortBy === 'email', function ($query) { $query->orderBy('email'); }, function ($query) { $query->orderBy('name'); }) ->get(); ``` ## デバッグ ```php theme={null} // 生成されるSQLを確認 $sql = DB::table('users')->where('active', true)->toSql(); // "select * from `users` where `active` = ?" // SQLとバインディングを両方確認 $bindings = DB::table('users')->where('active', true)->getBindings(); // クエリを実行してダンプ(実行は続行) DB::table('users')->where('active', true)->dump(); // クエリを実行してダンプして終了 DB::table('users')->where('active', true)->dd(); ``` `dd()` はデバッグ時に便利ですが、本番環境では絶対に使わないでください。`toSql()` と `getBindings()` でSQLとバインディングを確認するのが安全です。 ## まとめ | メソッド | 説明 | | - | - | | `get()` | 全件取得(Collection) | | `first()` | 最初の1件取得 | | `find($id)` | IDで1件取得 | | `value($column)` | カラムの値を1件取得 | | `pluck($column)` | カラム値のリストを取得 | | `count()` | 件数 | | `sum($col)` | 合計 | | `avg($col)` | 平均 | | `max($col)` | 最大値 | | `min($col)` | 最小値 | | `exists()` | 存在確認 | | `insert([...])` | 挿入 | | `update([...])` | 更新 | | `delete()` | 削除 | | `chunk($n, fn)` | 分割して処理 | | `when($cond, fn)` | 条件付きクエリ | | `toSql()` | 生成SQLを確認 | クエリビルダーはEloquentよりも低レベルで、モデルのインスタンスではなく `stdClass` を返します。 リレーションやモデルイベント(オブザーバー)が不要な場合、クエリビルダーの方がシンプルで高速です。 ```php theme={null} // Eloquent: Userモデルのインスタンスを返す $users = User::where('active', true)->get(); echo $users[0]->name; // Userオブジェクト // クエリビルダー: stdClassを返す $users = DB::table('users')->where('active', true)->get(); echo $users[0]->name; // stdClassオブジェクト ``` # HTTPリクエスト Source: https://kawax.biz/jp/requests LaravelのIlluminate\Http\Requestオブジェクトを使ってHTTPリクエストのデータを取得する方法を解説します。 ## Requestオブジェクトとは `Illuminate\Http\Request` は、アプリケーションが受け取るHTTPリクエストをオブジェクト指向で扱うためのクラスです。 フォームデータ・クエリ文字列・ファイル・ヘッダーなど、リクエストに含まれるあらゆる情報にアクセスできます。 ## コントローラーへのリクエスト注入 コントローラーのメソッドで `Illuminate\Http\Request` をタイプヒントすると、Laravelのサービスコンテナが自動的にリクエストインスタンスを注入します。 ```php theme={null} input('name'); // ユーザーを保存する処理... return redirect('/users'); } } ``` ルートクロージャでも同じようにタイプヒントできます。 ```php theme={null} use Illuminate\Http\Request; Route::get('/', function (Request $request) { // ... }); ``` ### ルートパラメーターとの併用 コントローラーメソッドがルートパラメーターも受け取る場合は、`Request` の後にルートパラメーターを記述します。 ```php theme={null} use App\Http\Controllers\UserController; Route::put('/user/{id}', [UserController::class, 'update']); ``` ```php theme={null} public function update(Request $request, string $id): RedirectResponse { // $request でリクエストデータに、$id でルートパラメーターにアクセスできる return redirect('/users'); } ``` ## リクエストデータの取得 ### すべての入力データを取得 `all` メソッドで、すべての入力データを配列として取得します。 HTMLフォームからのリクエストでも、XHRリクエストでも使えます。 ```php theme={null} $input = $request->all(); ``` ### 特定のフィールドを取得 `input` メソッドはHTTPメソッドに関わらず、リクエストペイロード全体(クエリ文字列を含む)から値を取得します。 ```php theme={null} $name = $request->input('name'); ``` 値が存在しない場合のデフォルト値を第2引数で指定できます。 ```php theme={null} $name = $request->input('name', 'Sally'); ``` 配列の入力にはドット記法でアクセスします。 ```php theme={null} $name = $request->input('products.0.name'); $names = $request->input('products.*.name'); ``` ### クエリ文字列の取得 `query` メソッドはクエリ文字列(URLの `?` 以降)からのみ値を取得します。 ```php theme={null} $name = $request->query('name'); $name = $request->query('name', 'Helen'); // すべてのクエリ文字列を配列で取得 $query = $request->query(); ``` `input` はリクエストボディとクエリ文字列の両方を対象にしますが、`query` はクエリ文字列のみを対象にします。 フォームのPOSTデータとURLパラメーターを区別したい場合は `query` を使いましょう。 ### 型付きの取得 `input` 以外にも、型を指定して入力値を取得するメソッドがあります。 ```php theme={null} // 文字列として取得(Stringableインスタンス) $name = $request->string('name')->trim(); // 整数として取得 $perPage = $request->integer('per_page'); // 真偽値として取得("1", "true", "on", "yes" → true) $archived = $request->boolean('archived'); // 配列として取得 $versions = $request->array('versions'); // 日付をCarbonインスタンスとして取得 $birthday = $request->date('birthday'); ``` ### 動的プロパティ フィールド名をそのままプロパティとしてアクセスすることもできます。 ```php theme={null} $name = $request->name; ``` 動的プロパティでは、まずリクエストペイロードから値を探し、見つからなければルートパラメーターを検索します。 ### 入力データの一部を取得 `only` と `except` で入力データのサブセットを取得できます。 ```php theme={null} $input = $request->only(['username', 'password']); $input = $request->except(['credit_card']); ``` ## 入力の存在確認 `has` メソッドで値がリクエストに含まれているか確認します。 ```php theme={null} if ($request->has('name')) { // ... } // 複数のキーをすべて含んでいるか確認 if ($request->has(['name', 'email'])) { // ... } ``` 空文字を除いて値が存在するかは `filled` を使います。 ```php theme={null} if ($request->filled('name')) { // ... } ``` 値が存在しない、または空文字の場合は `isNotFilled` を使います。 ```php theme={null} if ($request->isNotFilled('name')) { // ... } ``` ## リクエストのパスとメソッド確認 ### パスの取得と確認 ```php theme={null} // パスを取得(例: "foo/bar") $uri = $request->path(); // パターンと一致するか確認(ワイルドカード * が使える) if ($request->is('admin/*')) { // ... } // 名前付きルートと一致するか確認 if ($request->routeIs('admin.*')) { // ... } ``` ### URLの取得 ```php theme={null} // クエリ文字列なしのURL $url = $request->url(); // クエリ文字列を含むURL $urlWithQueryString = $request->fullUrl(); ``` ### HTTPメソッドの確認 `isMethod` でHTTPメソッドを確認できます。 ```php theme={null} $method = $request->method(); if ($request->isMethod('post')) { // POSTリクエストのみ処理する } ``` ## ファイルアップロード ### アップロードされたファイルの取得 `file` メソッドでアップロードされたファイルを取得します。 `Illuminate\Http\UploadedFile` インスタンスが返ります。 ```php theme={null} $file = $request->file('photo'); // 動的プロパティでも取得できる $file = $request->photo; ``` ファイルが存在するか確認するには `hasFile` を使います。 ```php theme={null} if ($request->hasFile('photo')) { // ... } ``` ### ファイルの保存 `store` メソッドでファイルをストレージに保存します。 ファイル名は自動的に生成されます。 ```php theme={null} // デフォルトディスクの "images" ディレクトリに保存 $path = $request->photo->store('images'); // S3に保存 $path = $request->photo->store('images', 's3'); ``` ファイル名を指定したい場合は `storeAs` を使います。 ```php theme={null} $path = $request->photo->storeAs('images', 'filename.jpg'); ``` ファイルアップロードを受け付ける場合は、必ずファイルの種類やサイズを検証してください。 バリデーションを使って安全に処理することを推奨します。 ## 実例:フォームデータの受け取り ユーザー登録フォームのデータをコントローラーで受け取る例です。 ```php theme={null} input('name'); $email = $request->input('email'); $password = $request->input('password'); // 必要なデータのみ取得 $data = $request->only(['name', 'email', 'password']); // ユーザーを作成する処理... return redirect('/dashboard'); } } ``` 対応するルートを定義します。 ```php theme={null} use App\Http\Controllers\RegisterController; Route::post('/register', [RegisterController::class, 'store']); ``` ## 次のステップ リクエストをフィルタリングするミドルウェアの使い方を振り返ります。 # レスポンス Source: https://kawax.biz/jp/responses Laravelのレスポンスの種類と、コントローラーから適切なレスポンスを返す方法を解説します。 ## レスポンスとは コントローラーやルートは、ユーザーのブラウザへ何らかの**レスポンス**を返す必要があります。 Laravelは文字列・配列・ビュー・リダイレクト・JSONなど、様々な形式のレスポンスをシンプルに返せる仕組みを提供しています。 ## 基本的なレスポンス ### 文字列を返す 最もシンプルなレスポンスは文字列の返却です。 Laravelは自動的に適切なHTTPレスポンスに変換します。 ```php theme={null} Route::get('/', function () { return 'Hello World'; }); ``` ### 配列を返す 配列を返すと、LaravelはそれをJSON形式に自動変換してレスポンスします。 ```php theme={null} Route::get('/users', function () { return [ ['id' => 1, 'name' => '田中'], ['id' => 2, 'name' => '鈴木'], ]; }); ``` EloquentモデルやコレクションをそのままReturnするとJSON形式に自動変換されます。APIを手軽に作れるのでよく使われるパターンです。 ### Eloquentモデルを返す ```php theme={null} use App\Models\User; Route::get('/user/{user}', function (User $user) { return $user; }); ``` ## Responseオブジェクト `response()` ヘルパーを使うと、HTTPステータスコードやヘッダーを指定した詳細なレスポンスを返せます。 ```php theme={null} Route::get('/home', function () { return response('Hello World', 200) ->header('Content-Type', 'text/plain'); }); ``` `response()` の第1引数はレスポンスのボディ、第2引数はHTTPステータスコードです。 ### よく使うHTTPステータスコード | コード | 意味 | | - | - | | `200` | OK(成功) | | `201` | Created(リソース作成成功) | | `204` | No Content(コンテンツなし) | | `301` | Moved Permanently(恒久的リダイレクト) | | `302` | Found(一時的リダイレクト) | | `404` | Not Found(見つからない) | | `422` | Unprocessable Entity(バリデーションエラー) | | `500` | Internal Server Error(サーバーエラー) | ## ビューのレスポンス コントローラーからBladeテンプレートを表示するには `view()` を使います。 これがWebアプリケーションにおける最も一般的なレスポンスです。 ```php theme={null} '田中', 'email' => 'tanaka@example.com'], ['name' => '鈴木', 'email' => 'suzuki@example.com'], ]; return view('users.index', ['users' => $users]); } } ``` `view()` の第1引数はビュー名、第2引数はビューへ渡すデータの配列です。 戻り値の型ヒントに `Illuminate\View\View` を指定すると、コードの意図が明確になります。 ### `response()->view()` で詳細を制御する ステータスコードやヘッダーも一緒に指定したい場合は `response()->view()` を使います。 ```php theme={null} return response() ->view('errors.404', ['message' => 'ページが見つかりません'], 404); ``` ## リダイレクト ### 基本的なリダイレクト `redirect()` ヘルパーはリダイレクトレスポンスを返します。 フォーム送信後に別ページへ誘導する際によく使います。 ```php theme={null} use Illuminate\Http\RedirectResponse; Route::get('/old-page', function (): RedirectResponse { return redirect('/new-page'); }); ``` コントローラーでの使用例です。 ```php theme={null} name('posts.index'); Route::get('/posts/{post}', [PostController::class, 'show'])->name('posts.show'); ``` ```php theme={null} // 名前付きルートへリダイレクト return redirect()->route('posts.index'); // ルートパラメーターを渡す場合 return redirect()->route('posts.show', ['post' => $post->id]); ``` ### 直前のページへ戻る `back()` でユーザーが直前にいたページへ戻れます。 バリデーション失敗時のリダイレクトによく使われます。 ```php theme={null} return back(); // 入力データを保持して戻る return back()->withInput(); ``` ### フラッシュメッセージ付きのリダイレクト `with()` でセッションにメッセージを付けてリダイレクトできます。 フォーム送信成功後のメッセージ表示に便利です。 ```php theme={null} return redirect('/posts')->with('success', '投稿を作成しました。'); ``` Bladeテンプレートでメッセージを表示します。 ```blade theme={null} @if (session('success'))
    {{ session('success') }}
    @endif ``` ## JSONレスポンス APIを作る場合には `response()->json()` を使ってJSONレスポンスを返します。 `Content-Type: application/json` ヘッダーが自動的に設定されます。 ```php theme={null} use Illuminate\Http\JsonResponse; Route::get('/api/users', function (): JsonResponse { $users = [ ['id' => 1, 'name' => '田中'], ['id' => 2, 'name' => '鈴木'], ]; return response()->json($users); }); ``` ステータスコードも指定できます。 ```php theme={null} return response()->json(['message' => '作成しました'], 201); ``` コントローラーでの使用例です。 ```php theme={null} json($users); } public function store(Request $request): JsonResponse { // バリデーション済みデータで作成(フォームリクエストまたは $request->validate() の後) $user = User::create($request->validated()); return response()->json($user, 201); } } ``` 単純に配列やEloquentモデルをReturnするだけでもJSONレスポンスになりますが、`response()->json()` を使うとステータスコードやヘッダーを細かく制御できます。 ## レスポンスヘッダー `header()` メソッドでレスポンスにHTTPヘッダーを追加できます。 ```php theme={null} return response('Hello World') ->header('Content-Type', 'text/plain') ->header('X-Custom-Header', 'MyValue'); ``` 複数のヘッダーをまとめて設定する場合は `withHeaders()` を使います。 ```php theme={null} return response('Hello World') ->withHeaders([ 'Content-Type' => 'text/plain', 'X-Custom-Header' => 'MyValue', 'Cache-Control' => 'no-cache', ]); ``` ## Cookie レスポンスにCookieを追加するには `cookie()` を使います。Cookieを削除する場合は、`withoutCookie()` で1つ、`withoutCookies()` で複数のCookieを期限切れにできます。 ```php theme={null} return response('Hello World') ->withoutCookie('name'); return response('Hello World') ->withoutCookies([ 'name', 'email', 'preferences', ]); ``` ## 実践例:コントローラーでの使い分け 用途に応じたレスポンスの使い分けをまとめた例です。 ```php theme={null} use App\Http\Controllers\PostController; Route::get('/posts', [PostController::class, 'index'])->name('posts.index'); Route::get('/posts/create', [PostController::class, 'create'])->name('posts.create'); Route::post('/posts', [PostController::class, 'store'])->name('posts.store'); Route::get('/posts/{post}', [PostController::class, 'show'])->name('posts.show'); Route::delete('/posts/{post}', [PostController::class, 'destroy'])->name('posts.destroy'); ``` ```php theme={null} get(); return view('posts.index', ['posts' => $posts]); } // 作成フォーム: ビューを返す public function create(): View { return view('posts.create'); } // 保存: リダイレクトを返す public function store(Request $request): RedirectResponse { // バリデーション済みデータで作成(フォームリクエストを使用している場合) $post = Post::create($request->validated()); return redirect() ->route('posts.show', ['post' => $post->id]) ->with('success', '投稿を作成しました。'); } // 詳細表示: ビューを返す public function show(Post $post): View { return view('posts.show', ['post' => $post]); } // 削除: リダイレクトを返す public function destroy(Post $post): RedirectResponse { $post->delete(); return redirect() ->route('posts.index') ->with('success', '投稿を削除しました。'); } } ``` Webアプリでは `View` と `RedirectResponse` の使い分けが基本です。表示系のアクションは `View`、変更系(作成・更新・削除)のアクションは処理後に `RedirectResponse` を返すのが一般的なパターンです。 ## 次のステップ レスポンスを返す前に、入力データをバリデーションで検証する方法を確認します。 # ルーティング Source: https://kawax.biz/jp/routing Laravelのルート定義方法、ルートパラメータ、名前付きルートの基本を解説します。 ## 基本的なルーティング Laravelの最も基本的なルートは、URIとクロージャを受け取ります。 複雑な設定ファイルなしに、ルートと動作を簡単に定義できます。 ```php theme={null} use Illuminate\Support\Facades\Route; Route::get('/greeting', function () { return 'Hello World'; }); ``` ### デフォルトのルートファイル すべてのLaravelルートは `routes/` ディレクトリのルートファイルに定義します。 これらのファイルは `bootstrap/app.php` の設定に基づいてLaravelが自動的に読み込みます。 ```mermaid theme={null} flowchart LR A["受信リクエスト"] --> B{{"リクエストの種類"}} B -->|"ブラウザ向け
    セッション / CSRF"| C["routes/web.php
    webミドルウェアグループ"] B -->|"API向け
    ステートレス"| D["routes/api.php
    apiミドルウェアグループ"] B -->|"CLIコマンド"| E["routes/console.php
    Artisanコマンド定義"] C --> F["コントローラー / クロージャ"] D --> F E --> G["コマンドハンドラー"] ``` `routes/web.php` にはブラウザ向けのルートを定義します。 このファイルのルートには `web` ミドルウェアグループが適用され、セッション・CSRF保護・クッキー暗号化が提供されます。 ```php theme={null} use App\Http\Controllers\UserController; Route::get('/user', [UserController::class, 'index']); ``` ### APIルート アプリケーションがステートレス API を提供する場合、`install:api` Artisanコマンドで API ルーティングを有効にできます。 ```shell theme={null} php artisan install:api ``` `install:api` コマンドは [Laravel Sanctum](/jp/sanctum) をインストールし、`routes/api.php` ファイルを作成します。 Sanctumはサードパーティの API コンシューマ、SPA、モバイルアプリケーション向けのシンプルで堅牢な API トークン認証ガードを提供します。 ```php theme={null} Route::get('/user', function (Request $request) { return $request->user(); })->middleware('auth:sanctum'); ``` `routes/api.php` のルートはステートレスで `api` [ミドルウェアグループ](/jp/middleware)が適用されます。 また、すべてのルートに自動的に `/api` URI プレフィックスが適用されるため、手動で付与する必要はありません。 プレフィックスを変更する場合は、アプリケーションの `bootstrap/app.php` ファイルを編集します。 ```php theme={null} ->withRouting( api: __DIR__.'/../routes/api.php', apiPrefix: 'api/admin', // ... ) ``` ### 利用可能なHTTPメソッド ルーターはすべてのHTTPメソッドに対応したルートを登録できます。 ```php theme={null} Route::get($uri, $callback); Route::post($uri, $callback); Route::put($uri, $callback); Route::patch($uri, $callback); Route::delete($uri, $callback); Route::options($uri, $callback); ``` 複数のHTTPメソッドに応答するルートは `match` または `any` を使います。 ```php theme={null} Route::match(['get', 'post'], '/', function () { // ... }); Route::any('/', function () { // ... }); ``` `web` ルートファイルで `POST`・`PUT`・`PATCH`・`DELETE` を受け取るHTMLフォームには、必ず `@csrf` ディレクティブを含めてください。 含めない場合、リクエストが拒否されます。 ## ビューを返すルート ルートがビューを返すだけの場合は、`Route::view` を使うと簡潔に書けます。 ```php theme={null} Route::view('/welcome', 'welcome'); // ビューにデータを渡す場合 Route::view('/welcome', 'welcome', ['name' => 'Taylor']); ``` ## ルートパラメータ ### 必須パラメータ URLの一部を動的に取得するには、ルートパラメータを使います。 パラメータは `{}` で囲み、ルートコールバックの引数として受け取ります。 ```php theme={null} Route::get('/user/{id}', function (string $id) { return 'ユーザー ID: ' . $id; }); ``` 複数のパラメータも定義できます。 ```php theme={null} Route::get('/posts/{post}/comments/{comment}', function (string $postId, string $commentId) { return "投稿 {$postId} のコメント {$commentId}"; }); ``` ルートパラメータには `/` 文字を含めることはできません。 ### 任意パラメータ パラメータを任意にするには、パラメータ名の末尾に `?` を付け、対応する引数にデフォルト値を設定します。 ```php theme={null} Route::get('/user/{name?}', function (?string $name = null) { return $name ?? 'ゲスト'; }); Route::get('/user/{name?}', function (?string $name = 'ゲスト') { return $name; }); ``` ### 正規表現によるパラメータ制約 `where` メソッドを使ってパラメータの形式を正規表現で制約できます。 ```php theme={null} Route::get('/user/{id}', function (string $id) { // ... })->where('id', '[0-9]+'); Route::get('/user/{name}', function (string $name) { // ... })->where('name', '[a-zA-Z]+'); ``` よく使うパターンは便利メソッドでも書けます。 ```php theme={null} Route::get('/user/{id}/{name}', function (string $id, string $name) { // ... })->whereNumber('id')->whereAlpha('name'); ``` ## 名前付きルート 名前付きルートを使うと、URL生成やリダイレクトを行う際にルート名を使って参照できます。 ```php theme={null} Route::get('/user/profile', function () { // ... })->name('profile'); ``` ### 名前付きルートへのURL生成 ルート名からURLを生成するには `route` ヘルパーを使います。 ```php theme={null} // URL生成 $url = route('profile'); // パラメータ付きURL生成 $url = route('user.show', ['id' => 1]); ``` ### リダイレクト ```php theme={null} return redirect()->route('profile'); ``` ## ルートグループ 関連するルートをグループ化して、ミドルウェア・コントローラー・プレフィックスなどを共通設定できます。 ```mermaid theme={null} flowchart TD A["HTTPリクエスト"] --> B["ルーター"] B --> C{{"マッチするルートが
    あるか?"}} C -->|"なし"| D["404レスポンス"] C -->|"あり"| E["ルートのミドルウェアを確認"] E --> F{{"ミドルウェアを
    通過できるか?"}} F -->|"拒否"| G["リダイレクト /
    エラーレスポンス"] F -->|"通過"| H["コントローラー /
    クロージャ実行"] H --> I["レスポンス生成"] I --> J["クライアントへ送信"] ``` ### ミドルウェア ```php theme={null} Route::middleware(['auth'])->group(function () { Route::get('/dashboard', function () { // 認証済みユーザーのみアクセス可能 }); Route::get('/account', function () { // 認証済みユーザーのみアクセス可能 }); }); ``` ### コントローラー 同じコントローラーを使うルートをグループ化できます。 ```php theme={null} use App\Http\Controllers\OrderController; Route::controller(OrderController::class)->group(function () { Route::get('/orders', 'index'); Route::post('/orders', 'store'); Route::get('/orders/{id}', 'show'); }); ``` ### プレフィックス ルートのURIに共通のプレフィックスを付けられます。 ```php theme={null} Route::prefix('admin')->group(function () { Route::get('/users', function () { // /admin/users にアクセス可能 }); Route::get('/posts', function () { // /admin/posts にアクセス可能 }); }); ``` ## ルート一覧の確認 定義されたすべてのルートを一覧表示するには、Artisanコマンドを使います。 ```shell theme={null} php artisan route:list ``` ミドルウェア情報も含めて表示する場合は次のようにします。 ```shell theme={null} php artisan route:list -v ``` 特定のURIから始まるルートのみを表示する場合は次のようにします。 ```shell theme={null} php artisan route:list --path=user ``` ## 次のステップ ビューを作成し、コントローラーからデータを渡す方法を学びます。 # Laravel Sanctum(APIトークン認証) Source: https://kawax.biz/jp/sanctum Laravel SanctumによるAPIトークン認証とSPA認証の実装方法を解説します。シンプルで軽量な認証パッケージの導入から実践的な使い方まで。 ## Sanctum とは Laravel Sanctum は、SPA(シングルページアプリケーション)・モバイルアプリ・シンプルなAPIに向けた軽量な認証パッケージです。複雑なOAuthの知識がなくても、ユーザーごとに複数のAPIトークンを発行・管理できます。 Sanctum が解決する問題は2つあります。 | 認証モード | 仕組み | 主な用途 | | - | - | - | | **APIトークン認証** | `Authorization: Bearer ` ヘッダー | モバイルアプリ・サードパーティ連携 | | **SPA認証** | セッションCookie + CSRF保護 | 自社フロントエンド(Vue/React等) | 自社SPAからAPIを呼び出す場合はSPA認証を使います。モバイルアプリやサードパーティがAPIを利用する場合はAPIトークン認証を使います。どちらか一方だけを使うことも構いません。 ### Passport との使い分け | | Sanctum | Passport | | - | - | - | | **複雑さ** | シンプル | フル機能OAuth2 | | **向いている用途** | 自社SPA・モバイルアプリ | 外部アプリへのOAuthプロバイダー | | **トークン種別** | パーソナルアクセストークン | OAuth2アクセストークン | 外部サービスに対してOAuth2プロバイダーになる必要がある場合は Passport を選びますが、多くのアプリケーションでは Sanctum で十分です。 *** ## インストールと設定 ### インストール `install:api` Artisanコマンドを実行するだけで Sanctum がセットアップされます。 ```shell theme={null} php artisan install:api ``` このコマンドは以下を自動的に行います。 * `laravel/sanctum` パッケージのインストール * `personal_access_tokens` テーブルのマイグレーションファイルの公開 * マイグレーションの実行 ### HasApiTokens トレイトを追加する `User` モデルに `HasApiTokens` トレイトを追加します。 ```php theme={null} // app/Models/User.php use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable { use HasApiTokens, HasFactory, Notifiable; } ``` これで `$user->createToken()` や `$user->tokens` などのメソッドが使えるようになります。 *** ## API トークン認証 ### トークンフロー ```mermaid theme={null} sequenceDiagram participant Client as クライアント participant API as Laravel API participant DB as データベース Client->>API: POST /login (email, password) API->>DB: ユーザー認証 DB-->>API: ユーザー情報 API->>DB: トークン生成・保存(ハッシュ) API-->>Client: plainTextToken を返却 Note over Client: トークンを保存 Client->>API: GET /api/user
    Authorization: Bearer API->>DB: トークン照合(SHA-256) DB-->>API: 認証済みユーザー API-->>Client: レスポンス ``` ### トークンを発行する `createToken()` メソッドでトークンを発行します。`plainTextToken` プロパティから平文のトークン値を取得できます。**平文トークンはデータベースには保存されない**ため、発行直後にユーザーへ返す必要があります。 ```php theme={null} use Illuminate\Http\Request; Route::post('/tokens/create', function (Request $request) { $token = $request->user()->createToken($request->token_name); return ['token' => $token->plainTextToken]; })->middleware('auth'); ``` データベースには SHA-256 でハッシュ化されたトークンが保存されます。 ### スコープ(アビリティ)を設定する トークンに対してアビリティ(スコープ)を付与することで、そのトークンで実行できる操作を制限できます。 ```php theme={null} // スコープ付きトークンを発行 $token = $user->createToken('mobile-app', ['server:update', 'server:read']); return $token->plainTextToken; ``` リクエスト処理時にトークンのスコープを確認します。 ```php theme={null} if ($request->user()->tokenCan('server:update')) { // 更新操作を実行 } if ($request->user()->tokenCant('server:update')) { abort(403); } ``` #### ミドルウェアでスコープを確認する `bootstrap/app.php` にミドルウェアエイリアスを登録します。 ```php theme={null} use Laravel\Sanctum\Http\Middleware\CheckAbilities; use Laravel\Sanctum\Http\Middleware\CheckForAnyAbility; ->withMiddleware(function (Middleware $middleware): void { $middleware->alias([ 'abilities' => CheckAbilities::class, // すべてのアビリティを持つ 'ability' => CheckForAnyAbility::class, // いずれかのアビリティを持つ ]); }) ``` ルートにミドルウェアを適用します。 ```php theme={null} // check-status と place-orders の両方を持つトークンのみ許可 Route::get('/orders', function () { // ... })->middleware(['auth:sanctum', 'abilities:check-status,place-orders']); // check-status または place-orders のいずれかを持つトークンを許可 Route::get('/orders', function () { // ... })->middleware(['auth:sanctum', 'ability:check-status,place-orders']); ``` ### トークンの有効期限 デフォルトでは Sanctum トークンに有効期限はありません。`config/sanctum.php` の `expiration` オプションで分単位の有効期限を設定できます。 ```php theme={null} // config/sanctum.php 'expiration' => 525600, // 365日(分) ``` トークンごとに有効期限を指定することもできます。 ```php theme={null} $token = $user->createToken( 'token-name', ['*'], now()->addWeeks(1) // 1週間後に期限切れ )->plainTextToken; ``` 有効期限を設定している場合は、期限切れトークンを定期的に削除するスケジュールを設定します。 ```php theme={null} use Illuminate\Support\Facades\Schedule; Schedule::command('sanctum:prune-expired --hours=24')->daily(); ``` ### トークンを失効させる ```php theme={null} // 全トークンを削除 $user->tokens()->delete(); // 現在のリクエストで使用されているトークンを削除 $request->user()->currentAccessToken()->delete(); // 特定のトークンを削除 $user->tokens()->where('id', $tokenId)->delete(); ``` *** ## SPA 認証 SPA認証はセッションCookieを使うため、トークンを発行・管理する必要がありません。自社フロントエンド(Vue, React, Next.js等)からAPIを呼び出す場合に適しています。 SPA認証を使うには、SPAとAPIが同じトップレベルドメインを共有している必要があります(サブドメインは異なっても構いません)。また、リクエストに `Accept: application/json` ヘッダーと `Referer` または `Origin` ヘッダーを含める必要があります。 ### Sanctum ミドルウェアを有効にする `bootstrap/app.php` で `statefulApi()` ミドルウェアを有効にします。 ```php theme={null} ->withMiddleware(function (Middleware $middleware): void { $middleware->statefulApi(); }) ``` ### ファーストパーティドメインを設定する `config/sanctum.php` の `stateful` オプションにSPAのドメインを設定します。 ```php theme={null} // config/sanctum.php 'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( '%s%s', 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', Sanctum::currentApplicationUrlWithPort() ))), ``` ### CORS の設定 別のサブドメインからAPIを呼び出す場合は、CORS設定が必要です。 ```shell theme={null} php artisan config:publish cors ``` `config/cors.php` で `supports_credentials` を `true` に設定します。 ```php theme={null} // config/cors.php 'supports_credentials' => true, ``` フロントエンドの axios にも設定が必要です。 ```js theme={null} // resources/js/bootstrap.js axios.defaults.withCredentials = true; axios.defaults.withXSRFToken = true; ``` セッションCookieのドメイン設定も忘れずに行います。 ```php theme={null} // config/session.php 'domain' => '.example.com', // 先頭にドットを付ける ``` ### SPA からの認証フロー ```mermaid theme={null} sequenceDiagram participant SPA as SPA フロントエンド participant API as Laravel API participant Session as セッション SPA->>API: GET /sanctum/csrf-cookie API-->>SPA: XSRF-TOKEN Cookie をセット SPA->>API: POST /login
    X-XSRF-TOKEN ヘッダー API->>Session: セッション作成 API-->>SPA: Set-Cookie: laravel_session SPA->>API: GET /api/user
    Cookie: laravel_session API->>Session: セッション確認 Session-->>API: 認証済みユーザー API-->>SPA: ユーザー情報 ``` ログイン前に `/sanctum/csrf-cookie` エンドポイントを叩いてCSRF保護を初期化します。 ```js theme={null} await axios.get('/sanctum/csrf-cookie'); ``` `/login` エンドポイントにPOSTリクエストを送ります。 ```js theme={null} await axios.post('/login', { email: 'user@example.com', password: 'password', }); ``` ログイン後のリクエストはセッションCookieで自動的に認証されます。 ```js theme={null} const response = await axios.get('/api/user'); console.log(response.data); // 認証済みユーザー情報 ``` *** ## 認証済みルートの保護 `auth:sanctum` ミドルウェアをルートに適用すると、未認証のリクエストに対して `401 Unauthorized` が返されます。APIトークン認証・SPA認証の両方をこのミドルウェア1つで処理できます。 ```php theme={null} use Illuminate\Http\Request; // 単一ルートを保護 Route::get('/user', function (Request $request) { return $request->user(); })->middleware('auth:sanctum'); // ルートグループを保護 Route::middleware('auth:sanctum')->group(function () { Route::get('/profile', [ProfileController::class, 'show']); Route::put('/profile', [ProfileController::class, 'update']); Route::get('/posts', [PostController::class, 'index']); }); ``` *** ## 実用例: ログイン API とトークン返却 モバイルアプリ向けのAPIトークン認証を実装する例です。 ```php theme={null} // routes/api.php use App\Models\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; use Illuminate\Validation\ValidationException; Route::post('/sanctum/token', function (Request $request) { $request->validate([ 'email' => ['required', 'email'], 'password' => ['required'], 'device_name' => ['required', 'string'], ]); $user = User::where('email', $request->email)->first(); if (! $user || ! Hash::check($request->password, $user->password)) { throw ValidationException::withMessages([ 'email' => ['提供された認証情報が正しくありません。'], ]); } return response()->json([ 'token' => $user->createToken($request->device_name)->plainTextToken, ]); }); ``` ```php theme={null} // routes/api.php Route::middleware('auth:sanctum')->group(function () { // 現在のユーザー情報を返す Route::get('/user', function (Request $request) { return $request->user(); }); // 現在のトークンを失効させてログアウト Route::post('/logout', function (Request $request) { $request->user()->currentAccessToken()->delete(); return response()->json(['message' => 'ログアウトしました。']); }); // 全デバイスからログアウト Route::post('/logout/all', function (Request $request) { $request->user()->tokens()->delete(); return response()->json(['message' => '全デバイスからログアウトしました。']); }); }); ``` ```js theme={null} // ログイン const { data } = await axios.post('/api/sanctum/token', { email: 'user@example.com', password: 'password', device_name: 'My iPhone', }); const token = data.token; // 認証済みリクエスト const response = await axios.get('/api/user', { headers: { Authorization: `Bearer ${token}`, }, }); ``` *** ## テスト Sanctum のテストでは `Sanctum::actingAs()` を使ってユーザーを認証し、付与するアビリティを指定します。 ```php theme={null} use App\Models\User; use Laravel\Sanctum\Sanctum; test('タスク一覧を取得できる', function () { Sanctum::actingAs( User::factory()->create(), ['view-tasks'] ); $response = $this->get('/api/tasks'); $response->assertOk(); }); test('全アビリティを持つトークンでアクセスできる', function () { Sanctum::actingAs( User::factory()->create(), ['*'] ); $response = $this->get('/api/tasks'); $response->assertOk(); }); ``` ```php theme={null} use App\Models\User; use Laravel\Sanctum\Sanctum; public function test_task_list_can_be_retrieved(): void { Sanctum::actingAs( User::factory()->create(), ['view-tasks'] ); $response = $this->get('/api/tasks'); $response->assertOk(); } ``` *** ## まとめ ```shell theme={null} # Sanctum をインストールしてマイグレーションを実行 php artisan install:api ``` User モデルに `HasApiTokens` トレイトを追加: ```php theme={null} use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable { use HasApiTokens, HasFactory, Notifiable; } ``` ```php theme={null} // トークン発行 $token = $user->createToken('token-name')->plainTextToken; // スコープ付きトークン発行 $token = $user->createToken('token-name', ['read', 'write'])->plainTextToken; // スコープ確認 $user->tokenCan('read'); // true/false $user->tokenCant('write'); // true/false // トークン失効 $user->tokens()->delete(); // 全トークン $request->user()->currentAccessToken()->delete(); // 現在のトークン $user->tokens()->where('id', $id)->delete(); // 特定のトークン ``` * **APIトークン認証**: モバイルアプリ、サードパーティ、CLIツールなど、セッションを持たないクライアントから利用する場合。 * **SPA認証**: 自社のVue/React/Next.jsフロントエンドなど、同じドメイン(またはサブドメイン)上のSPAから利用する場合。よりセキュアでトークン管理が不要。 # Laravel Scout Source: https://kawax.biz/jp/scout Eloquentモデルに全文検索・セマンティック検索機能を追加する公式パッケージ。Meilisearch・Algolia・Typesense・Turbopuffer・データベースエンジンに対応し、モデルオブザーバーによりインデックスを自動同期します。 ## はじめに **Laravel Scout** は、[Eloquentモデル](/jp/eloquent)に全文検索機能を追加するための、シンプルなドライバーベースのソリューションです。モデルオブザーバーを使用して、Eloquentレコードと検索インデックスを自動的に同期します。 Scout には、MySQL / PostgreSQL の全文インデックスと `LIKE` 句を使ってデータベースを直接検索する組み込みの `database` エンジンが含まれており、外部サービスは不要です。大規模なプロダクション環境で、タイポ許容・ファセット検索・ジオサーチが必要な場合は、外部エンジンが役立ちます。 ### 対応エンジン一覧 | エンジン | 特徴 | 用途 | | - | - | - | | `database` | MySQL / PostgreSQL の全文インデックス | ほとんどのアプリ | | `collection` | PHPでフィルタリング(SQLiteも可) | ローカル開発・テスト | | Meilisearch | オープンソース、タイポ許容、高速 | 本番環境 | | Algolia | クラウドSaaS、高度な機能 | 本番環境 | | Typesense | オープンソース、ベクトル検索対応 | 本番環境 | | Turbopuffer | フルテキスト・セマンティック・ハイブリッド検索 | 本番環境 | ```mermaid theme={null} flowchart LR APP["Laravelアプリ
    (Eloquentモデル)"] -->|"save / delete"| SCOUT["Laravel Scout
    (モデルオブザーバー)"] SCOUT -->|"インデックス同期"| ENGINE["検索エンジン
    Meilisearch / Algolia / Typesense / Turbopuffer"] USER["ユーザー"] -->|"Model::search()"| APP ENGINE -->|"検索結果"| APP ``` ## インストール Composerでパッケージをインストールします。 ```shell theme={null} composer require laravel/scout ``` インストール後、`vendor:publish` コマンドで設定ファイルを公開します。`config/scout.php` が生成されます。 ```shell theme={null} php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider" ``` 最後に、検索可能にしたいモデルに `Laravel\Scout\Searchable` トレイトを追加します。このトレイトがモデルオブザーバーを登録し、検索ドライバーとの自動同期を有効にします。 ```php theme={null} true, ``` 接続名とキュー名を指定することもできます。 ```php theme={null} 'queue' => [ 'connection' => 'redis', 'queue' => 'scout' ], ``` 設定後、専用のキューワーカーを起動します。 ```shell theme={null} php artisan queue:work redis --queue=scout ``` ### ユニークなジョブの使用 書き込みの多いアプリケーションでは、同じモデルレコードに対する重複したキュージョブがキューに登録されるのを防ぎたい場合があります。`config/scout.php` で `MakeSearchableUniquely` と `RemoveFromSearchUniquely` ジョブクラスを登録することで、ユニークなインデックスジョブを使用できます。通常、これはサービスプロバイダーの `boot` メソッドで設定します。 ```php theme={null} use Laravel\Scout\Jobs\MakeSearchableUniquely; use Laravel\Scout\Jobs\RemoveFromSearchUniquely; use Laravel\Scout\Scout; Scout::makeSearchableUsing(MakeSearchableUniquely::class); Scout::removeFromSearchUsing(RemoveFromSearchUniquely::class); ``` これらのジョブは Laravel の[ユニークなジョブロック](#ユニークなジョブの使用)を使用して、既にキューに登録されている同じモデルレコードの重複したインデックス操作をディスパッチするのを防ぎます。 ## ドライバーの前提条件 ### Algolia Algoliaドライバーを使用する場合は、`config/scout.php` に `id` と `secret` の認証情報を設定し、Algolia PHP SDKをインストールします。 ```shell theme={null} composer require algolia/algoliasearch-client-php ``` `.env` ファイルに認証情報を追加します。 ```ini theme={null} ALGOLIA_APP_ID=your-app-id ALGOLIA_SECRET=your-secret-key ``` #### インデックス設定 Algoliaでは `config/scout.php` でインデックス設定を管理できます。 ```php theme={null} use App\Models\User; 'algolia' => [ 'id' => env('ALGOLIA_APP_ID', ''), 'secret' => env('ALGOLIA_SECRET', ''), 'index-settings' => [ User::class => [ 'searchableAttributes' => ['id', 'name', 'email'], 'attributesForFaceting' => ['filterOnly(email)'], ], ], ], ``` 設定後、`scout:sync-index-settings` コマンドを実行してAlgoliaに設定を反映させます。 ```shell theme={null} php artisan scout:sync-index-settings ``` ### Meilisearch [Meilisearch](https://www.meilisearch.com) は高速なオープンソース検索エンジンです。ローカル開発では [Laravel Sail](https://laravel.com/docs/sail#meilisearch) のDockerを使うのが最も簡単です。 ```shell theme={null} # Laravel Sailを使う場合 ./vendor/bin/sail up -d meilisearch ``` Sailを使わない場合は、Dockerで直接起動できます。 ```shell theme={null} docker run -it --rm \ -p 7700:7700 \ getmeili/meilisearch:latest \ meilisearch --master-key="masterKey" ``` Meilisearch PHP SDKをインストールします。 ```shell theme={null} composer require meilisearch/meilisearch-php http-interop/http-factory-guzzle ``` `.env` ファイルにドライバーとホストを設定します。 ```ini theme={null} SCOUT_DRIVER=meilisearch MEILISEARCH_HOST=http://127.0.0.1:7700 MEILISEARCH_KEY=masterKey ``` Scoutをアップグレードする際は、Meilisearchサービス自体の[破壊的変更](https://github.com/meilisearch/Meilisearch/releases)も必ず確認してください。 #### インデックス設定(Meilisearch) Meilisearchでは `where()` でフィルタリングするカラムを `filterableAttributes` に、`orderBy()` でソートするカラムを `sortableAttributes` に事前登録する必要があります。 ```php theme={null} use App\Models\User; 'meilisearch' => [ 'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'), 'key' => env('MEILISEARCH_KEY', null), 'index-settings' => [ User::class => [ 'filterableAttributes' => ['id', 'name', 'email'], 'sortableAttributes' => ['created_at'], ], ], ], ``` 数値カラムのデータ型に注意が必要です。Meilisearchは正しい型のデータにのみフィルター操作(`>`、`<` など)を実行できます。 ```php theme={null} public function toSearchableArray(): array { return [ 'id' => (int) $this->id, 'name' => $this->name, 'price' => (float) $this->price, ]; } ``` 設定後、`scout:sync-index-settings` コマンドを実行します。 ```shell theme={null} php artisan scout:sync-index-settings ``` #### セマンティック検索とハイブリッド検索(Meilisearch) Meilisearchでセマンティック検索またはハイブリッド検索を使うには、インデックス設定でembedderを、モデル設定で埋め込み情報を指定します。 ```php theme={null} 'meilisearch' => [ 'index-settings' => [ Article::class => [ 'embedders' => [ 'default' => [ 'source' => 'userProvided', 'dimensions' => 1536, ], ], ], ], 'model-settings' => [ Article::class => [ 'embedding' => [ 'embedder' => 'default', 'dimensions' => 1536, ], ], ], ], ``` モデルの `toSearchableEmbedding` メソッドは、Laravel AI SDKで埋め込む元テキスト、または事前計算済みの埋め込み配列を返します。設定後は `scout:sync-index-settings` を実行してください。 ### Typesense [Typesense](https://typesense.org) は高速なオープンソース検索エンジンで、キーワード検索・セマンティック検索・ジオ検索・ベクトル検索に対応しています。 ```shell theme={null} composer require typesense/typesense-php ``` `.env` ファイルに接続情報を設定します。 ```ini theme={null} SCOUT_DRIVER=typesense TYPESENSE_API_KEY=masterKey TYPESENSE_HOST=localhost TYPESENSE_PORT=8108 TYPESENSE_PATH= TYPESENSE_PROTOCOL=http ``` Typesenseを使用する場合、`toSearchableArray` メソッドでモデルの主キーを文字列に、作成日時をUNIXタイムスタンプにキャストする必要があります。 ```php theme={null} public function toSearchableArray(): array { return array_merge($this->toArray(), [ 'id' => (string) $this->id, 'created_at' => $this->created_at->timestamp, ]); } ``` #### セマンティック検索とハイブリッド検索(Typesense) Typesenseでセマンティック検索・ハイブリッド検索を有効にするには、モデルのTypesense設定で `embedding` 設定とベクターフィールドを定義します。デフォルトではScoutは[Laravel AI SDK](/jp/ai-sdk)を使って埋め込みを生成します。 ```php theme={null} use App\Models\Article; 'model-settings' => [ Article::class => [ 'collection-schema' => [ 'fields' => [ ['name' => 'title', 'type' => 'string'], ['name' => 'embedding', 'type' => 'float[]', 'num_dim' => 1536], ], ], 'search-parameters' => ['query_by' => 'title'], 'embedding' => [ 'attribute' => 'embedding', 'dimensions' => 1536, ], ], ], ``` モデルの `toSearchableEmbedding` メソッドは、Scoutが埋め込む元テキスト、または事前計算済みの埋め込み配列を返します。 ```php theme={null} public function toSearchableEmbedding(): string|array { return $this->title.' '.$this->body; } ``` Typesenseのネイティブ埋め込み機能を使う場合は、Laravel AI SDKなしでも埋め込みを生成できます。詳細は[Typesense公式ドキュメント](https://typesense.org/docs/latest/api/vector-search.html)を参照してください。 ### Turbopuffer [Turbopuffer](https://turbopuffer.com) はフルテキスト・セマンティック・ハイブリッド検索をサポートする検索エンジンです。Turbopufferドライバーを使うには、`SCOUT_DRIVER` とAPIキーを設定します。 ```ini theme={null} SCOUT_DRIVER=turbopuffer TURBOPUFFER_API_KEY=tpuf_... TURBOPUFFER_REGION=gcp-us-central1 ``` `TURBOPUFFER_REGION` は省略可能で、デフォルトは `gcp-us-central1` です。 ### データベース / コレクションエンジン 外部サービスなしで検索を追加したい場合に最適なオプションです。 **データベースエンジン**はMySQL / PostgreSQLの全文インデックスと `LIKE` 句を使用します。ほとんどのアプリケーションでこれで十分です。 ```ini theme={null} SCOUT_DRIVER=database ``` #### セマンティック検索とハイブリッド検索 データベースエンジンは、`pgvector` 拡張を有効にした PostgreSQL でセマンティック検索とハイブリッド検索をサポートします。モデルのテーブルに nullable なベクターカラムと全文インデックスを追加します。Scoutはモデル保存後に埋め込みを保存するため、ベクターカラムは nullable にしてください。 ```php theme={null} Schema::ensureVectorExtensionExists(); Schema::table('articles', function (Blueprint $table) { $table->vector('embedding', dimensions: 1536)->nullable(); $table->vectorIndex('embedding'); $table->fullText(['title', 'body']); }); ``` モデルに `toSearchableEmbedding` メソッドを定義します。このメソッドはScoutが埋め込む元テキスト、または事前計算済みの埋め込み配列を返します。保存先はデフォルトで `embedding` カラムですが、`searchableEmbeddingColumn` メソッドで変更できます。 **コレクションエンジン**はPHPでフィルタリングするため、SQLiteを含むLaravelがサポートするすべてのデータベースで動作します。ローカル開発・テスト・小規模データセット向けです。 ```ini theme={null} SCOUT_DRIVER=collection ``` データベースエンジンでは、外部エンジンとは異なりインデックスの手動管理は不要です。データベーステーブルを直接検索します。 ### Turbopufferの設定 Turbopufferでは、モデルごとに検索可能な属性とスキーマを `config/scout.php` の `model-settings` に定義します。 ```php theme={null} 'turbopuffer' => [ 'model-settings' => [ Article::class => [ 'searchable-attributes' => [ 'title' => 3, 'body' => 1, ], 'schema' => [ 'title' => ['type' => 'string', 'full_text_search' => true], 'body' => ['type' => 'string', 'full_text_search' => true], 'status' => ['type' => 'string'], ], ], ], ], ``` `searchable-attributes` の数値はBM25の相対的な重みです。例では、タイトルの一致が本文の3倍のスコアに寄与します。セマンティック検索を使う場合は `embedding` 設定とベクターのスキーマを追加し、モデルの `toSearchableEmbedding` メソッドから元テキストまたは埋め込み配列を返します。 ```php theme={null} 'embedding' => [ 'attribute' => 'embedding', 'dimensions' => 1536, ], 'schema' => [ 'embedding' => ['type' => '[1536]f32', 'ann' => true], ], ``` Turbopufferのネイティブ埋め込みを使う場合は、Laravel AI SDKや `toSearchableEmbedding` は不要です。埋め込みの元属性を `toSearchableArray` の戻り値に含め、次のように設定します。 ```php theme={null} 'embedding' => [ 'driver' => 'turbopuffer', 'attribute' => 'embedding_text', ], ``` ## Searchable トレイト ### toSearchableArray() のカスタマイズ デフォルトでは、モデルの `toArray()` の全データが検索インデックスに保存されます。インデックスに同期するデータをカスタマイズするには、`toSearchableArray` メソッドをオーバーライドします。 ```php theme={null} */ public function toSearchableArray(): array { $array = $this->toArray(); // データをカスタマイズ... return $array; } } ``` ### インデックス名のカスタマイズ デフォルトでは、モデルのテーブル名(複数形)がインデックス名として使用されます。`searchableAs` メソッドをオーバーライドしてカスタマイズできます。 ```php theme={null} public function searchableAs(): string { return 'posts_index'; } ``` ### データベースエンジン用の検索戦略 データベースエンジンでは、カラムごとに効率的な検索戦略をPHP属性で指定できます。 ```php theme={null} use Laravel\Scout\Attributes\SearchUsingFullText; use Laravel\Scout\Attributes\SearchUsingPrefix; #[SearchUsingPrefix(['id', 'email'])] #[SearchUsingFullText(['bio'])] public function toSearchableArray(): array { return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, 'bio' => $this->bio, ]; } ``` `SearchUsingFullText` を使用する前に、対象カラムに[全文インデックス](/jp/migrations)が付与されていることを確認してください。 ### 条件付きで検索可能にする 特定の条件下でのみモデルを検索可能にしたい場合は、`shouldBeSearchable` メソッドを定義します。 ```php theme={null} /** * このモデルを検索可能にすべきか判断します。 */ public function shouldBeSearchable(): bool { return $this->isPublished(); } ``` `shouldBeSearchable` はデータベースエンジンでは機能しません。データベースエンジンで同様の動作を実現するには、[where句](#フィルタリングとソート)を使用してください。 ## インデックスの管理 このセクションのコマンドは、主にAlgolia・Meilisearch・Typesenseなどのサードパーティエンジンを使用する場合に関係します。データベースエンジンではインデックス管理は不要です。 ### 既存レコードのインポート 既存プロジェクトにScoutを導入する場合、`scout:import` コマンドで既存レコードをインデックスにインポートします。 ```shell theme={null} php artisan scout:import "App\Models\Post" ``` キューを使ってバックグラウンドでインポートすることもできます。 ```shell theme={null} php artisan scout:queue-import "App\Models\Post" --chunk=500 ``` ### インデックスのクリア モデルのすべてのレコードを検索インデックスから削除するには `scout:flush` を使います。 ```shell theme={null} php artisan scout:flush "App\Models\Post" ``` ### インデックスの一時停止 Eloquent操作中に検索インデックスとの同期を一時的に止めたい場合は `withoutSyncingToSearch` を使います。 ```php theme={null} use App\Models\Order; Order::withoutSyncingToSearch(function () { // この中のモデル操作は検索インデックスに同期されない }); ``` ### レコードの手動追加・削除 クエリを使ってモデルのコレクションをインデックスに追加できます。 ```php theme={null} // クエリ結果を追加 Order::where('price', '>', 100)->searchable(); // リレーション経由で追加 $user->orders()->searchable(); ``` インデックスからレコードを削除するには `unsearchable` を使います。 ```php theme={null} Order::where('price', '>', 100)->unsearchable(); ``` モデルを `delete` すると、インデックスからも自動的に削除されます。 ## 検索 `search` メソッドでモデルを検索します。`get` を連結してEloquentモデルのコレクションを取得します。 ```php theme={null} use App\Models\Order; $orders = Order::search('Star Trek')->get(); ``` コントローラーやルートから直接返すとJSONに自動変換されます。 ```php theme={null} use Illuminate\Http\Request; Route::get('/search', function (Request $request) { return Order::search($request->search)->get(); }); ``` 生の検索結果が必要な場合は `raw` メソッドを使います。 ```php theme={null} $orders = Order::search('Star Trek')->raw(); ``` ### セマンティック検索 埋め込みを設定した `database`、Meilisearch、Typesense、Turbopuffer エンジンでは、クエリの意味に基づいてレコードを検索できます。検索クエリに `semantic` メソッドを追加します。Scoutが埋め込みを生成する場合、セマンティック検索・ハイブリッド検索には[Laravel AI SDK](/jp/ai-sdk)が必要です。Typesenseのネイティブ埋め込みとTurbopufferのネイティブ埋め込み、事前計算済みのクエリベクターを使う場合はLaravel AI SDKは不要です。 ```php theme={null} $articles = Article::search('電力なしで家を快適に保つ方法') ->semantic() ->get(); ``` 対応しているエンジンでは、最小類似度の閾値も指定できます。 ```php theme={null} $articles = Article::search('再生可能エネルギーの蓄電') ->semantic(minSimilarity: 0.6) ->get(); ``` 全文検索とセマンティック検索を組み合わせるには `hybrid` メソッドを使います。引数でテキスト検索とセマンティック検索の相対的な重みを指定できます。 ```php theme={null} $articles = Article::search('再生可能エネルギーの蓄電') ->hybrid(textWeight: 1, semanticWeight: 2) ->get(); ``` ### ページネーション `paginate` メソッドで検索結果をページネーションできます。通常のEloquentクエリのページネーションと同様に機能します。 ```php theme={null} $orders = Order::search('Star Trek')->paginate(); // 1ページあたりの件数を指定 $orders = Order::search('Star Trek')->paginate(15); ``` データベースエンジンでは `simplePaginate` も使用できます。総件数を取得しないため大規模データセットで効率的です。 ```php theme={null} $orders = Order::search('Star Trek')->simplePaginate(15); ``` Bladeテンプレートでの表示例: ```html theme={null}
    @foreach ($orders as $order) {{ $order->price }} @endforeach
    {{ $orders->links() }} ``` ## フィルタリングとソート `where` メソッドで検索クエリにフィルター条件を追加できます。 ```php theme={null} use App\Models\Order; // 等値フィルター $orders = Order::search('Star Trek')->where('user_id', 1)->get(); // 配列内のいずれかに一致 $orders = Order::search('Star Trek')->whereIn('status', ['open', 'paid'])->get(); // 配列内のいずれにも一致しない $orders = Order::search('Star Trek')->whereNotIn('status', ['closed'])->get(); ``` Meilisearchを使用する場合、`where` を使う前に[フィルタリング可能な属性](#インデックス設定(meilisearch))を設定する必要があります。 `query` メソッドでEloquentクエリをカスタマイズすることもできます。 ```php theme={null} use Illuminate\Database\Eloquent\Builder; $orders = Order::search('Star Trek') ->query(fn (Builder $query) => $query->with('invoices')) ->get(); ``` ## Eager Loading Scout を使用すると、検索エンジンからIDの一覧を取得した後、Eloquentでモデルを取得します。N+1問題を避けるには、`query` メソッドで `with()` を使ったEager Loadingを指定します。 ```php theme={null} use App\Models\Order; use Illuminate\Database\Eloquent\Builder; $orders = Order::search('Star Trek') ->query(fn (Builder $query) => $query->with(['invoices', 'user'])) ->get(); ``` バッチインポート時にリレーションをEager Loadするには、`makeAllSearchableUsing` メソッドを定義します。 ```php theme={null} use Illuminate\Database\Eloquent\Builder; protected function makeAllSearchableUsing(Builder $query): Builder { return $query->with('author'); } ``` `makeAllSearchableUsing` はキューを使ったバッチインポートでは使用できない場合があります。キュージョブでモデルコレクションが処理されるときにリレーションは復元されません。 ## ソフトデリート インデックスされたモデルが[ソフトデリート](/jp/eloquent)を使用していて、削除済みモデルも検索したい場合は、`config/scout.php` の `soft_delete` オプションを `true` に設定します。 ```php theme={null} 'soft_delete' => true, ``` 有効にすると、`withTrashed` や `onlyTrashed` で削除済みレコードを検索できます。 ```php theme={null} // 削除済みを含めて検索 $orders = Order::search('Star Trek')->withTrashed()->get(); // 削除済みのみ検索 $orders = Order::search('Star Trek')->onlyTrashed()->get(); ``` ## カスタムエンジン 組み込みの検索エンジンがニーズに合わない場合は、独自のカスタムエンジンを実装できます。カスタムエンジンは `Laravel\Scout\Engines\Engine` 抽象クラスを継承し、以下の8つのメソッドを実装する必要があります。 ```php theme={null} use Laravel\Scout\Builder; abstract public function update($models); abstract public function delete($models); abstract public function search(Builder $builder); abstract public function paginate(Builder $builder, $perPage, $page); abstract public function mapIds($results); abstract public function map(Builder $builder, $results, $model); abstract public function getTotalCount($results); abstract public function flush($model); ``` 実装の参考として、`Laravel\Scout\Engines\AlgoliaEngine` クラスを確認してください。 作成したカスタムエンジンは、`App\Providers\AppServiceProvider` の `boot` メソッドでScoutに登録します。 ```php theme={null} use App\ScoutExtensions\MySqlSearchEngine; use Laravel\Scout\EngineManager; public function boot(): void { resolve(EngineManager::class)->extend('mysql', function () { return new MySqlSearchEngine; }); } ``` 登録後、`config/scout.php` でドライバーとして指定します。 ```php theme={null} 'driver' => 'mysql', ``` ## 関連ページ Eloquentモデルの基本的な使い方を確認します。 リレーションの定義とEager Loadingを確認します。 Scoutはキューとあわせてインデックスをバックグラウンドで更新できます。 # 検索 Source: https://kawax.biz/jp/search Laravelの検索機能の全体像。全文検索・ベクター検索・リランキング・Laravel Scoutを用途に合わせて使い分ける方法を解説します。 ## はじめに アプリケーションに検索機能を追加する方法は用途に応じて複数あります。Laravelはキーワード一致から AI を使ったセマンティック検索まで、外部サービス不要で対応できる組み込みツールを提供しています。 ```mermaid theme={null} flowchart TD Q["検索要件"] Q --> A{"キーワード一致で十分?"} A -->|Yes| B["全文検索
    whereFullText"] A -->|No - 意味で一致させたい| C["ベクター検索
    whereVectorSimilarTo"] Q --> D{"モデル自動同期が必要?"} D -->|Yes| E["Laravel Scout
    Searchable トレイト"] Q --> F{"結果の関連度を AI で再順位付け?"} F -->|Yes| G["リランキング
    AI SDK Reranking"] ``` ### 機能比較 | 機能 | 外部サービス | 特徴 | | - | - | - | | `whereFullText` | 不要 | MariaDB / MySQL / PostgreSQL 組み込みの全文インデックス | | `whereVectorSimilarTo` | 不要(PostgreSQL + pgvector / MariaDB 11.7+) | 意味的な類似度で検索。AI SDKが必要 | | `Reranking` | AI プロバイダー | 任意の結果セットを AI で関連度順に並び替え | | Laravel Scout | 不要(database エンジン)/ 任意 | Eloquent モデルの自動インデックス同期 | *** ## 全文検索 `LIKE` クエリはシンプルな部分一致には便利ですが、言語を理解しません。全文検索は専用インデックスを使って単語の境界・語形変化・関連度スコアを考慮した検索を行います。 MariaDB・MySQL・PostgreSQL は全文検索を組み込みでサポートしており、外部サービスは不要です。 全文検索は MariaDB・MySQL・PostgreSQL でサポートされています。 ### 全文インデックスの追加 全文検索を使うには、まず対象カラムに全文インデックスを追加します。 ```php theme={null} Schema::create('articles', function (Blueprint $table) { $table->id(); $table->string('title'); $table->text('body'); $table->timestamps(); $table->fullText(['title', 'body']); // 複合インデックス }); ``` PostgreSQL ではインデックスに言語設定を指定できます。英語以外の言語では語形変化の処理が変わります。 ```php theme={null} $table->fullText('body')->language('english'); ``` インデックスの詳細は[マイグレーション](/jp/migrations)を参照してください。 **日本語全文検索の注意事項**\ MySQL での日本語全文検索はやや使いにくい面があります。AWS RDS では N-gram パーサーのみ利用可能で、形態素解析プラグイン(MeCab)を追加できないため制限されます。Laravel Cloud の MySQL も同様と考えられます。PostgreSQL でも日本語の語形変化対応は限定的です。日本語検索の精度が重要な場合は Meilisearch や Typesense などの専用エンジンの利用を検討してください。 ### 全文クエリの実行 インデックスを追加したら `whereFullText` メソッドで検索します。Laravel は使用しているデータベースドライバーに合わせて適切な SQL を生成します(MariaDB / MySQL では `MATCH(...) AGAINST(...)`、PostgreSQL では `to_tsvector(...) @@ plainto_tsquery(...)`)。 ```php theme={null} $articles = Article::whereFullText('body', 'web developer')->get(); ``` 複合インデックスを作成した場合は、同じカラム配列を渡します。 ```php theme={null} $articles = Article::whereFullText( ['title', 'body'], 'web developer' )->get(); ``` MariaDB・MySQL では結果が自動的に関連度スコア順に並びます。PostgreSQL では `whereFullText` はマッチするレコードをフィルタリングするだけで関連度順にはなりません。PostgreSQL で自動的な関連度順が必要な場合は [Scout のデータベースエンジン](#laravel-scout) が有効です。 `orWhereFullText` メソッドで OR 条件として全文検索を追加することもできます。詳細は[クエリビルダードキュメント](/jp/query-builder)を参照してください。 *** ## セマンティック / ベクター検索 全文検索はキーワードの一致に依存しています。ベクター検索は根本的に異なるアプローチで、AI が生成したベクター埋め込みを使ってテキストの*意味*を数値配列として表現し、意味的に近い結果を検索します。 たとえば「best wineries in Napa Valley」という検索で「Top Vineyards to Visit」というタイトルの記事がヒットします。単語は一致しなくても意味が近いからです。 ベクター検索には [Laravel AI SDK](/jp/ai-sdk) が必要で、PostgreSQL(`pgvector` 拡張が必要)、MariaDB 11.7以降、MongoDB(Laravel MongoDB パッケージが必要)をサポートしています。[Laravel Cloud](https://laravel.com/cloud) のすべての Postgres データベースには `pgvector` が事前にインストールされています。 ### 埋め込みの生成 埋め込みはテキストの意味を表す高次元の数値配列(通常数百〜数千次元)です。Laravel の `Stringable` クラスの `toEmbeddings` メソッドで生成できます。 ```php theme={null} use Illuminate\Support\Str; $embedding = Str::of('Napa Valley has great wine.')->toEmbeddings(); ``` 複数のテキストを一度に処理する場合は `Embeddings` クラスを使うと効率的です(API 呼び出しが 1 回で済みます)。 ```php theme={null} use Laravel\Ai\Embeddings; $response = Embeddings::for([ 'Napa Valley has great wine.', 'Laravel is a PHP framework.', ])->generate(); $response->embeddings; // [[0.123, 0.456, ...], [0.789, 0.012, ...]] ``` 埋め込みプロバイダーの設定や次元数のカスタマイズについては [AI SDK ドキュメント](/jp/ai-sdk)を参照してください。 ### ベクターの保存とインデックス作成 ベクター埋め込みを保存するには、マイグレーションで `vector` カラムを定義します。埋め込みプロバイダーの出力次元数に合わせて `dimensions` を指定してください(例:OpenAI の `text-embedding-3-small` は 1536 次元)。`index()` を呼び出して HNSW インデックスを作成すると、大規模データセットでの類似検索が大幅に高速化されます。 ```php theme={null} Schema::ensureVectorExtensionExists(); // pgvector 拡張を有効化 Schema::create('documents', function (Blueprint $table) { $table->id(); $table->string('title'); $table->text('content'); $table->vector('embedding', dimensions: 1536)->index(); $table->timestamps(); }); ``` Eloquent モデルでは `AsVector` キャストを使って、PHP 配列とデータベースのベクター形式の変換を自動化します。 ```php theme={null} use Illuminate\Database\Eloquent\Casts\AsVector; protected function casts(): array { return [ 'embedding' => AsVector::class, ]; } ``` ベクターカラムとインデックスの詳細は[マイグレーション](/jp/migrations)を参照してください。 ### 類似度による検索 埋め込みを保存したら、`whereVectorSimilarTo` メソッドで類似レコードを検索できます。コサイン類似度でベクターを比較し、`minSimilarity` 閾値以下の結果をフィルタリングして、関連性の高い順に自動ソートします。閾値は `0.0`〜`1.0` の値で、`1.0` は完全一致を意味します。 ```php theme={null} $documents = Document::query() ->whereVectorSimilarTo('embedding', $queryEmbedding, minSimilarity: 0.4) ->limit(10) ->get(); ``` 埋め込み配列の代わりに文字列を渡すと、Laravel が自動的に埋め込みを生成してくれます。ユーザーの検索クエリをそのまま渡せるのでとても便利です。 ```php theme={null} $documents = Document::query() ->whereVectorSimilarTo('embedding', 'best wineries in Napa Valley') ->limit(10) ->get(); ``` より細かい制御が必要な場合は `whereVectorDistanceLessThan`・`selectVectorDistance`・`orderByVectorDistance` も利用できます。詳細は[クエリビルダードキュメント](/jp/query-builder)と [AI SDK ドキュメント](/jp/ai-sdk)を参照してください。 *** ## リランキング リランキングは、AI モデルが結果セットをクエリとの関連度順に並び替える手法です。ベクター検索と異なり、事前に埋め込みを計算・保存する必要はなく、任意のテキストコレクションに適用できます。 全文検索で大量のレコードを素早く絞り込んだ後にリランキングを適用する「**検索→リランク**」パターンが特に効果的です。データベースの速さと AI の精度を両立できます。 ```php theme={null} use Laravel\Ai\Reranking; $response = Reranking::of([ 'Django is a Python web framework.', 'Laravel is a PHP web application framework.', 'React is a JavaScript library for building user interfaces.', ])->rerank('PHP frameworks'); $response->first()->document; // "Laravel is a PHP web application framework." ``` Laravel コレクションには `rerank` マクロがあり、フィールド名(またはクロージャ)とクエリを渡すだけで Eloquent の結果をリランクできます。 ```php theme={null} $articles = Article::all() ->rerank('body', 'Laravel tutorials'); ``` リランキングプロバイダーの設定と利用可能なオプションについては [AI SDK ドキュメント](/jp/ai-sdk)を参照してください。 *** ## Laravel Scout ここまで説明した検索手法はすべてクエリビルダーのメソッドを直接呼び出すものでした。[Laravel Scout](/jp/scout) は別アプローチを取ります。Eloquent モデルに `Searchable` トレイトを追加すると、Scout がレコードの作成・更新・削除に合わせて検索インデックスを自動同期します。 ### データベースエンジン Scout 組み込みのデータベースエンジンは既存のデータベースに対して全文検索と `LIKE` 検索を行います。外部サービスもインフラも追加不要です。 `toSearchableArray` メソッドで検索対象のカラムを定義します。PHP 属性でカラムごとに検索戦略を指定できます。 ```php theme={null} $this->id, 'title' => $this->title, 'body' => $this->body, ]; } } ``` | 属性 | 検索戦略 | | - | - | | `SearchUsingFullText` | 全文インデックスを使用(`MATCH...AGAINST` / `to_tsvector`) | | `SearchUsingPrefix` | 前方一致(`example%`) | | なし | 前後ワイルドカード(`%example%`) | `SearchUsingFullText` を指定するカラムには事前に[全文インデックス](/jp/migrations)が必要です。 トレイトを追加したら、`search` メソッドでモデルを検索できます。Scout のデータベースエンジンは PostgreSQL でも自動的に関連度順で結果を返します。 ```php theme={null} $articles = Article::search('Laravel')->get(); ``` ### サードパーティエンジン Scout は [Algolia](https://www.algolia.com/)・[Meilisearch](https://www.meilisearch.com)・[Typesense](https://typesense.org) などのサードパーティ検索エンジンもサポートします。これらはタイポ許容・ファセット検索・ジオサーチ・カスタムランキングなど高度な機能を提供します。 Scout は統一 API を提供するため、後でデータベースエンジンからサードパーティエンジンに移行する際のコード変更は最小限で済みます。 ほとんどのアプリケーションは外部検索エンジンを必要としません。このページで説明した組み込み手法が大多数のユースケースをカバーします。 Scout の詳細は [Laravel Scout ガイド](/jp/scout)を参照してください。 *** ## 技術の組み合わせ ここまで紹介した検索手法は相互に排他的ではありません。組み合わせることで最良の結果が得られます。 ### 全文検索 + リランキング 全文検索で候補を素早く絞り込み、リランキングでセマンティックな関連度順に並び替えます。データベース速度と AI 精度を両立するパターンです。 ```php theme={null} $articles = Article::query() ->whereFullText('body', $request->input('query')) ->limit(50) ->get() ->rerank('body', $request->input('query'), limit: 10); ``` ### ベクター検索 + 通常のフィルター ベクター類似検索と通常の `where` 句を組み合わせて、特定のサブセットに絞ったセマンティック検索を実現します。チームやカテゴリーで絞り込みながら意味的な検索をしたい場合に便利です。 ```php theme={null} $documents = Document::query() ->where('team_id', $user->team_id) ->whereVectorSimilarTo('embedding', $request->input('query')) ->limit(10) ->get(); ``` *** ## 関連ページ Searchable トレイトでモデルを自動インデックス同期する完全ガイド。 ベクター埋め込みとリランキングに必要な AI SDK の設定方法。 whereFullText・whereVectorSimilarTo などのクエリビルダーメソッドの詳細。 全文インデックスとベクターカラムの作成方法。 # データベースシーディング Source: https://kawax.biz/jp/seeding Laravelのシーディング機能を使って開発・テスト用のサンプルデータをデータベースに投入する方法を解説します。 ## シーディングとは シーディング(Seeding)はデータベースにサンプルデータや初期データを投入する仕組みです。 開発環境のセットアップや自動テストでは、あらかじめデータが入っている状態が必要になります。毎回手動でデータを入力するのは手間がかかるため、シーダーを使って繰り返し実行できる形にまとめます。 シーダークラスは `database/seeders` ディレクトリに格納されます。デフォルトで `DatabaseSeeder` クラスが用意されています。 ```mermaid theme={null} flowchart TD A["php artisan db:seed"] --> B["DatabaseSeeder::run()"] B --> C["UserSeeder::run()"] B --> D["PostSeeder::run()"] C --> E["usersテーブルにデータ挿入"] D --> F["postsテーブルにデータ挿入"] ``` ## シーダーの作成 `make:seeder` Artisanコマンドで新しいシーダークラスを生成します。 ```shell theme={null} php artisan make:seeder UserSeeder ``` 生成されたファイルは `database/seeders/UserSeeder.php` に配置されます。 ```php theme={null} insert([ [ 'name' => '山田太郎', 'email' => 'taro@example.com', 'password' => Hash::make('password'), 'created_at' => now(), 'updated_at' => now(), ], [ 'name' => '鈴木花子', 'email' => 'hanako@example.com', 'password' => Hash::make('password'), 'created_at' => now(), 'updated_at' => now(), ], ]); } } ``` ### Eloquentモデルを使う ```php theme={null} use App\Models\User; use Illuminate\Support\Facades\Hash; public function run(): void { User::create([ 'name' => '管理者', 'email' => 'admin@example.com', 'password' => Hash::make('password'), ]); } ``` シーディング実行中はマスアサインメント保護が自動的に無効になります。`$fillable` や `$guarded` の設定を気にせずデータを挿入できます。 ## モデルファクトリーとの組み合わせ 大量のテストデータが必要な場合は[モデルファクトリー](/jp/eloquent)との組み合わせが便利です。ファクトリーを使うとランダムなダミーデータを一括生成できます。 ```php theme={null} use App\Models\User; use App\Models\Post; public function run(): void { // 10人のユーザーを作成 User::factory(10)->create(); // 各ユーザーに3件の投稿を作成 User::factory(5) ->hasPosts(3) ->create(); } ``` ファクトリーの詳細な使い方はEloquentファクトリーのドキュメントを参照してください。 ## DatabaseSeeder の使い方 `DatabaseSeeder` は複数のシーダーをまとめて管理するエントリーポイントです。`call()` メソッドで実行するシーダーを指定します。 ```php theme={null} call([ UserSeeder::class, PostSeeder::class, CommentSeeder::class, ]); } } ``` `call()` に渡した順序でシーダーが実行されます。外部キー制約がある場合は、参照先のテーブルを先にシードする順序にしてください(例:`users` → `posts`)。 ## シーダーの実行 ### 全シーダーを実行する ```shell theme={null} php artisan db:seed ``` `DatabaseSeeder` が呼び出され、そこから `call()` で指定したシーダーが順番に実行されます。 ### 特定のシーダーだけ実行する `--class` オプションで実行するシーダークラスを指定できます。 ```shell theme={null} php artisan db:seed --class=UserSeeder ``` ## マイグレーションと同時に実行する `migrate:fresh` コマンドに `--seed` オプションを付けると、全テーブルを再作成してからシーディングを一括実行できます。 ```shell theme={null} php artisan migrate:fresh --seed ``` 特定のシーダーだけ実行したい場合は `--seeder` オプションを使います。 ```shell theme={null} php artisan migrate:fresh --seed --seeder=UserSeeder ``` `migrate:fresh` はすべてのテーブルを削除して作り直します。既存のデータはすべて失われるため、本番環境では使用しないでください。 ## 本番環境での実行 本番環境でシーディングを実行しようとすると確認プロンプトが表示されます。確認なしで実行するには `--force` フラグを使います。 ```shell theme={null} php artisan db:seed --force ``` 本番環境でのシーディングはデータの上書きや損失につながる可能性があります。実行前に必ずバックアップを取ってください。 ## モデルイベントの抑制 シーディング中にモデルのイベント(`creating`、`created` など)が発火するのを防ぎたい場合は、`WithoutModelEvents` トレイトを使います。 ```php theme={null} call([ UserSeeder::class, ]); } } ``` `call()` 経由で実行される子シーダーにも適用されます。 ## 実践例:ブログアプリのシーダー ユーザーと投稿を持つブログアプリの初期データをセットアップする例です。 ```php theme={null} // database/seeders/UserSeeder.php class UserSeeder extends Seeder { public function run(): void { User::factory(10)->create(); } } // database/seeders/PostSeeder.php class PostSeeder extends Seeder { public function run(): void { // 各ユーザーに2〜5件の投稿を作成 User::all()->each(function ($user) { Post::factory(rand(2, 5))->create([ 'user_id' => $user->id, ]); }); } } // database/seeders/DatabaseSeeder.php class DatabaseSeeder extends Seeder { public function run(): void { $this->call([ UserSeeder::class, PostSeeder::class, // usersの後に実行 ]); } } ``` 実行手順: ```shell theme={null} php artisan migrate:fresh --seed ``` ## 次のステップ シーダーで投入したデータをEloquent ORMで取得・操作する方法を学びます。 # サービスコンテナ Source: https://kawax.biz/jp/service-container Laravelのサービスコンテナを使った依存性注入の仕組みと、バインディングの基本を解説します。 ## サービスコンテナとは Laravelのサービスコンテナは、クラスの依存関係を管理し、依存性注入を行うための仕組みです。依存性注入とは、クラスが必要とする依存をコンストラクターや、場合によってはセッターメソッドを通じてクラスへ「注入」することを指します。 次の例を見てみましょう。 ```php theme={null} $this->apple->findPodcast($id) ]); } } ``` この例では、`PodcastController` はApple MusicなどのデータソースからPodcastを取得する必要があります。そこで、Podcastを取得できるサービスを**注入**します。サービスを注入することで、テスト時に`AppleMusic`サービスのモック(ダミー実装)を簡単に差し替えられます。 サービスコンテナを深く理解することは、大規模なLaravelアプリケーションを構築するうえで不可欠です。Laravelコア自体への貢献にも役立ちます。 ```mermaid theme={null} flowchart TD A["サービスプロバイダ
    register()"] --> B["サービスコンテナ
    バインディング登録"] B --> C{"解決要求
    make() / 自動注入"} C -- "具体クラス" --> D["リフレクションで
    自動解決"] C -- "インターフェース" --> E["登録済み実装クラス
    を解決"] D --> F["インスタンス生成
    コンストラクタへ注入"] E --> F ``` ## ゼロコンフィギュレーション解決 クラスが他の具体クラス(インターフェースではない)にしか依存していない場合、コンテナにその解決方法を教える必要はありません。例えば、次のコードを`routes/web.php`に書いたとします。 ```php theme={null} この例はルートファイル内でクラスを定義していますが、これはデモ用です。実際のアプリケーションでは、サービスクラスは `app/Services` ディレクトリに定義してください。 このルートにアクセスすると、Laravelは自動的に`Service`クラスを解決してルートハンドラーへ注入します。設定ファイルを用意しなくても依存性注入の恩恵を受けられます。 コントローラー、イベントリスナー、ミドルウェアなど、Laravelアプリケーションで書くクラスの多くは、コンテナを通じて自動的に依存が注入されます。 ## バインディング ### 基本的なバインディング ほとんどのバインディングは[サービスプロバイダ](/jp/service-providers)内で登録します。サービスプロバイダ内では `$this->app` プロパティを通じてコンテナにアクセスできます。 #### bind `bind` メソッドを使って、クラスまたはインターフェース名とクロージャを渡してバインディングを登録します。 ```php theme={null} use App\Services\Transistor; use App\Services\PodcastParser; use Illuminate\Contracts\Foundation\Application; $this->app->bind(Transistor::class, function (Application $app) { return new Transistor($app->make(PodcastParser::class)); }); ``` クロージャの引数としてコンテナ自身を受け取ることができます。これを使ってサブ依存を解決できます。 サービスプロバイダの外でコンテナを操作したい場合は `App` ファサードを使います。 ```php theme={null} use App\Services\Transistor; use Illuminate\Contracts\Foundation\Application; use Illuminate\Support\Facades\App; App::bind(Transistor::class, function (Application $app) { // ... }); ``` インターフェースに依存しないクラスはコンテナへバインドする必要はありません。コンテナはリフレクションを使ってこれらのオブジェクトを自動的に解決できます。 #### singleton `singleton` メソッドは、クラスまたはインターフェースを一度だけ解決するようにバインドします。一度解決されたシングルトンは、以降のコンテナへの呼び出しで同じインスタンスが返されます。 ```php theme={null} use App\Services\Transistor; use App\Services\PodcastParser; use Illuminate\Contracts\Foundation\Application; $this->app->singleton(Transistor::class, function (Application $app) { return new Transistor($app->make(PodcastParser::class)); }); ``` `singletonIf` メソッドを使うと、指定した型に対するバインディングがまだ登録されていない場合のみシングルトンバインディングを登録できます。 ```php theme={null} $this->app->singletonIf(Transistor::class, function (Application $app) { return new Transistor($app->make(PodcastParser::class)); }); ``` #### Singleton属性 クラスやインターフェースに `#[Singleton]` 属性を付けることでも、一度だけ解決されるようコンテナに指示できます。 ```php theme={null} app->scoped(Transistor::class, function (Application $app) { return new Transistor($app->make(PodcastParser::class)); }); ``` `scopedIf` メソッドを使うと、指定した型に対するバインディングがまだ登録されていない場合のみスコープ付きバインディングを登録できます。 ```php theme={null} $this->app->scopedIf(Transistor::class, function (Application $app) { return new Transistor($app->make(PodcastParser::class)); }); ``` #### Scoped属性 クラスやインターフェースに `#[Scoped]` 属性を付けることでも、リクエスト/ジョブのライフサイクル内で一度だけ解決されるようコンテナに指示できます。 ```php theme={null} app->instance(Transistor::class, $service); ``` ### インターフェースを実装にバインドする サービスコンテナの強力な機能の一つは、インターフェースを特定の実装にバインドできることです。例えば、`EventPusher` インターフェースと `RedisEventPusher` 実装があるとします。 ```php theme={null} use App\Contracts\EventPusher; use App\Services\RedisEventPusher; $this->app->bind(EventPusher::class, RedisEventPusher::class); ``` これにより、コンテナは `EventPusher` の実装が必要なクラスに `RedisEventPusher` を注入するようになります。あとはコンストラクターで `EventPusher` インターフェースをタイプヒントするだけです。 ```php theme={null} use App\Contracts\EventPusher; /** * 新しいクラスインスタンスを生成する */ public function __construct( protected EventPusher $pusher, ) {} ``` インターフェースに依存することで、実装を差し替えてもコードを変更する必要がなくなります。これにより、テストや将来の変更が容易になります。 #### Bind属性 Laravelはさらに便利な `Bind` 属性も提供しています。インターフェースにこの属性を付けることで、そのインターフェースが要求されたときにどの実装を自動的に注入するかをLaravelに伝えられます。`Bind` 属性を使う場合、サービスプロバイダで追加の登録処理を行う必要はありません。 さらに、インターフェースに複数の `Bind` 属性を配置することで、環境ごとに異なる実装を注入するよう設定することもできます。 ```php theme={null} Feature::active('beta-events'))] interface EventPusher { // ... } ``` `BindWhen` 属性を使うには PHP 8.5 以上が必要です。 また、[Singleton](#singleton属性)属性や[Scoped](#scoped属性)属性を併用することで、そのコンテナバインディングを一度だけ解決するか、リクエスト/ジョブごとに一度だけ解決するかを指定できます。 ```php theme={null} use App\Services\RedisEventPusher; use Illuminate\Container\Attributes\Bind; use Illuminate\Container\Attributes\Singleton; #[Bind(RedisEventPusher::class)] #[Singleton] interface EventPusher { // ... } ``` ### コンテキストバインディング 同じインターフェースを利用する2つのクラスに、それぞれ異なる実装を注入したい場合があります。例えば、2つのコントローラーが `Illuminate\Contracts\Filesystem\Filesystem` [コントラクト](/jp/contracts)の異なる実装に依存しているとします。Laravelはこの挙動を定義するためのシンプルで流暢なインターフェースを提供しています。 ```php theme={null} use App\Http\Controllers\PhotoController; use App\Http\Controllers\UploadController; use App\Http\Controllers\VideoController; use Illuminate\Contracts\Filesystem\Filesystem; use Illuminate\Support\Facades\Storage; $this->app->when(PhotoController::class) ->needs(Filesystem::class) ->give(function () { return Storage::disk('local'); }); $this->app->when([VideoController::class, UploadController::class]) ->needs(Filesystem::class) ->give(function () { return Storage::disk('s3'); }); ``` `when()` にはクラスまたはクラスの配列を渡せます。`needs()` で対象のインターフェース、`give()` で解決に使うクロージャを指定します。これにより、同じ `Filesystem` インターフェースを型ヒントしていても、コントローラーごとに異なるディスクを注入できます。 #### コンテキスト属性 コンテキストバインディングは、ドライバーや設定値の実装を注入する目的でよく使われるため、Laravelはサービスプロバイダで手動でコンテキストバインディングを定義しなくても済むよう、さまざまなコンテキストバインディング属性を用意しています。 例えば、`Storage` 属性を使うと特定の[ストレージディスク](/jp/filesystem)を注入できます。 ```php theme={null} `Give` 属性は、インターフェースを具象クラスに解決する際に、通常の型ヒントによる自動解決の代わりに特定の実装を指定したい場合に使用します。 ## 自動解決(型ヒントによるDI) サービスコンテナは、コントローラー、イベントリスナー、ミドルウェアなどのクラスを解決する際に、コンストラクターの型ヒントを見て依存を自動的に注入します。 ```php theme={null} make(Transistor::class); ``` クラスの依存がコンテナで解決できない場合、`makeWith` メソッドを使って追加の引数を渡すこともできます。 ```php theme={null} $transistor = $this->app->makeWith(Transistor::class, ['id' => 1]); ``` ### 自動注入 実際には、`make` メソッドを直接呼び出すことはほとんどありません。コンテナが解決するクラス(コントローラー、イベントリスナー、ミドルウェアなど)のコンストラクターに型ヒントを追加するだけで、コンテナが自動的に注入してくれます。 ## ファサードとコンテナの関係 Laravelのファサードは、コンテナ内のオブジェクトへの静的インターフェースを提供します。例えば、`Cache::get()` は内部的にコンテナから `Cache` サービスを取得して呼び出しています。 ```php theme={null} use Illuminate\Support\Facades\Cache; // ファサードを使った呼び出し Cache::get('key'); // コンテナを直接使った同等の呼び出し app('cache')->get('key'); ``` ファサードはコンテナの便利なラッパーです。テスト時にはファサードをモックに差し替えることもできます。 ```php theme={null} use Illuminate\Support\Facades\Cache; Cache::shouldReceive('get') ->once() ->with('key') ->andReturn('value'); ``` ## コンストラクターインジェクションの実践例 実際のアプリケーションでの典型的なパターンを見てみましょう。 ```php theme={null} ```php theme={null} ```php theme={null} use App\Contracts\PaymentGateway; use App\Services\StripePaymentGateway; $this->app->singleton(PaymentGateway::class, StripePaymentGateway::class); ``` ```php theme={null} payment->charge( $request->amount, $request->payment_token ); // ... } } ``` このパターンにより、決済サービスを`Stripe`から別のプロバイダに切り替える場合も、バインディングを1か所変更するだけで対応できます。 ## 次のステップ サービスプロバイダを使ってバインディングを登録する方法を学びます。 # サービスプロバイダ Source: https://kawax.biz/jp/service-providers Laravelのサービスプロバイダを使ってアプリケーションのサービスを登録・起動する方法を解説します。 ## サービスプロバイダとは サービスプロバイダは、Laravelアプリケーション全体のブートストラップの中心となる場所です。あなた自身のアプリケーションも、Laravelのすべてのコアサービスも、サービスプロバイダを通じてブートストラップされます。 「ブートストラップ」とは、サービスコンテナへのバインディング、イベントリスナー、ミドルウェア、ルートの登録など、さまざまなものを**登録**することを意味します。サービスプロバイダはアプリケーションの設定を行う中心的な場所です。 ```mermaid theme={null} flowchart TD A["アプリケーション起動"] --> B["bootstrap/providers.phpを読み込む"] B --> C["全プロバイダをインスタンス化"] C --> D["全プロバイダのregister()を実行
    サービスコンテナへのバインディング登録のみ"] D --> E["全プロバイダのboot()を実行
    ビューコンポーザー・イベントリスナー等"] E --> F["アプリケーション準備完了
    リクエスト処理開始"] ``` Laravelは内部的に、メーラー、キュー、キャッシュなどのコアサービスをブートストラップするために多くのサービスプロバイダを使用しています。これらのプロバイダの多くは「遅延」プロバイダで、すべてのリクエストでロードされるのではなく、提供するサービスが実際に必要になった時だけロードされます。 ユーザー定義のサービスプロバイダはすべて `bootstrap/providers.php` ファイルで登録します。 Laravelがリクエストをどのように処理するかについて詳しく知りたい場合は、[リクエストライフサイクル](https://laravel.com/docs/lifecycle)のドキュメントを参照してください。 ## サービスプロバイダを書く すべてのサービスプロバイダは `Illuminate\Support\ServiceProvider` クラスを継承します。ほとんどのサービスプロバイダには `register` メソッドと `boot` メソッドが含まれます。 新しいプロバイダを生成するには、`make:provider` Artisanコマンドを使います。Laravelは自動的に新しいプロバイダを `bootstrap/providers.php` に登録します。 ```shell theme={null} php artisan make:provider RiakServiceProvider ``` ### registerメソッド `register` メソッド内では、[サービスコンテナ](/jp/service-container)へのバインディングのみを行ってください。イベントリスナー、ルート、その他の機能を `register` メソッド内で登録しようとしないでください。まだロードされていないサービスプロバイダが提供するサービスを誤って使用してしまう可能性があります。 ```php theme={null} app->singleton(Connection::class, function (Application $app) { return new Connection(config('riak')); }); } } ``` サービスプロバイダのメソッド内では、常に `$this->app` プロパティを通じてサービスコンテナにアクセスできます。 #### bindingsプロパティとsingletonsプロパティ 多くのシンプルなバインディングを登録する場合、各バインディングを手動で登録する代わりに、`bindings` プロパティと `singletons` プロパティを使うことができます。フレームワークがサービスプロバイダをロードすると、これらのプロパティを自動的にチェックしてバインディングを登録します。 ```php theme={null} DigitalOceanServerProvider::class, ]; /** * 登録するすべてのコンテナシングルトン * * @var array */ public $singletons = [ DowntimeNotifier::class => PingdomDowntimeNotifier::class, ServerProvider::class => ServerToolsProvider::class, ]; } ``` ### bootメソッド サービスプロバイダ内で[ビューコンポーザー](https://laravel.com/docs/views#view-composers)を登録する場合は `boot` メソッドで行います。**このメソッドは他のすべてのサービスプロバイダが登録された後に呼び出されます**。つまり、フレームワークが登録した他のすべてのサービスにアクセスできます。 ```php theme={null} `register` メソッドと `boot` メソッドの役割を混同しないでください。`register` はバインディングの登録のみ、`boot` はサービスの初期化やその他のセットアップに使います。 #### bootメソッドの依存性注入 `boot` メソッドでも型ヒントを使って依存を注入できます。[サービスコンテナ](/jp/service-container)が必要な依存を自動的に注入します。 ```php theme={null} use Illuminate\Contracts\Routing\ResponseFactory; /** * アプリケーションサービスを起動する */ public function boot(ResponseFactory $response): void { $response->macro('serialized', function (mixed $value) { // ... }); } ``` ## サービスプロバイダの登録方法 すべてのサービスプロバイダは `bootstrap/providers.php` ファイルで登録します。このファイルはアプリケーションのサービスプロバイダのクラス名の配列を返します。 ```php theme={null} Laravel 13では、`config/app.php` の `providers` 配列ではなく、`bootstrap/providers.php` でサービスプロバイダを登録します。`make:provider` コマンドを使うと自動的に追加されます。 `make:provider` Artisanコマンドを実行すると、Laravelは自動的にファイルへプロバイダを追加します。手動でプロバイダクラスを作成した場合は、手動でクラスを配列に追加してください。 ```php theme={null} Artisanコマンドでサービスプロバイダを生成します。 ```shell theme={null} php artisan make:provider PaymentServiceProvider ``` 生成されたプロバイダの `register` メソッドにバインディングを記述します。 ```php theme={null} app->singleton(PaymentGateway::class, function (Application $app) { return new StripePaymentGateway( config('services.stripe.secret') ); }); } /** * アプリケーションサービスを起動する */ public function boot(): void { // 起動時の処理があればここに書く } } ``` `make:provider` を使った場合は自動登録されます。手動の場合は追加してください。 ```php theme={null} ## 遅延プロバイダ(Deferred Providers) プロバイダがサービスコンテナへのバインディングのみを登録している場合、登録されたバインディングが実際に必要になるまでその登録を遅延させることができます。このような遅延プロバイダのロードを遅らせることで、すべてのリクエストでファイルシステムからロードされなくなり、アプリケーションのパフォーマンスが向上します。 ```mermaid theme={null} flowchart TD A["アプリケーション起動"] --> B["通常プロバイダをロード・初期化"] B --> C["遅延プロバイダは
    提供サービスのリストのみ記録"] C --> D["リクエスト処理"] D --> E{{"遅延プロバイダの
    サービスが要求された?"}} E -->|"No"| F["プロバイダはロードされない
    メモリ・処理を節約"] E -->|"Yes"| G["プロバイダをその場でロード"] G --> H["register()を実行"] H --> I["サービスをコンテナから提供"] ``` 遅延プロバイダを作成するには、`\Illuminate\Contracts\Support\DeferrableProvider` インターフェースを実装し、`provides` メソッドを定義します。`provides` メソッドはプロバイダが登録するサービスコンテナのバインディングを返します。 ```php theme={null} app->singleton(Connection::class, function (Application $app) { return new Connection(config('riak')); }); } /** * プロバイダが提供するサービスを取得する * * @return array */ public function provides(): array { return [Connection::class]; } } ``` 遅延プロバイダは、アプリケーション全体ではなく特定の機能でのみ使うサービスに適しています。不必要なサービスのロードを避けることでパフォーマンスを最適化できます。 ## 次のステップ サービスコンテナの仕組みとバインディングの詳細を確認します。 # セッション Source: https://kawax.biz/jp/session LaravelのHTTPセッションを使ってリクエスト間でデータを保持する方法 ## セッションとは HTTPはステートレスなプロトコルです。つまり、リクエストをまたいでユーザーの情報を保持する仕組みがHTTP自体にはありません。 Laravelのセッション機能は、複数のリクエストにわたってユーザーのデータを保持する手段を提供します。 ログイン状態、カート内の商品、フォーム入力中のデータなど、リクエストをまたいで保持したい情報をセッションに保存します。 ## セッションドライバの設定 セッションの設定は `config/session.php` に記述されています。 `SESSION_DRIVER` 環境変数(`.env` ファイル)でドライバを切り替えられます。 ```ini theme={null} SESSION_DRIVER=database ``` Laravelが標準でサポートするドライバは以下のとおりです。 | ドライバ | 概要 | | - | - | | `file` | `storage/framework/sessions` にファイルとして保存する | | `cookie` | 暗号化されたCookieとしてブラウザに保存する | | `database` | データベースのテーブルに保存する | | `redis` | Redisに保存する(高速) | | `array` | PHPの配列に保存する(テスト用。リクエストをまたいで保持されない) | Laravelのデフォルトプロジェクトでは `database` ドライバが設定されています。`database` ドライバを使う場合は `sessions` テーブルが必要ですが、デフォルトのマイグレーションに含まれているのでそのまま使えます。 ## セッションのシリアライゼーション Laravel 13のアプリケーションスケルトンでは、`config/session.php` の `serialization` が `json` に設定されています。フレームワーク内部のデフォルトは `php` のままです。 `php` から `json` に変更すると、アクティブなユーザーのセッションがすべて無効になります。また、セッションにPHPオブジェクトを保存している場合はエラーになる可能性があります。 アップグレード中もアクティブなセッションを維持するには、`php` を明示的に設定してください。セッションにPHPオブジェクトを保存しておらず、ユーザーに再認証を求めても問題ない場合は、セキュリティ向上のため `json` を使用できます。 ```php theme={null} // config/session.php 'serialization' => 'php', ``` ## セッションへのアクセス方法 セッションにアクセスするには、`Request` インスタンスのメソッドを使う方法と、グローバルヘルパー `session()` を使う方法の2つがあります。 ### Requestインスタンスを使う方法 コントローラーのメソッドに `Request` をタイプヒントとして受け取り、`$request->session()` でセッションにアクセスします。 ```php theme={null} session()->get('username'); return view('dashboard', ['username' => $username]); } } ``` ### グローバルヘルパー `session()` を使う方法 `session()` ヘルパー関数はコントローラーやビューなど、どこからでも呼び出せます。 ```php theme={null} // 値を取得する $value = session('key'); // デフォルト値付きで取得する $value = session('key', 'デフォルト値'); // 値を保存する session(['key' => 'value']); ``` どちらの方法でもテスト時に `assertSessionHas` メソッドで検証できます。コントローラー内では `$request->session()` を使うと依存関係が明示されてわかりやすくなります。 ## セッションの操作 ### データの取得 ```php theme={null} // キーを指定して取得する $value = $request->session()->get('key'); // デフォルト値を指定する $value = $request->session()->get('key', 'default'); // デフォルト値をクロージャで指定する $value = $request->session()->get('key', function () { return 'default'; }); // すべてのセッションデータを取得する $data = $request->session()->all(); ``` ### データの存在チェック ```php theme={null} // 値が存在し、かつ null でない場合に true if ($request->session()->has('user_id')) { // ... } // null でも存在すれば true if ($request->session()->exists('user_id')) { // ... } // 存在しない場合に true if ($request->session()->missing('user_id')) { // ... } ``` `has` と `exists` の違いに注意してください。 | メソッド | `null` の場合 | | - | - | | `has` | `false` を返す | | `exists` | `true` を返す | ### データの保存 ```php theme={null} // Requestインスタンス経由 $request->session()->put('user_name', '田中'); // グローバルヘルパー経由 session(['user_name' => '田中']); // 配列への追加 $request->session()->push('cart.items', ['id' => 1, 'name' => '商品A']); ``` ### データの削除 ```php theme={null} // 特定のキーを削除する $request->session()->forget('user_name'); // 複数のキーをまとめて削除する $request->session()->forget(['user_name', 'cart']); // すべてのセッションデータを削除する $request->session()->flush(); ``` ## フラッシュデータ フラッシュデータは**次のリクエストだけ**有効なセッションデータです。 フォームの送信完了メッセージやエラーメッセージなど、一度だけ表示したい情報に使います。 ### `flash()` でデータを保存する ```php theme={null} $request->session()->flash('status', '投稿を保存しました。'); ``` このデータは次のリクエストで参照できますが、その後は自動的に削除されます。 ### フラッシュデータを延長する ```php theme={null} // すべてのフラッシュデータをもう1リクエスト延長する $request->session()->reflash(); // 特定のキーだけ延長する $request->session()->keep(['status', 'message']); ``` ### Bladeでフラッシュメッセージを表示する ```blade theme={null} {{-- resources/views/layouts/app.blade.php --}} @if (session('success'))
    {{ session('success') }}
    @endif @if (session('error'))
    {{ session('error') }}
    @endif ``` フラッシュメッセージの表示はレイアウトファイル(`layouts/app.blade.php` など)に書いておくと、すべてのページで一貫して表示されます。 ## 実用例:フォーム送信後のメッセージ表示 フォームを送信したあと、成功メッセージをセッションに保存してリダイレクトし、次のページで表示するのはWebアプリの定番パターンです。 ```php theme={null} use App\Http\Controllers\PostController; Route::get('/posts', [PostController::class, 'index'])->name('posts.index'); Route::get('/posts/create', [PostController::class, 'create'])->name('posts.create'); Route::post('/posts', [PostController::class, 'store'])->name('posts.store'); ``` フォーム送信を受け付ける `store` メソッドで、保存後に成功メッセージを付けてリダイレクトします。 ```php theme={null} get(); return view('posts.index', ['posts' => $posts]); } public function create(): View { return view('posts.create'); } public function store(Request $request): RedirectResponse { $validated = $request->validate([ 'title' => 'required|string|max:255', 'body' => 'required|string', ]); Post::create($validated); return redirect() ->route('posts.index') ->with('success', '投稿を作成しました。'); } } ``` `redirect()->with('success', '...')` はリダイレクト先へフラッシュデータを渡すショートカットです。 `resources/views/layouts/app.blade.php` にフラッシュメッセージの表示部分を追加します。 ```blade theme={null} My App {{-- フラッシュメッセージ --}} @if (session('success'))
    {{ session('success') }}
    @endif @if (session('error'))
    {{ session('error') }}
    @endif @yield('content') ```
    ```blade theme={null} {{-- resources/views/posts/index.blade.php --}} @extends('layouts.app') @section('content')

    投稿一覧

    @foreach ($posts as $post)

    {{ $post->title }}

    {{ $post->body }}

    @endforeach @endsection ``` 投稿が保存されてリダイレクトされると、この一覧ページの上部に「投稿を作成しました。」というメッセージが1回だけ表示されます。
    ### バリデーションエラーと組み合わせたパターン バリデーション失敗時は `back()->withErrors()` と組み合わせて使います。 Laravelはバリデーション失敗時に自動でエラーをフラッシュデータとして保持するため、Bladeで `$errors` 変数を使えます。 ```php theme={null} public function store(Request $request): RedirectResponse { $validated = $request->validate([ 'title' => 'required|string|max:255', 'body' => 'required|string', ]); Post::create($validated); // 成功時: フラッシュメッセージ付きでリダイレクト return redirect() ->route('posts.index') ->with('success', '投稿を作成しました。'); // バリデーション失敗時はLaravelが自動的に back()->withErrors()->withInput() を実行する } ``` ```blade theme={null} {{-- フォームページ --}} @if ($errors->any())
      @foreach ($errors->all() as $error)
    • {{ $error }}
    • @endforeach
    @endif
    @csrf
    @error('title') {{ $message }} @enderror
    @error('body') {{ $message }} @enderror
    ``` `flush()` はセッションのすべてのデータを削除します。ログイン状態なども失われるため、ユーザーのデータだけを削除したい場合は `forget()` で特定のキーを指定してください。 ## 次のステップ フォームの入力データをバリデーションで検証する方法を確認します。 # Laravel Socialite(ソーシャル認証) Source: https://kawax.biz/jp/socialite OAuth 2.0を使ったソーシャルログイン(GitHub、Google、Facebook など)をLaravel Socialiteでシンプルに実装する方法を解説します。 ## Laravel Socialite とは Laravel Socialite は、OAuth 2.0 を使ったソーシャルログインをシンプルに実装できる公式パッケージです。GitHub、Google、Facebook、X(Twitter)、LinkedIn など主要なプロバイダーに対応しており、複雑な OAuth の実装を数行のコードで済ませることができます。 組み込みでサポートされているプロバイダーは以下のとおりです。 | プロバイダー | キー名 | | - | - | | Bitbucket | `bitbucket` | | Facebook | `facebook` | | GitHub | `github` | | GitLab | `gitlab` | | Google | `google` | | LinkedIn (OpenID) | `linkedin-openid` | | Slack | `slack` / `slack-openid` | | Spotify | `spotify` | | Twitch | `twitch` | | X (Twitter) | `x` | ### ソーシャルログインのフロー ```mermaid theme={null} sequenceDiagram participant User as ユーザー participant App as Laravel アプリ participant OAuth as GitHub (OAuth) participant DB as データベース User->>App: ログインボタンをクリック App->>OAuth: リダイレクト (Socialite::driver('github')->redirect()) OAuth->>User: 認証・権限確認画面 User->>OAuth: 承認 OAuth->>App: コールバック (code パラメータ付き) App->>OAuth: アクセストークン取得 OAuth-->>App: ユーザー情報を返却 App->>DB: ユーザーを登録 / 更新 (updateOrCreate) DB-->>App: ユーザーレコード App->>User: ログイン完了・ダッシュボードへリダイレクト ``` *** ## インストール Composer でパッケージを追加します。 ```shell theme={null} composer require laravel/socialite ``` Socialite をメジャーバージョンアップする際は、必ず [アップグレードガイド](https://github.com/laravel/socialite/blob/master/UPGRADE.md) を確認してください。 *** ## 設定 ### config/services.php 各プロバイダーのクライアント ID・シークレット・コールバック URL を `config/services.php` に追加します。 ```php theme={null} // config/services.php 'github' => [ 'client_id' => env('GITHUB_CLIENT_ID'), 'client_secret' => env('GITHUB_CLIENT_SECRET'), 'redirect' => env('GITHUB_REDIRECT_URI'), ], 'google' => [ 'client_id' => env('GOOGLE_CLIENT_ID'), 'client_secret' => env('GOOGLE_CLIENT_SECRET'), 'redirect' => env('GOOGLE_REDIRECT_URI'), ], 'facebook' => [ 'client_id' => env('FACEBOOK_CLIENT_ID'), 'client_secret' => env('FACEBOOK_CLIENT_SECRET'), 'redirect' => env('FACEBOOK_REDIRECT_URI'), ], ``` `redirect` オプションに相対パスを指定すると、自動的に完全な URL に解決されます。 ### .env 環境変数でクレデンシャルを管理します。GitHub を例に示します。 ```ini theme={null} GITHUB_CLIENT_ID=your-client-id GITHUB_CLIENT_SECRET=your-client-secret GITHUB_REDIRECT_URI=https://example.com/auth/github/callback ``` GitHub の場合、[GitHub Developer Settings](https://github.com/settings/developers) で OAuth App を作成するとクライアント ID とシークレットを取得できます。 *** ## 認証フロー ### ルーティング OAuth 認証には2つのルートが必要です。リダイレクト用とコールバック用です。 ```php theme={null} use Laravel\Socialite\Facades\Socialite; // ユーザーを GitHub にリダイレクト Route::get('/auth/github', function () { return Socialite::driver('github')->redirect(); }); // GitHub からのコールバックを処理 Route::get('/auth/github/callback', function () { $user = Socialite::driver('github')->user(); // $user->token でアクセストークンを取得 }); ``` ### ユーザーの保存とログイン コールバックルートでユーザー情報を取得し、データベースに保存してからログインします。 ```php theme={null} use App\Models\User; use Illuminate\Support\Facades\Auth; use Laravel\Socialite\Facades\Socialite; Route::get('/auth/github/callback', function () { $githubUser = Socialite::driver('github')->user(); $user = User::updateOrCreate( ['github_id' => $githubUser->id], [ 'name' => $githubUser->name, 'email' => $githubUser->email, 'github_token' => $githubUser->token, 'github_refresh_token' => $githubUser->refreshToken, ] ); Auth::login($user); return redirect('/dashboard'); }); ``` `updateOrCreate` を使う場合、`github_id` カラムが `users` テーブルに存在している必要があります。後述のマイグレーション例を参考にしてください。 *** ## ユーザー情報の取得 `user()` メソッドが返すオブジェクトから、以下のプロパティ・メソッドでユーザー情報を取得できます。 ```php theme={null} Route::get('/auth/github/callback', function () { $user = Socialite::driver('github')->user(); // OAuth 2.0 プロバイダー $token = $user->token; $refreshToken = $user->refreshToken; $expiresIn = $user->expiresIn; // OAuth 1.0 プロバイダー (X など) $token = $user->token; $tokenSecret = $user->tokenSecret; // 全プロバイダー共通 $user->getId(); $user->getNickname(); $user->getName(); $user->getEmail(); $user->getAvatar(); }); ``` ### アクセストークンからユーザーを取得 既存のアクセストークンからユーザー情報を取得する場合は `userFromToken()` を使います。 ```php theme={null} $user = Socialite::driver('github')->userFromToken($token); ``` iOSアプリでFacebook Limited Loginを使う場合、FacebookはアクセストークンではなくOIDCトークンを返します。OIDCトークンからユーザー情報を取得するには、ログイン開始時に使ったnonceを `userFromToken()` に渡します。 ```php theme={null} $user = Socialite::driver('facebook')->userFromToken($token, $nonce); ``` ### ステートレスモード Cookie セッションを使わない API では `stateless()` メソッドでセッション状態の検証を無効化できます。 ```php theme={null} return Socialite::driver('google')->stateless()->user(); ``` *** ## データベース連携 ### マイグレーション `users` テーブルにソーシャルログイン用のカラムを追加します。 ```php theme={null} // database/migrations/xxxx_xx_xx_add_github_columns_to_users_table.php return new class extends Migration { public function up(): void { Schema::table('users', function (Blueprint $table) { $table->string('github_id')->nullable()->unique()->after('id'); $table->string('github_token')->nullable()->after('github_id'); $table->string('github_refresh_token')->nullable()->after('github_token'); }); } public function down(): void { Schema::table('users', function (Blueprint $table) { $table->dropColumn(['github_id', 'github_token', 'github_refresh_token']); }); } }; ``` ### provider カラムで複数プロバイダーに対応 複数のプロバイダーをまとめて管理する場合は `provider` / `provider_id` の2カラム構成が一般的です。 ```php theme={null} Schema::table('users', function (Blueprint $table) { $table->string('provider')->nullable()->after('id'); $table->string('provider_id')->nullable()->after('provider'); $table->string('provider_token')->nullable()->after('provider_id'); $table->unique(['provider', 'provider_id']); }); ``` コールバック処理はプロバイダー名を動的に渡すようにします。 ```php theme={null} Route::get('/auth/{provider}/callback', function (string $provider) { $socialUser = Socialite::driver($provider)->user(); $user = User::updateOrCreate( [ 'provider' => $provider, 'provider_id' => $socialUser->getId(), ], [ 'name' => $socialUser->getName(), 'email' => $socialUser->getEmail(), 'provider_token' => $socialUser->token, ] ); Auth::login($user); return redirect('/dashboard'); }); ``` ### 既存ユーザーとの紐付け 同じメールアドレスを持つ既存ユーザーとアカウントを紐付ける場合は、メールアドレスで検索してからカラムを更新します。 ```php theme={null} $socialUser = Socialite::driver('github')->user(); $user = User::where('email', $socialUser->getEmail())->first(); if ($user) { // 既存ユーザーに GitHub 情報を紐付け $user->update([ 'github_id' => $socialUser->getId(), 'github_token' => $socialUser->token, ]); } else { // 新規ユーザーとして作成 $user = User::create([ 'name' => $socialUser->getName(), 'email' => $socialUser->getEmail(), 'github_id' => $socialUser->getId(), 'github_token' => $socialUser->token, ]); } Auth::login($user); ``` *** ## スコープとオプション ### スコープの追加 `scopes()` メソッドで追加のスコープを指定できます。 ```php theme={null} return Socialite::driver('github') ->scopes(['read:user', 'public_repo']) ->redirect(); ``` `setScopes()` メソッドを使うと、既存のスコープをすべて上書きします。 ```php theme={null} return Socialite::driver('github') ->setScopes(['read:user', 'public_repo']) ->redirect(); ``` ### オプションパラメータ `with()` メソッドで追加パラメータをリダイレクトリクエストに含めます。 ```php theme={null} // Google でホスト制限を指定 return Socialite::driver('google') ->with(['hd' => 'example.com']) ->redirect(); // Google で毎回同意画面を表示 return Socialite::driver('google') ->with(['prompt' => 'consent']) ->redirect(); ``` `with()` メソッドで `state` や `response_type` などの予約済みキーワードを渡さないように注意してください。 ### Slack Bot トークン Slack Bot トークンを生成する場合は `asBotUser()` を使います。 ```php theme={null} // リダイレクト時 return Socialite::driver('slack') ->asBotUser() ->setScopes(['chat:write', 'chat:write.public', 'chat:write.customize']) ->redirect(); // コールバック時 $user = Socialite::driver('slack')->asBotUser()->user(); ``` *** ## テスト Socialite はテスト用のモック機能を提供しています。実際のプロバイダーへのリクエストなしに OAuth フローをテストできます。 ### リダイレクトのテスト ```php theme={null} use Laravel\Socialite\Facades\Socialite; test('ユーザーがGitHubにリダイレクトされる', function () { Socialite::fake('github'); $response = $this->get('/auth/github'); $response->assertRedirect(); }); ``` ### コールバックのテスト `fake()` メソッドにユーザーインスタンスを渡して、プロバイダーから返ってくるユーザー情報をモックします。`User::fake()` でフェイクユーザーを生成します。 ```php theme={null} use Laravel\Socialite\Facades\Socialite; use Laravel\Socialite\Two\User; test('GitHubでログインできる', function () { Socialite::fake('github', User::fake([ 'id' => 'github-123', 'name' => 'Jane Doe', 'email' => 'jane@example.com', ])); $response = $this->get('/auth/github/callback'); $response->assertRedirect('/dashboard'); $this->assertDatabaseHas('users', [ 'name' => 'Jane Doe', 'email' => 'jane@example.com', 'github_id' => 'github-123', ]); }); ``` デフォルトではフェイクの OAuth トークン値が設定されます。必要に応じて `fake()` に追加属性を渡してオーバーライドできます。 ```php theme={null} $fakeUser = User::fake([ 'id' => 'github-123', 'name' => 'Jane Doe', 'email' => 'jane@example.com', 'token' => 'fake-token', 'refreshToken' => 'fake-refresh-token', 'expiresIn' => 3600, 'approvedScopes' => ['read:user', 'public_repo'], ]); ``` OAuth 1 のユーザーをフェイクする場合は `Laravel\Socialite\One\User` クラスを使用してください。 *** ## カスタムプロバイダーの作り方 組み込みプロバイダー以外を使いたい場合、`Socialite::extend()` でカスタムドライバーを登録するのが公式の正規の方法です。`SocialiteManager` は `Illuminate\Support\Manager` を継承しているため、他の Laravel ドライバーシステムと同じ仕組みで拡張できます。 Manager パターンと `extend()` の仕組みについては [Manager クラスの解説](/jp/advanced/manager) も参照してください。 ### 1. プロバイダークラスを作成する `Laravel\Socialite\Two\AbstractProvider` を継承し、4つの抽象メソッドを実装します。 ```php theme={null} // app/Socialite/ExampleProvider.php namespace App\Socialite; use Laravel\Socialite\Two\AbstractProvider; use Laravel\Socialite\Two\User; class ExampleProvider extends AbstractProvider { // 認可 URL (ユーザーをリダイレクトする先) public function getAuthUrl($state): string { return $this->buildAuthUrlFromBase('https://example.com/oauth/authorize', $state); } // アクセストークン取得 URL protected function getTokenUrl(): string { return 'https://example.com/oauth/token'; } // アクセストークンでユーザー情報を取得する protected function getUserByToken($token): array { $response = $this->getHttpClient()->get('https://example.com/api/user', [ 'headers' => ['Authorization' => 'Bearer '.$token], ]); return json_decode($response->getBody(), true); } // 取得した配列を Socialite の User オブジェクトにマッピング protected function mapUserToObject(array $user): User { return (new User)->setRaw($user)->map([ 'id' => $user['id'], 'nickname' => $user['login'] ?? null, 'name' => $user['name'], 'email' => $user['email'], 'avatar' => $user['avatar_url'] ?? null, ]); } } ``` 実装が必要な4つのメソッドの役割は以下のとおりです。 | メソッド | 役割 | | - | - | | `getAuthUrl($state)` | ユーザーをリダイレクトする OAuth 認可 URL を返す | | `getTokenUrl()` | 認可コードをアクセストークンに交換するエンドポイント | | `getUserByToken($token)` | アクセストークンを使ってユーザー情報 API を叩き、配列で返す | | `mapUserToObject(array $user)` | 取得した配列を Socialite の `User` オブジェクトに変換する | ### 2. サービスプロバイダーで登録する `AppServiceProvider` の `boot()` メソッドで `Socialite::extend()` を使ってドライバーを登録します。 ```php theme={null} // app/Providers/AppServiceProvider.php use App\Socialite\ExampleProvider; use Laravel\Socialite\Facades\Socialite; public function boot(): void { Socialite::extend('example', function ($app) { $config = $app['config']['services.example']; return Socialite::buildProvider(ExampleProvider::class, $config); }); } ``` ### 3. 設定を追加する ```php theme={null} // config/services.php 'example' => [ 'client_id' => env('EXAMPLE_CLIENT_ID'), 'client_secret' => env('EXAMPLE_CLIENT_SECRET'), 'redirect' => env('EXAMPLE_REDIRECT_URI'), ], ``` ### 4. 通常の Socialite と同じように使う 登録後は組み込みプロバイダーとまったく同じ API で使えます。 ```php theme={null} // リダイレクト Route::get('/auth/example', function () { return Socialite::driver('example')->redirect(); }); // コールバック Route::get('/auth/example/callback', function () { $user = Socialite::driver('example')->user(); }); ``` `extend()` を使って正規の方法で Socialite を拡張しているサードパーティパッケージを利用するのもおすすめです。 *** ## 関連パッケージ 当サイトのオーナーが公開している Socialite 拡張パッケージです。いずれも `Socialite::extend()` を使った正規の方法で実装されており、`config/services.php` に設定を追加するだけで使えます。 LINE SDK for Laravel。Socialite による OAuth ログインに加え、Messaging API も統合。 AT Protocol (Bluesky) との連携。OAuth 認証とポスト送信をサポート。 Discord OAuth2 ログイン。 Meta Threads との連携。OAuth 認証と投稿 API をサポート。 Login with Amazon による OAuth ログイン。 Mastodon インスタンスへの OAuth ログイン。 WordPress.com およびセルフホスト WordPress への OAuth ログイン。 # スターターキット Source: https://kawax.biz/jp/starter-kits Laravelのスターターキットを使って認証・UIを素早くセットアップする方法 ## スターターキットとは スターターキットは、Laravelアプリケーションの出発点として用意された公式のスキャフォールディングです。 ログイン・登録・パスワードリセット・メール確認といった認証機能と、それに対応するUIを一括でセットアップできます。 `laravel new` でアプリケーションを作成する際にスターターキットを選ぶだけで、認証まわりのルート・コントローラー・ビュー・フロントエンドコードがすべて自動で生成されます。 生成されたコードはすべて自分のアプリケーション内に存在するため、自由にカスタマイズできます。 Laravel 13では、従来の **Breeze** と **Jetstream** は廃止されました。 新しいプロジェクトでは以下で紹介するスターターキットを使用してください。 既存のBreezeやJetstreamを使ったプロジェクトはそのまま動作しますが、新規開発には推奨されません。 ## 利用可能なスターターキット Laravel 13では、以下の4種類のスターターキットが提供されています。 React 19・TypeScript・Tailwind 4・[shadcn/ui](https://ui.shadcn.com) を使ったモダンなSPA。 [Inertia.js](https://inertiajs.com) によりサーバーサイドルーティングを維持しながらReactを使用できます。 Vue 3 Composition API・TypeScript・Tailwind・[shadcn-vue](https://www.shadcn-vue.com/) を採用。 Reactと同じくInertia.jsでサーバーサイドと連携します。 PHPだけで動的UIを構築できる [Livewire](https://livewire.laravel.com)。 Bladeテンプレート中心のチームや、JavaScriptフレームワークを使わずに済ませたい場合に最適です。[Flux UI](https://fluxui.dev) を採用。 Svelte 5・TypeScript・Tailwind・[shadcn-svelte](https://www.shadcn-svelte.com/) を使ったSPA。 Inertia.jsと組み合わせてモダンなフロントエンドを構築できます。 ### どれを選ぶべきか | スターターキット | 向いているチーム | | - | - | | React | ReactエコシステムやTypeScriptに慣れているチーム | | Vue | VueやNuxtの経験があるチーム | | Livewire | PHPとBladeを中心に開発したいチーム | | Svelte | SvelteやSvelteKitの経験があるチーム | どれを選んでも認証機能はまったく同じです。チームが最も慣れているフロントエンド技術を選んでください。 Blade・Livewire・Inertia・独立SPAの違いを全体像から確認したい場合は、先に [フロントエンド](/jp/frontend) を読むと選びやすくなります。 ## インストール方法 スターターキットはプロジェクト作成時に選択します。`laravel new` コマンドを実行すると、対話形式でスターターキットを選べます。 まだインストールしていない場合は、ComposerでLaravelインストーラーを取得します。 ```bash theme={null} composer global require laravel/installer ``` `laravel new` コマンドを実行します。スターターキットの選択を含む対話的プロンプトが表示されます。 ```bash theme={null} laravel new my-app ``` プロンプトで **React**、**Vue**、**Livewire**、**Svelte** のいずれかを選択します。 認証プロバイダーとして標準の Laravel 認証か WorkOS AuthKit かも選択できます。 ```bash theme={null} cd my-app npm install && npm run build ``` `.env` ファイルのデータベース設定を確認してからマイグレーションを実行します。 ```bash theme={null} php artisan migrate ``` ```bash theme={null} composer run dev ``` ブラウザで `http://localhost:8000` にアクセスすると、ナビゲーションに「Register」と「Log in」リンクが表示されます。 スターターキットをインストールすると、以下の認証機能がすぐに使えます。 | 機能 | URL | | - | - | | ユーザー登録 | `/register` | | ログイン | `/login` | | パスワードリセット | `/forgot-password` | | メール確認 | `/email/verify` | | プロフィール編集 | `/settings/profile` | ## スターターキットのカスタマイズ 生成されたコードはすべて自分のアプリケーション内にあるので、自由に変更できます。 フロントエンドのコードの大半は `resources/js`(LivewireはBladeなので `resources/views`)ディレクトリにあります。 ### レイアウトの切り替え 各スターターキットには「サイドバー」と「ヘッダー」の2種類のレイアウトが用意されています。 デフォルトはサイドバーレイアウトです。 `resources/js/layouts/app-layout.tsx` を編集します。 ```tsx theme={null} // サイドバーレイアウト(デフォルト) import AppLayoutTemplate from '@/layouts/app/app-sidebar-layout'; // ヘッダーレイアウトに変更する場合 import AppLayoutTemplate from '@/layouts/app/app-header-layout'; ``` `resources/js/layouts/AppLayout.vue` を編集します。 ```js theme={null} // サイドバーレイアウト(デフォルト) import AppLayout from '@/layouts/app/AppSidebarLayout.vue'; // ヘッダーレイアウトに変更する場合 import AppLayout from '@/layouts/app/AppHeaderLayout.vue'; ``` `resources/views/layouts/app.blade.php` を編集します。 ```blade theme={null} {{-- ヘッダーレイアウトに変更する場合 --}} {{ $slot }} ``` `resources/js/layouts/AppLayout.svelte` を編集します。 ```js theme={null} // サイドバーレイアウト(デフォルト) import AppLayout from '@/layouts/app/AppSidebarLayout.svelte'; // ヘッダーレイアウトに変更する場合 import AppLayout from '@/layouts/app/AppHeaderLayout.svelte'; ``` ### 認証ページのレイアウト変更 ログインや登録ページも「simple」「card」「split」の3種類のレイアウトから選べます。 `resources/js/layouts/auth-layout.tsx` を編集します。 ```tsx theme={null} // シンプルレイアウト(デフォルト) import AuthLayoutTemplate from '@/layouts/auth/auth-simple-layout'; // 分割レイアウトに変更する場合 import AuthLayoutTemplate from '@/layouts/auth/auth-split-layout'; ``` `resources/js/layouts/AuthLayout.vue` を編集します。 ```js theme={null} import AuthLayout from '@/layouts/auth/AuthSimpleLayout.vue'; // 分割レイアウトに変更する場合 import AuthLayout from '@/layouts/auth/AuthSplitLayout.vue'; ``` `resources/views/layouts/auth.blade.php` を編集します。 ```blade theme={null} {{ $slot }} ``` `resources/js/layouts/AuthLayout.svelte` を編集します。 ```js theme={null} import AuthLayout from '@/layouts/auth/AuthSimpleLayout.svelte'; // 分割レイアウトに変更する場合 import AuthLayout from '@/layouts/auth/AuthSplitLayout.svelte'; ``` ### ユーザー登録ロジックのカスタマイズ スターターキットはユーザー登録やパスワードリセットの処理に `app/Actions/Fortify` ディレクトリのアクションクラスを使います。 たとえば登録時に電話番号フィールドを追加するには `CreateNewUser.php` を編集します。 ```php theme={null} public function create(array $input): User { Validator::make($input, [ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email', 'max:255', 'unique:users'], 'phone' => ['required', 'string', 'max:20'], 'password' => $this->passwordRules(), ])->validate(); return User::create([ 'name' => $input['name'], 'email' => $input['email'], 'phone' => $input['phone'], 'password' => Hash::make($input['password']), ]); } ``` ## WorkOS AuthKit 認証 `laravel new` 実行時に認証プロバイダーとして **WorkOS AuthKit** を選ぶこともできます。 WorkOS AuthKitを使うと、以下の機能が追加されます。 * ソーシャル認証(Google・Microsoft・GitHub・Apple) * パスキー認証 * メールによる「Magic Auth」 * SSO(シングルサインオン) WorkOS AuthKitの利用にはWorkOSアカウントが必要です。月間アクティブユーザー100万人までは無料で利用できます。 WorkOSを選択した場合は、`.env` ファイルに以下の環境変数を設定します。 ```ini theme={null} WORKOS_CLIENT_ID=your-client-id WORKOS_API_KEY=your-api-key WORKOS_REDIRECT_URL="${APP_URL}/authenticate" ``` ## 注意点 スターターキットは**プロジェクト作成時**に選択するものです。 既存のプロジェクトに後から追加することはできません。 認証UIが必要な場合は、新規プロジェクトでスターターキットを選択するか、手動で認証機能を実装してください。 ## 次のステップ `Auth` ファサードの使い方やルートの保護など、Laravelの認証機能をさらに詳しく学びます。 # 文字列操作(Str クラス) Source: https://kawax.biz/jp/strings Laravel の Str クラスと Stringable(Fluent Strings)を使った文字列操作を実用例とともに解説します。 ## Str クラスとは `Illuminate\Support\Str` クラスは、文字列操作のための豊富なメソッドを提供します。PHPの標準文字列関数を個別に呼ぶ代わりに、統一されたインターフェースで直感的に文字列を処理できます。 Laravelの文字列操作には2つのスタイルがあります。 * **静的メソッドスタイル**: `Str::slug($title, '-')` — 単発の操作に向いています * **Fluentスタイル(メソッドチェーン)**: `Str::of($title)->slug('-')->limit(50)` — 複数の操作をつなげて書けます ```php theme={null} use Illuminate\Support\Str; // 静的メソッド $slug = Str::slug('My New Blog Post', '-'); // 'my-new-blog-post' // Fluent(Str::of) $result = Str::of(' My New Blog Post: Part 1 ') ->trim() ->slug('-') ->limit(30); // 'my-new-blog-post-part-1' ``` `str()` グローバルヘルパー関数は `Str::of()` と同等です。`str('hello')->upper()` のように書けます。 ## ケース変換 文字列のケース(大文字・小文字・記法)を変換するメソッドです。データベースのカラム名やAPIレスポンスのキー変換でよく使います。 ### `Str::camel()` / `Str::snake()` / `Str::kebab()` / `Str::studly()` ```php theme={null} use Illuminate\Support\Str; // camelCase(先頭小文字) Str::camel('foo_bar'); // 'fooBar' Str::camel('user_profile_id'); // 'userProfileId' // snake_case Str::snake('fooBar'); // 'foo_bar' Str::snake('UserProfile'); // 'user_profile' // kebab-case Str::kebab('fooBar'); // 'foo-bar' // StudlyCase(PascalCase) Str::studly('foo_bar'); // 'FooBar' Str::studly('user-profile'); // 'UserProfile' ``` ### `Str::lower()` / `Str::upper()` / `Str::title()` ```php theme={null} use Illuminate\Support\Str; Str::lower('LARAVEL'); // 'laravel' Str::upper('laravel'); // 'LARAVEL' Str::title('hello world'); // 'Hello World' ``` ## スラッグとURL ### `Str::slug()` — URLフレンドリーなスラッグを生成 記事やページのURLを生成するときに必須のメソッドです。スペースや特殊文字を変換し、URLに安全な文字列を作ります。 ```php theme={null} use Illuminate\Support\Str; Str::slug('Laravel フレームワーク', '-'); // 'laravel'(日本語はASCIIに変換されないため除去される) Str::slug('My New Blog Post', '-'); // 'my-new-blog-post' Str::slug('Hello World!', '_'); // 'hello_world' ``` 日本語を含む場合はスラッグが空になることがあります。日本語のスラッグを使いたい場合は、別途ローマ字変換ライブラリを組み合わせるか、IDやタイムスタンプをURLに使うことを検討してください。 ### `Str::of()` でスラッグ生成からバリデーションまで一括処理 ```php theme={null} use Illuminate\Support\Str; $title = ' My New Blog Post: Part 1! '; $slug = Str::of($title) ->trim() ->slug('-') ->limit(50); // 'my-new-blog-post-part-1' ``` ## 文字列の切り詰め ### `Str::limit()` — 文字数で切り詰める 記事の本文やコメントを一覧表示するときにプレビューテキストを生成するメソッドです。 ```php theme={null} use Illuminate\Support\Str; $text = 'Laravelは美しいWebアプリケーションを作るためのPHPフレームワークです。'; // 20文字で切り詰め(デフォルトで末尾に "..." を追加) Str::limit($text, 20); // 'Laravelは美しいWebアプリケー...' // 末尾文字列をカスタマイズ Str::limit($text, 20, ' → 続きを読む'); // 'Laravelは美しいWebアプリケー → 続きを読む' // 単語の途中で切らない(英語テキスト向け) Str::limit('The quick brown fox jumps over the lazy dog', 20, preserveWords: true); // 'The quick brown fox...' ``` ### `Str::words()` — 単語数で切り詰める ```php theme={null} use Illuminate\Support\Str; Str::words('Perfectly balanced, as all things should be.', 3, ' >>>'); // 'Perfectly balanced, as >>>' ``` ## 文字列の検索と確認 ### `Str::contains()` — 文字列を含むか確認 ```php theme={null} use Illuminate\Support\Str; Str::contains('This is my name', 'my'); // true // 配列で複数の候補をチェック(いずれかを含む) Str::contains('This is my name', ['my', 'your']); // true // 大文字・小文字を無視 Str::contains('This is my name', 'MY', ignoreCase: true); // true ``` ### `Str::startsWith()` / `Str::endsWith()` ```php theme={null} use Illuminate\Support\Str; Str::startsWith('https://laravel.com', 'https://'); // true Str::startsWith('https://laravel.com', ['https://', 'http://']); // true Str::endsWith('photo.jpg', '.jpg'); // true Str::endsWith('photo.jpg', ['.jpg', '.png', '.gif']); // true ``` ### `Str::is()` — ワイルドカードパターンマッチ ```php theme={null} use Illuminate\Support\Str; Str::is('foo*', 'foobar'); // true Str::is('*/user/*', '/admin/user/profile'); // true // 大文字・小文字を無視 Str::is('F*', 'foo', ignoreCase: true); // true ``` ## 文字列の変換と置換 ### `Str::replace()` — 文字列を置換 ```php theme={null} use Illuminate\Support\Str; Str::replace('8.x', '13.x', 'Laravel 8.x'); // 'Laravel 13.x' // 大文字・小文字を無視して置換 Str::replace('laravel', 'Symphony', 'I love Laravel', caseSensitive: false); // 'I love Symphony' ``` ### `Str::replaceArray()` — 配列で順番に置換 ```php theme={null} use Illuminate\Support\Str; $string = '予定は ? から ? まで'; $result = Str::replaceArray('?', ['午前9時', '午後5時'], $string); // '予定は 午前9時 から 午後5時 まで' ``` ### `Str::replaceMatches()` — 正規表現で置換 ```php theme={null} use Illuminate\Support\Str; // 電話番号から数字以外を除去 Str::replaceMatches('/[^0-9]/', '', '(03) 1234-5678'); // '0312345678' // クロージャで置換内容を動的に生成 Str::replaceMatches('/\d+/', fn ($matches) => '[' . $matches[0] . ']', '1つ 2個 3本'); // '[1]つ [2]個 [3]本' ``` ### `Str::remove()` — 文字列を削除 ```php theme={null} use Illuminate\Support\Str; Str::remove('e', 'Peter Piper picked a peck of pickled peppers.'); // 'Ptr Pipr pickd a pck of pickld ppprs.' // 配列で複数文字列を削除 Str::remove(['foo', 'bar'], 'foo and bar and baz'); // ' and and baz' ``` ### `Str::squish()` — 余分な空白を除去 ```php theme={null} use Illuminate\Support\Str; Str::squish(' Laravel Framework '); // 'Laravel Framework' ``` ## 文字列の前後操作 ### `Str::before()` / `Str::after()` ```php theme={null} use Illuminate\Support\Str; // 指定文字列より前の部分 Str::before('test@example.com', '@'); // 'test' // 指定文字列より後の部分 Str::after('test@example.com', '@'); // 'example.com' // 最後の出現箇所を基準にする Str::afterLast('App\Http\Controllers\UserController', '\\'); // 'UserController' Str::beforeLast('App\Http\Controllers\UserController', '\\'); // 'App\Http\Controllers' ``` ### `Str::between()` — 2つの文字列の間を取得 ```php theme={null} use Illuminate\Support\Str; Str::between('[debug] Error occurred in module', '[', ']'); // 'debug' ``` ### `Str::start()` / `Str::finish()` — 特定文字で始まる・終わるようにする すでにその文字で始まっていれば追加しません(二重化しない)。 ```php theme={null} use Illuminate\Support\Str; // '/' で始まるようにする Str::start('users/profile', '/'); // '/users/profile' Str::start('/users/profile', '/'); // '/users/profile'(二重にならない) // '/' で終わるようにする Str::finish('https://example.com', '/'); // 'https://example.com/' ``` ### `Str::chopStart()` / `Str::chopEnd()` — 特定の接頭辞・接尾辞を除去 ```php theme={null} use Illuminate\Support\Str; Str::chopStart('https://laravel.com', 'https://'); // 'laravel.com' // 配列で複数パターンに対応 Str::chopStart('http://laravel.com', ['https://', 'http://']); // 'laravel.com' Str::chopEnd('UserController.php', '.php'); // 'UserController' ``` ## マスキングとセキュリティ ### `Str::mask()` — 文字列をマスクする メールアドレスや電話番号の一部を隠す処理に使います。 ```php theme={null} use Illuminate\Support\Str; // 4文字目以降を '*' でマスク Str::mask('yamada@example.com', '*', 4); // 'yama**************' // 末尾4文字だけ表示(負の値でオフセット) Str::mask('1234-5678-9012-3456', '*', -4); // '***************3456' // 範囲を指定してマスク Str::mask('yamada@example.com', '*', 3, 5); // 'yam*****example.com' ``` ### `Str::excerpt()` — 文脈付きで文字列を抜き出す 検索結果のスニペット表示などに使います。 ```php theme={null} use Illuminate\Support\Str; $text = 'Laravelは美しいWebアプリケーションを構築するためのPHPフレームワークです。'; Str::excerpt($text, 'PHP', ['radius' => 10]); // '...ためのPHPフレームワー...' ``` ## ランダム文字列と識別子 ### `Str::random()` — ランダム文字列を生成 トークンやパスワードのリセット用のキーを生成するときに使います。 ```php theme={null} use Illuminate\Support\Str; Str::random(32); // 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' (英数字32文字) ``` ### `Str::uuid()` / `Str::ulid()` — UUID・ULID生成 ```php theme={null} use Illuminate\Support\Str; (string) Str::uuid(); // '550e8400-e29b-41d4-a716-446655440000' // 時系列ソート可能なUUID (UUIDv7) (string) Str::uuid7(); // ULID(ソート可能なID) (string) Str::ulid(); // '01ARZ3NDEKTSV4RRFFQ69G5FAV' ``` ### `Str::password()` — セキュアなパスワード生成 ```php theme={null} use Illuminate\Support\Str; Str::password(12); // 大文字・小文字・数字・記号を含む12文字のランダム文字列 ``` ### `Str::counted()` — 数量付きの単数・複数形 数値に応じて単数・複数形に変換し、フォーマットされた数値と一緒に返します。 ```php theme={null} use Illuminate\Support\Str; Str::counted('order', 1); // '1 order' Str::counted('order', 1000); // '1,000 orders' ``` Fluent スタイルでも同様に使えます。 ```php theme={null} Str::of('order')->counted(3); // '3 orders' ``` UIでの件数表示(「1件」「2件以上」の使い分け)を英語で行う場合に便利です。カンマ区切りのフォーマットは自動で適用されます。 ## Fluent Strings(メソッドチェーン) `Str::of()` または `str()` で始めると、`Stringable` インスタンスが返り、メソッドをチェーンできます。最終的に文字列として使いたい場合は `(string)` でキャストするか、文字列コンテキストで使います。 ```php theme={null} use Illuminate\Support\Str; $result = Str::of(' hello world ') ->trim() ->title() ->append('!') ->toString(); // 'Hello World!' ``` ### 実用的なメソッドチェーンの例 **ブログ記事のスラッグ生成** ```php theme={null} use Illuminate\Support\Str; $slug = Str::of(' My New Blog Post: Part 1 ') ->trim() ->lower() ->slug('-') ->limit(50); // 'my-new-blog-post-part-1' ``` **URLからドメインを抽出** ```php theme={null} use Illuminate\Support\Str; $domain = Str::of('https://www.example.com/path/to/page') ->after('//') ->before('/') ->chopStart('www.'); // 'example.com' ``` **ユーザー入力のサニタイズ** ```php theme={null} use Illuminate\Support\Str; $clean = Str::of($userInput) ->squish() // 余分な空白を除去 ->limit(255) // 長さを制限 ->toString(); ``` **クラス名からファイルパスを生成** ```php theme={null} use Illuminate\Support\Str; $path = Str::of('App\Http\Controllers\UserController') ->replace('\\', '/') ->append('.php') ->toString(); // 'App/Http/Controllers/UserController.php' ``` ### 条件付きメソッドチェーン(`when()`) ```php theme={null} use Illuminate\Support\Str; $result = Str::of('Laravel') ->when($isUppercase, fn ($str) => $str->upper()) ->append(' Framework'); // $isUppercase が true なら 'LARAVEL Framework' // false なら 'Laravel Framework' ``` ### `pipe()` — 任意のコールバックを挟む ```php theme={null} use Illuminate\Support\Str; $result = Str::of('my-slug') ->pipe(fn ($str) => $str->replace('-', '_')) ->upper() ->toString(); // 'MY_SLUG' ``` ## `Str::of()` で使える主要メソッド一覧 Fluent Strings で使えるメソッドは静的メソッドとほぼ同じセットです。以下は代表的なものです。 ```php theme={null} use Illuminate\Support\Str; $str = Str::of('Hello, World!'); $str->length(); // 13 $str->upper(); // 'HELLO, WORLD!' $str->lower(); // 'hello, world!' $str->trim(); // 'Hello, World!' $str->slug(); // 'hello-world' $str->camel(); // 'hello,World!'(記号除去なし) $str->contains('World'); // true $str->startsWith('Hello'); // true $str->endsWith('!'); // true $str->replace(',', ''); // 'Hello World!' $str->prepend('>>> '); // '>>> Hello, World!' $str->append(' <<<'); // 'Hello, World! <<<' $str->reverse(); // '!dlroW ,olleH' $str->wordCount(); // 2 ``` ## まとめ | メソッド | 用途 | | - | - | | `Str::slug($str)` | URLフレンドリーなスラッグ生成 | | `Str::limit($str, $n)` | 文字数で切り詰め | | `Str::contains($str, $needle)` | 文字列を含むか確認 | | `Str::startsWith($str, $needle)` | 指定文字列で始まるか確認 | | `Str::endsWith($str, $needle)` | 指定文字列で終わるか確認 | | `Str::replace($search, $replace, $str)` | 文字列を置換 | | `Str::camel($str)` | camelCase に変換 | | `Str::snake($str)` | snake\_case に変換 | | `Str::kebab($str)` | kebab-case に変換 | | `Str::studly($str)` | StudlyCase に変換 | | `Str::upper($str)` | 大文字に変換 | | `Str::lower($str)` | 小文字に変換 | | `Str::squish($str)` | 余分な空白を除去 | | `Str::after($str, $search)` | 指定文字列以降を取得 | | `Str::before($str, $search)` | 指定文字列以前を取得 | | `Str::between($str, $from, $to)` | 2文字列間を取得 | | `Str::mask($str, '*', $index)` | 文字列をマスク | | `Str::random($length)` | ランダム文字列生成 | | `Str::uuid()` | UUID生成 | | `Str::of($str)` | Fluentスタイルの開始 | **静的メソッドを使う場面**: * 1〜2回の変換だけでよい場合 * シンプルで読みやすいコード ```php theme={null} $slug = Str::slug($title); ``` **Fluent(Str::of)を使う場面**: * 3つ以上の変換をまとめて行う場合 * 条件分岐(`when()`)を含む場合 * 処理の流れを上から下に読めるようにしたい場合 ```php theme={null} $slug = Str::of($title) ->trim() ->lower() ->slug() ->limit(50); ``` PHPの `strtolower()`, `substr()`, `str_replace()` などの標準関数の代わりに `Str` クラスを使うことで: * マルチバイト文字(日本語)を安全に扱える(`mb_*` 関数を内部で使用) * メソッドチェーンで処理をまとめられる * 引数の順序を覚えなくてよい(一貫したインターフェース) * Laravelのコードスタイルに統一できる # テスト入門 Source: https://kawax.biz/jp/testing PestとPHPUnitを使ったLaravelアプリケーションのテストの書き方 ## テストとは テストとは、コードが期待どおりに動作するかを自動的に検証する仕組みです。 テストを書くことで、機能追加や修正をしたときに既存の動作が壊れていないかを素早く確認できます。 チームでの開発では、テストがあることで安心してコードを変更・レビューできます。 Laravelは最初からテストのサポートが組み込まれており、[Pest](https://pestphp.com) と [PHPUnit](https://phpunit.de) の両方が使えます。 新規インストール時には `phpunit.xml` 設定ファイルと `tests/` ディレクトリが自動的に用意されています。 PestはPHPUnitをベースに構築されており、より簡潔で読みやすい構文でテストを書けます。これからテストを始める場合はPestを選ぶのがおすすめです。 ## `tests/` ディレクトリの構成 ``` tests/ ├── Feature/ # 機能テスト │ └── ExampleTest.php ├── Unit/ # ユニットテスト │ └── ExampleTest.php └── TestCase.php ``` * **`Feature/`** — 機能テストを置きます。HTTPリクエストを含む大きな単位のテストに使います。複数のオブジェクトが連携する動作やAPIエンドポイントの検証など、アプリケーション全体に近いテストです。**ほとんどのテストはここに置くことになります。** * **`Unit/`** — ユニットテストを置きます。単一のクラスやメソッドなど、小さな単位のテストに使います。Laravelアプリケーションは起動しないため、データベースや他のフレームワーク機能は使えません。 ## テストの作成 `make:test` Artisanコマンドで新しいテストクラスを生成します。 ```shell theme={null} # Feature テストを作成する(デフォルト) php artisan make:test TodoTest # Unit テストを作成する php artisan make:test TodoTest --unit ``` `tests/Feature/TodoTest.php` が生成されます。 ## テストの実行 `php artisan test` コマンドでテストを実行します。 ```shell theme={null} php artisan test ``` `vendor/bin/pest` や `vendor/bin/phpunit` を直接実行しても同じ結果が得られますが、`php artisan test` のほうが見やすい出力が得られるため推奨します。 特定のテストスイートだけを実行したいときはオプションを使います。 ```shell theme={null} # Feature テストのみ実行する php artisan test --testsuite=Feature # 失敗したらすぐ止める php artisan test --stop-on-failure ``` ## 基本的なテストの書き方 シンプルなアサーションの例です。 ```php Pest theme={null} toBeTrue(); }); test('文字列が含まれているか確認する', function () { $message = 'Hello, Laravel!'; expect($message)->toContain('Laravel'); }); ``` ```php PHPUnit theme={null} assertTrue(true); } public function test_string_contains_laravel(): void { $message = 'Hello, Laravel!'; $this->assertStringContainsString('Laravel', $message); } } ``` ### よく使うアサーション | Pest | PHPUnit | 説明 | | - | - | - | | `expect($x)->toBe($y)` | `$this->assertSame($y, $x)` | 厳密に等しい | | `expect($x)->toEqual($y)` | `$this->assertEquals($y, $x)` | 等しい(型は問わない) | | `expect($x)->toBeTrue()` | `$this->assertTrue($x)` | `true` である | | `expect($x)->toBeFalse()` | `$this->assertFalse($x)` | `false` である | | `expect($x)->toBeNull()` | `$this->assertNull($x)` | `null` である | | `expect($x)->toContain($y)` | `$this->assertStringContainsString($y, $x)` | 文字列を含む | | `expect($x)->toHaveCount($n)` | `$this->assertCount($n, $x)` | 要素数が一致する | ## HTTPテスト LaravelのHTTPテストでは、実際のHTTPサーバーを立てることなく、ルートへのリクエストをシミュレートできます。 `Feature/` ディレクトリのテストクラスで使える強力な機能です。 ### ページの表示を確認する `get()` メソッドでGETリクエストを送り、レスポンスを検証します。 ```php Pest theme={null} get('/'); $response->assertStatus(200); }); ``` ```php PHPUnit theme={null} get('/'); $response->assertStatus(200); } } ``` ### ToDoアプリのHTTPテスト例 `Todo` モデルがあるアプリケーションを例に、CRUDの各操作をテストします。 ```php Pest theme={null} count(3)->create(); $response = $this->get('/todos'); $response->assertStatus(200); $response->assertSee('一覧'); }); test('ToDoを新規作成できる', function () { $response = $this->post('/todos', [ 'title' => 'Laravelを学ぶ', ]); $response->assertRedirect('/todos'); $this->assertDatabaseHas('todos', ['title' => 'Laravelを学ぶ']); }); test('ToDoを削除できる', function () { $todo = Todo::factory()->create(); $response = $this->delete("/todos/{$todo->id}"); $response->assertRedirect('/todos'); $this->assertDatabaseMissing('todos', ['id' => $todo->id]); }); ``` ```php PHPUnit theme={null} count(3)->create(); $response = $this->get('/todos'); $response->assertStatus(200); $response->assertSee('一覧'); } public function test_todo_can_be_created(): void { $response = $this->post('/todos', [ 'title' => 'Laravelを学ぶ', ]); $response->assertRedirect('/todos'); $this->assertDatabaseHas('todos', ['title' => 'Laravelを学ぶ']); } public function test_todo_can_be_deleted(): void { $todo = Todo::factory()->create(); $response = $this->delete("/todos/{$todo->id}"); $response->assertRedirect('/todos'); $this->assertDatabaseMissing('todos', ['id' => $todo->id]); } } ``` ### よく使うレスポンスのアサーション | メソッド | 説明 | | - | - | | `assertStatus(200)` | レスポンスのHTTPステータスコードを確認する | | `assertOk()` | ステータスコードが200であることを確認する | | `assertRedirect('/path')` | 指定URLへのリダイレクトを確認する | | `assertSee('テキスト')` | レスポンス本文にテキストが含まれるか確認する | | `assertDontSee('テキスト')` | レスポンス本文にテキストが含まれないか確認する | | `assertJson([...])` | JSONレスポンスのデータを確認する | | `assertDatabaseHas('table', [...])` | データベースにレコードが存在するか確認する | | `assertDatabaseMissing('table', [...])` | データベースにレコードが存在しないか確認する | `RefreshDatabase` トレイトを使うと、各テストの実行後にデータベースをリセットします。テスト間でデータが干渉しないようにするために使います。 ## テスト環境 ### `phpunit.xml` の設定 プロジェクトルートの `phpunit.xml` でテスト環境の設定を行います。 デフォルトではセッションとキャッシュが `array` ドライバーに設定されており、テスト実行中にデータが残りません。 ```xml theme={null} ``` データベースには、ファイルを残さないSQLiteのインメモリデータベースを使うのが一般的です。 ```xml theme={null} ``` ### `.env.testing` ファイル プロジェクトルートに `.env.testing` ファイルを作成すると、テスト実行時に `.env` の代わりに読み込まれます。 テスト専用のデータベース接続や外部サービスの設定を分けたい場合に使います。 ```ini theme={null} APP_ENV=testing DB_CONNECTION=sqlite DB_DATABASE=:memory: ``` 設定をキャッシュしている場合、`php artisan config:clear` を実行してからテストを実行してください。古いキャッシュが使われて設定が反映されないことがあります。 ## 並列テスト実行 テスト数が増えてくると実行時間が長くなります。 `--parallel` オプションを使うと、複数のプロセスでテストを同時実行して時間を短縮できます。 まず `brianium/paratest` パッケージをインストールします。 ```shell theme={null} composer require brianium/paratest --dev ``` その後、`--parallel` オプションを付けて実行します。 ```shell theme={null} php artisan test --parallel ``` デフォルトではCPUのコア数分のプロセスが立ち上がります。プロセス数を指定するには `--processes` オプションを使います。 ```shell theme={null} php artisan test --parallel --processes=4 ``` 並列テストを使うときは、各プロセスが独立したデータベースを使う必要があります。`RefreshDatabase` トレイトと `phpunit.xml` のインメモリSQLite設定を組み合わせると問題なく動作します。 ## 次のステップ リクエストのシミュレーション、認証付きテスト、JSONアサーションなど、より詳しいHTTPテストの方法を確認できます。 # Laravelを始める前に必要な知識 Source: https://kawax.biz/jp/true-tutorial Laravel学習の前提になる4つの分野を整理し、先に身につけるべき知識を案内します。 Laravelの公式ドキュメントは、Laravelそのものを学ぶための資料です。\ ここでは「Laravelを読む前に、どの分野を先に押さえるべきか」を短く整理します。 このページは詳細解説ではありません。足りない分野を見つけたら先に学び、準備ができたらLaravel学習に戻ってきてください。 ## PHPの基礎 Laravelを使うなら、PHPの基礎は前提です。古い手続き型スタイルの感覚だけで進むと、サービスコンテナやEloquentの理解でつまずきやすくなります。\ 特に重要なのは、PHPのバージョンアップ情報を継続して追うことです。Laravelは旧PHPのサポートを積極的に終了します。\ 実務ではnamed arguments、match式、Attributes、FibersなどPHP 8.xの機能を自然に読める状態が必要です。Laravel 13ではPHP 8.3以上が前提です。ベテランはPHP RFCまで確認します。 ## フロントエンドの知識 現代のWeb開発では、Laravelでもフロントエンド知識が必須です。「APIだけLaravelで、UIは完全に別チーム」という体制は、少なくともLaravelの標準的な開発像では主流ではありません。\ Blade(サーバーサイド中心)、Livewire(JavaScriptを最小化したリアクティブUI)、Inertia + React/Vue(SPAに近い体験)のどれを選ぶかで設計も実装も変わります。\ さらに、jQuery中心からSPA、そしてSSR/ハイブリッドへ進んだ流れを知ると、現在の選択肢を理解しやすくなります。npmとViteの基礎も先に押さえておくべきです。 ## PHPフレームワークの歴史的経緯 他フレームワークの流儀をLaravelにそのまま持ち込むと、学習効率が大きく下がります。\ CodeIgniterやSymfonyの経験があるほど、「いったん忘れてLaravelの作法で学び直す」姿勢が重要です。\ LaravelはSymfonyコンポーネントを活用しつつ、ファサードやEloquentのような独自の哲学で開発体験を設計しています。2011年以降に急速に普及した背景には、この一貫した思想と生産性の高さがあります。 ## インフラの知識 Laravel Cloudのような便利な基盤があっても、土台となるインフラ知識は必要です。\ VPSやAWS EC2で手動構築できるレベルを知っていると、トラブル時の切り分けが速くなります。\ Nginx/Apache、PHP-FPM、MySQLやPostgreSQLの基本構成、環境変数と`.env`管理、そしてDockerの基礎は避けて通れません。Laravel Sailを使う場合も、内部で何が動いているか理解しているほど運用が安定します。 4分野のうち不安がある項目から先に補強してください。Laravelは前提知識があるほど、公式ドキュメントを短時間で深く理解できます。 # URL生成 Source: https://kawax.biz/jp/urls Laravelのurl()ヘルパー、名前付きルートURL、署名付きURL、コントローラーアクションURLの生成方法を解説します。 ## はじめに LaravelはアプリケーションのURLを生成するためのヘルパーをいくつか提供しています。 これらはテンプレートやAPIレスポンスでリンクを構築する際、またはアプリケーションの別の場所へリダイレクトレスポンスを生成する際に特に便利です。 ## 基本的な使い方 ### URLの生成 `url` ヘルパーを使って任意のURLを生成できます。 生成されたURLは、アプリケーションが処理している現在のリクエストのスキーム(HTTPまたはHTTPS)とホストを自動的に使用します。 ```php theme={null} $post = App\Models\Post::find(1); echo url("/posts/{$post->id}"); // http://example.com/posts/1 ``` クエリ文字列パラメータを含むURLを生成するには `query` メソッドを使います。 ```php theme={null} echo url()->query('/posts', ['search' => 'Laravel']); // https://example.com/posts?search=Laravel echo url()->query('/posts?sort=latest', ['search' => 'Laravel']); // http://example.com/posts?sort=latest&search=Laravel ``` パスにすでに存在するクエリ文字列パラメータを指定すると、既存の値が上書きされます。 ```php theme={null} echo url()->query('/posts?sort=latest', ['sort' => 'oldest']); // http://example.com/posts?sort=oldest ``` 配列の値もクエリパラメータとして渡せます。これらの値は適切にキー付けされ、生成されたURLにエンコードされます。 ```php theme={null} echo $url = url()->query('/posts', ['columns' => ['title', 'body']]); // http://example.com/posts?columns%5B0%5D=title&columns%5B1%5D=body echo urldecode($url); // http://example.com/posts?columns[0]=title&columns[1]=body ``` ### 現在のURLの取得 `url` ヘルパーにパスを指定しない場合、`Illuminate\Routing\UrlGenerator` インスタンスが返され、現在のURLに関する情報にアクセスできます。 ```php theme={null} // クエリ文字列なしの現在のURL echo url()->current(); // クエリ文字列を含む現在のURL echo url()->full(); ``` これらのメソッドは `URL` [ファサード](./facades)からもアクセスできます。 ```php theme={null} use Illuminate\Support\Facades\URL; echo URL::current(); ``` ### 直前のURLの取得 ユーザーが訪問していた直前のURLを知りたい場合があります。`url` ヘルパーの `previous` メソッドや `previousPath` メソッドで取得できます。 ```php theme={null} // 直前のリクエストの完全なURL echo url()->previous(); // 直前のリクエストのパス echo url()->previousPath(); ``` セッション経由で直前のURLを取得することもできます。 ```php theme={null} use Illuminate\Http\Request; Route::post('/users', function (Request $request) { $previousUri = $request->session()->previousUri(); // ... }); ``` 直前に訪問したURLのルート名をセッション経由で取得することも可能です。 ```php theme={null} $previousRoute = $request->session()->previousRoute(); ``` ## 名前付きルートのURL `route` ヘルパーは[名前付きルート](./routing#名前付きルート)へのURLを生成するために使えます。 名前付きルートを使うと、ルートに定義された実際のURLに依存せずにURLを生成できます。 そのため、ルートのURLが変更されても、`route` 関数の呼び出しを変更する必要がありません。 ```php theme={null} Route::get('/post/{post}', function (Post $post) { // ... })->name('post.show'); ``` このルートへのURLを生成するには次のようにします。 ```php theme={null} echo route('post.show', ['post' => 1]); // http://example.com/post/1 ``` 複数のパラメータを持つルートにも対応しています。 ```php theme={null} Route::get('/post/{post}/comment/{comment}', function (Post $post, Comment $comment) { // ... })->name('comment.show'); echo route('comment.show', ['post' => 1, 'comment' => 3]); // http://example.com/post/1/comment/3 ``` ルートの定義パラメータに対応しない追加の配列要素はURLのクエリ文字列に追加されます。 ```php theme={null} echo route('post.show', ['post' => 1, 'search' => 'rocket']); // http://example.com/post/1?search=rocket ``` ### Eloquentモデル Eloquentモデルのルートキー(通常は主キー)を使ってURLを生成することが多いでしょう。 そのためにEloquentモデルをパラメータ値として渡せます。`route` ヘルパーはモデルのルートキーを自動的に取り出します。 ```php theme={null} echo route('post.show', ['post' => $post]); ``` ### 署名付きURL Laravelでは名前付きルートへの「署名付き」URLを簡単に作成できます。 これらのURLにはクエリ文字列に「署名」ハッシュが付加され、URLが作成後に改ざんされていないことをLaravelが検証できます。 署名付きURLは、公開アクセス可能だがURL操作に対する保護が必要なルートに特に便利です。 たとえば、顧客にメールで送信する公開「登録解除」リンクを実装するために署名付きURLを使えます。 `URL` ファサードの `signedRoute` メソッドを使って署名付きURLを作成します。 ```php theme={null} use Illuminate\Support\Facades\URL; return URL::signedRoute('unsubscribe', ['user' => 1]); ``` `signedRoute` メソッドに `absolute` 引数を指定することで、署名URLハッシュからドメインを除外できます。 ```php theme={null} return URL::signedRoute('unsubscribe', ['user' => 1], absolute: false); ``` 指定した時間後に有効期限が切れる一時的な署名付きURLを生成するには `temporarySignedRoute` メソッドを使います。 Laravelは一時的な署名付きURLを検証する際、署名付きURLにエンコードされた有効期限タイムスタンプが経過していないことを確認します。 ```php theme={null} use Illuminate\Support\Facades\URL; return URL::temporarySignedRoute( 'unsubscribe', now()->plus(minutes: 30), ['user' => 1] ); ``` #### 署名付きURLの検証フロー ```mermaid theme={null} sequenceDiagram participant App as Laravelアプリ participant User as ユーザー participant Mail as メール App->>App: URL::signedRoute() / temporarySignedRoute() App->>Mail: 署名付きURLをメール送信 Mail->>User: メール受信 User->>App: URLをクリック(GETリクエスト) App->>App: 署名検証
    hasValidSignature() alt 署名が有効 App->>User: 処理実行(例: 登録解除) else 署名が無効・期限切れ App->>User: 403 エラー end ``` #### 署名付きルートリクエストの検証 受信リクエストに有効な署名があることを確認するには、`Illuminate\Http\Request` インスタンスの `hasValidSignature` メソッドを呼び出します。 ```php theme={null} use Illuminate\Http\Request; Route::get('/unsubscribe/{user}', function (Request $request) { if (! $request->hasValidSignature()) { abort(401); } // ... })->name('unsubscribe'); ``` 特定のクエリパラメータを検証時に無視したい場合は `hasValidSignatureWhileIgnoring` メソッドを使います。 ```php theme={null} if (! $request->hasValidSignatureWhileIgnoring(['page', 'order'])) { abort(401); } ``` 受信リクエストインスタンスを使う代わりに、`signed`(`Illuminate\Routing\Middleware\ValidateSignature`)[ミドルウェア](./middleware)をルートに割り当てることもできます。 受信リクエストに有効な署名がない場合、ミドルウェアは自動的に `403` HTTPレスポンスを返します。 ```php theme={null} Route::post('/unsubscribe/{user}', function (Request $request) { // ... })->name('unsubscribe')->middleware('signed'); ``` 署名付きURLにドメインが含まれない場合は `relative` 引数をミドルウェアに指定します。 ```php theme={null} Route::post('/unsubscribe/{user}', function (Request $request) { // ... })->name('unsubscribe')->middleware('signed:relative'); ``` 署名はURLのパスとクエリ文字列を保護しますが、フォームやJSONの本文は署名されません。 Laravelは入力を統合する際に本文をクエリ文字列より優先するため、署名済みの値は `$request->query('email')` のように `query()` で取得してください。 `input()`、`all()`、動的プロパティでは、署名されていない本文の値が返る可能性があります。GETリクエストでも本文を含められるため、同じ注意が必要です。 実装例と入力の使い分けは[署名付きURLの入力を正しく扱う](/jp/signed-urls)を参照してください。 #### 無効な署名付きルートへの応答 有効期限切れの署名付きURLにアクセスすると、`403` HTTPステータスコードの汎用エラーページが表示されます。 アプリケーションの `bootstrap/app.php` ファイルで `InvalidSignatureException` 例外のカスタム「render」クロージャを定義することで、この動作をカスタマイズできます。 ```php theme={null} use Illuminate\Routing\Exceptions\InvalidSignatureException; ->withExceptions(function (Exceptions $exceptions): void { $exceptions->render(function (InvalidSignatureException $e) { return response()->view('errors.link-expired', status: 403); }); }) ``` ## コントローラーアクションのURL `action` 関数は指定されたコントローラーアクションのURLを生成します。 ```php theme={null} use App\Http\Controllers\HomeController; $url = action([HomeController::class, 'index']); ``` コントローラーメソッドがルートパラメータを受け取る場合は、関数の第2引数にルートパラメータの連想配列を渡します。 ```php theme={null} $url = action([UserController::class, 'profile'], ['id' => 1]); ``` ## フルエントURIオブジェクト LaravelのURIクラスは、オブジェクトを通じてURIを作成・操作するための便利なフルエントインターフェースを提供します。 ```php theme={null} use App\Http\Controllers\UserController; use Illuminate\Support\Uri; // 文字列からURIインスタンスを生成 $uri = Uri::of('https://example.com/path'); // パス、名前付きルート、コントローラーアクションへのURIを生成 $uri = Uri::to('/dashboard'); $uri = Uri::route('users.show', ['user' => 1]); $uri = Uri::signedRoute('users.show', ['user' => 1]); $uri = Uri::temporarySignedRoute('user.index', now()->plus(minutes: 5)); $uri = Uri::action([UserController::class, 'index']); // 現在のリクエストURLからURIインスタンスを生成 $uri = $request->uri(); ``` URIインスタンスを取得したら、フルエントに変更できます。 ```php theme={null} $uri = Uri::of('https://example.com') ->withScheme('http') ->withHost('test.com') ->withPort(8000) ->withPath('/users') ->withQuery(['page' => 2]) ->withFragment('section-1'); ``` ## デフォルトのURLパラメータ 特定のURLパラメータにリクエスト全体のデフォルト値を指定したい場合があります。 たとえば、多くのルートに `{locale}` パラメータが定義されているとします。 ```php theme={null} Route::get('/{locale}/posts', function () { // ... })->name('post.index'); ``` `route` ヘルパーを呼び出すたびに `locale` を渡すのは面倒です。 そこで `URL::defaults` メソッドを使って、現在のリクエスト中に常に適用されるこのパラメータのデフォルト値を定義できます。 このメソッドは[ルートミドルウェア](/jp/middleware#ルートへのミドルウェア適用)から呼び出すと、現在のリクエストにアクセスできます。 ```php theme={null} $request->user()->locale]); return $next($request); } } ``` `locale` パラメータのデフォルト値が設定されると、`route` ヘルパーでURLを生成する際にその値を渡す必要がなくなります。 URLデフォルト値の設定はLaravelの暗黙的なモデルバインディングの処理を妨げる場合があります。 URLデフォルト値を設定するミドルウェアをLaravel自身の `SubstituteBindings` ミドルウェアより前に実行されるよう[ミドルウェアの優先度設定](./middleware)を行ってください。 アプリケーションの `bootstrap/app.php` ファイルで `priority` ミドルウェアメソッドを使って設定できます。 ```php theme={null} ->withMiddleware(function (Middleware $middleware): void { $middleware->prependToPriorityList( before: \Illuminate\Routing\Middleware\SubstituteBindings::class, prepend: \App\Http\Middleware\SetDefaultLocaleForUrls::class, ); }) ``` # バリデーション Source: https://kawax.biz/jp/validation Laravelのバリデーション機能を使って、フォームの入力データを検証する方法を解説します。 ## バリデーションとは バリデーションとは、ユーザーから送信されたデータが期待する形式や条件を満たしているかを検証することです。 Laravelはリクエストオブジェクトの `validate` メソッドや専用のフォームリクエストクラスを使った、シンプルで強力なバリデーション機能を提供しています。 ```mermaid theme={null} flowchart TD A["HTTPリクエスト受信"] --> B["バリデーションルール適用"] B --> C{{"バリデーション
    通過?"}} C -->|"成功"| D["バリデーション済みデータ取得"] D --> E["コントローラーロジック実行"] E --> F["成功レスポンス"] C -->|"失敗
    (Webリクエスト)"| G["前の画面にリダイレクト
    エラーをセッションに保存"] C -->|"失敗
    (APIリクエスト)"| H["422 Unprocessable Entity
    JSONエラーレスポンス"] ``` ## コントローラーでのバリデーション ### `$request->validate()` の使い方 コントローラーのメソッド内で `$request->validate()` を呼び出すのが、最もシンプルなバリデーション方法です。 バリデーションに失敗すると、Laravelはユーザーを前の画面に自動でリダイレクトし、エラー情報をセッションに保存します。 ルートを定義します。 ```php theme={null} use App\Http\Controllers\PostController; Route::get('/post/create', [PostController::class, 'create']); Route::post('/post', [PostController::class, 'store']); ``` コントローラーを作成します。 ```php theme={null} validate([ 'title' => 'required|string|max:255', 'body' => 'required|string', 'email' => 'required|email', ]); // バリデーション済みのデータで投稿を保存する... return redirect('/posts'); } } ``` `validate` メソッドにはフィールド名をキー、ルールを値とした配列を渡します。 ルールはパイプ `|` で区切るか、配列で指定できます。 ```php theme={null} // パイプ区切り 'title' => 'required|string|max:255', // 配列形式 'title' => ['required', 'string', 'max:255'], ``` ### 主要なバリデーションルール | ルール | 説明 | | - | - | | `required` | 値が存在し、空でないこと | | `string` | 文字列であること | | `email` | 有効なメールアドレス形式であること | | `min:値` | 文字列の長さ、または数値が指定値以上であること | | `max:値` | 文字列の長さ、または数値が指定値以下であること | | `unique:テーブル` | 指定したテーブルで値が一意であること | | `nullable` | 値が `null` でも許可する | | `integer` | 整数であること | | `boolean` | 真偽値(`true`/`false`、`1`/`0` など)であること | | `date` | 有効な日付形式であること | | `confirmed` | `フィールド名_confirmation` フィールドと一致すること | | `array_keys:キー1,キー2,...` | 配列のキーが指定したキーだけで構成されていること | ### メールアドレスの詳細な検証 `email` ルールには、検証方法を指定するパラメータを追加できます。 ```php theme={null} 'email' => ['email:rfc,dns'], ``` `rfc` は RFC に従った形式を、`dns` はメールアドレスのドメインに有効な MX レコードがあることを検証します。`dns` は実際に DNS lookup を実行しますが、個別のメールボックスが存在するかどうかは判定しません。 fluent な Rule builder を使って同じ検証を記述することもできます。 ```php theme={null} use Illuminate\Validation\Rule; $request->validate([ 'email' => [ 'required', Rule::email() ->rfcCompliant(strict: false) ->validateMxRecord() ->preventSpoofing(), ], ]); ``` `dns` バリデーターは実際に DNS lookup を実行し、メールアドレスのドメインに有効な MX レコードがあることを確認します。ただし、個別のメールボックスが存在するかどうかは判定しません。 テストでは live DNS lookup に依存しないよう、`Validator::fakeDnsLookups` を使って `dns` lookup を fake できます。`rfc` など他の指定した検証は通常どおり実行されます。 ```php theme={null} use Illuminate\Support\Facades\Validator; Validator::fakeDnsLookups(); ``` 既存のバリデーションルールを使ったまま、次のようなルールをテストできます。 ```php theme={null} 'email' => ['required', 'email:rfc,dns'], ``` `dns` と `spoof` の検証には PHP の `intl` 拡張機能が必要です。 ### 配列のキーを検証する `array_keys` ルールは、対象の値がPHPの配列であり、すべてのキーが指定した一覧に含まれていることを検証します。少なくとも1つのキーを指定してください。 ```php theme={null} 'user' => ['array_keys:name,username'], ``` ルールオブジェクトを使う場合は、`Rule::arrayKeys` メソッドで指定できます。 ```php theme={null} use Illuminate\Validation\Rule; 'user' => [Rule::arrayKeys('name', 'username')], ``` 利用可能なすべてのバリデーションルールは[公式ドキュメント](https://laravel.com/docs/validation#available-validation-rules)で確認できます。 ### バリデーション済みデータの利用 `validate` メソッドの戻り値は、バリデーションに通ったデータのみを含む配列です。 信頼できるデータとして安全に使えます。 ```php theme={null} $validated = $request->validate([ 'title' => 'required|string|max:255', 'body' => 'required|string', ]); // $validated は ['title' => '...', 'body' => '...'] の形式 Post::create($validated); ``` ## Bladeテンプレートでのエラー表示 バリデーションに失敗すると、Laravelは `$errors` 変数をすべてのビューで自動的に利用できるようにします。 `web` ミドルウェアグループに含まれる `ShareErrorsFromSession` ミドルウェアがこれを担っています。 ### すべてのエラーを一覧表示する ```blade theme={null} @if ($errors->any())
      @foreach ($errors->all() as $error)
    • {{ $error }}
    • @endforeach
    @endif ``` ### フィールドごとにエラーを表示する `@error` ディレクティブを使うと、特定のフィールドのエラーをインラインで表示できます。 ```blade theme={null} @error('title')
    {{ $message }}
    @enderror ``` `old('フィールド名')` を使うと、バリデーション失敗後もユーザーが入力した値をフォームに再表示できます。 ## フォームリクエスト ### フォームリクエストとは バリデーションロジックが複雑になってきた場合は、「フォームリクエスト」クラスに切り出すのが効果的です。 フォームリクエストはバリデーションと認可のロジックをひとつのクラスにまとめたカスタムリクエストクラスです。 ```mermaid theme={null} flowchart TD A["コントローラーメソッド呼び出し"] --> B["FormRequestのインスタンス生成"] B --> C["authorize()メソッド実行"] C --> D{{"認可OK?"}} D -->|"false"| E["403 Forbiddenレスポンス"] D -->|"true"| F["rules()メソッド実行"] F --> G["バリデーション実行"] G --> H{{"バリデーション
    通過?"}} H -->|"失敗"| I["バリデーション
    エラーレスポンス"] H -->|"成功"| J["コントローラーメソッド実行"] ``` ### フォームリクエストの作成 `make:request` Artisanコマンドでフォームリクエストクラスを生成します。 ```shell theme={null} php artisan make:request StorePostRequest ``` `app/Http/Requests/StorePostRequest.php` が生成されます。 ```php theme={null} |string> */ public function rules(): array { return [ 'title' => 'required|string|max:255', 'body' => 'required|string', 'email' => 'required|email|unique:posts', ]; } } ``` ### `rules()` メソッド バリデーションルールを配列で返します。コントローラーの `validate` メソッドに渡す配列と同じ形式です。 ### `authorize()` メソッド このリクエストを実行する権限があるかを判定します。 `true` を返すと認可OK、`false` を返すと403レスポンスが自動的に返されます。 認証済みユーザーのみ許可する場合、`auth()->check()` で確認できます。 チュートリアルでは `true` を返しておけば問題ありません。 `authorize` メソッドが `false` を返すと、コントローラーのメソッドは実行されず、403 Forbiddenレスポンスが返されます。 ### コントローラーへの注入 フォームリクエストはコントローラーメソッドのタイプヒントとして指定するだけで使えます。 コントローラーが呼び出される前にバリデーションが実行されるため、メソッド内にバリデーションコードを書く必要はありません。 ```php theme={null} use App\Http\Requests\StorePostRequest; class PostController extends Controller { public function store(StorePostRequest $request): RedirectResponse { // ここに到達した時点でバリデーション済み $validated = $request->validated(); Post::create($validated); return redirect('/posts'); } } ``` `$request->validated()` でバリデーション済みのデータのみを取得できます。 ## パスワードの複雑性と長さを指定する `Password` ルールオブジェクトを使うと、パスワードの複雑性と最小・最大文字数をまとめて指定できます。 ```php theme={null} use Illuminate\Validation\Rules\Password; Password::min(16) ->max(256) ->letters() ->mixedCase() ->numbers() ->symbols(); ``` `min()` は最小文字数、`max()` は最大文字数を指定します。`max()` を指定すると、複雑なパスワードを許可しながら、保存や外部サービス連携で想定する上限も明示できます。 ## 実践例:投稿フォームの作成と保存 フォームの表示から送信・バリデーション・保存までの一連のフローです。 ```php theme={null} use App\Http\Controllers\PostController; Route::get('/posts/create', [PostController::class, 'create']); Route::post('/posts', [PostController::class, 'store']); ``` ```shell theme={null} php artisan make:request StorePostRequest ``` ```php theme={null} public function rules(): array { return [ 'title' => 'required|string|max:255', 'body' => 'required|string', ]; } public function authorize(): bool { return true; } ``` ```php theme={null} use App\Http\Requests\StorePostRequest; use App\Models\Post; class PostController extends Controller { public function create(): View { return view('posts.create'); } public function store(StorePostRequest $request): RedirectResponse { Post::create($request->validated()); return redirect('/posts'); } } ``` ```blade theme={null} {{-- resources/views/posts/create.blade.php --}}

    新しい投稿

    @if ($errors->any())
      @foreach ($errors->all() as $error)
    • {{ $error }}
    • @endforeach
    @endif
    @csrf
    @error('title') {{ $message }} @enderror
    @error('body') {{ $message }} @enderror
    ```
    Bladeフォームには必ず `@csrf` ディレクティブを含めてください。CSRFトークンがないとLaravelは419エラーを返します。 ## 次のステップ リクエストオブジェクトを使ったデータ取得方法を振り返ります。 # メール認証(Email Verification) Source: https://kawax.biz/jp/verification Laravelのメール認証機能を実装する手順を解説します。モデル準備、3つのルート定義、verifiedミドルウェア、カスタマイズ、イベントを学べます。 ## はじめに メール認証(Email Verification)は、ユーザーが登録したメールアドレスを実際に所有しているか確認する仕組みです。Laravelは確認メールの送信と検証処理を標準で提供するため、あなたは最小限の実装で安全な認証フローを構築できます。 スターターキットを使う場合は、認証画面とメール認証フローがすでに組み込まれています。手動実装する場合は、このページの手順でモデル・ルート・ビューを自分で定義してください。 ## モデルとデータベースの準備 ### MustVerifyEmail を実装する `App\Models\User` が `Illuminate\Contracts\Auth\MustVerifyEmail` を実装していることを確認してください。 ```php theme={null} 未認証ユーザーに「メールを確認してください」と表示する画面を返します。 ```php theme={null} Route::get('/email/verify', function () { return view('auth.verify-email'); })->middleware('auth')->name('verification.notice'); ``` メール内リンクを踏んだときに、署名付きURLを検証して認証完了します。 ```php theme={null} use Illuminate\Foundation\Auth\EmailVerificationRequest; Route::get('/email/verify/{id}/{hash}', function (EmailVerificationRequest $request) { $request->fulfill(); return redirect('/home'); })->middleware(['auth', 'signed'])->name('verification.verify'); ``` ユーザーが確認メールを紛失したときに再送信します。 ```php theme={null} use Illuminate\Http\Request; Route::post('/email/verification-notification', function (Request $request) { $request->user()->sendEmailVerificationNotification(); return back()->with('message', 'Verification link sent!'); })->middleware(['auth', 'throttle:6,1'])->name('verification.send'); ``` ## ルートの保護 認証済みユーザーだけにアクセスを許可するには、`verified` ミドルウェアを使います。通常は `auth` と組み合わせます。 ```php theme={null} Route::get('/profile', function () { // 認証済みユーザーのみアクセス可能 })->middleware(['auth', 'verified']); ``` 未認証ユーザーがアクセスすると、Laravelは自動的に `verification.notice` へリダイレクトします。 ## カスタマイズ 確認メール本文をカスタマイズするには、`AppServiceProvider` の `boot` メソッドで `VerifyEmail::toMailUsing` を設定します。 ```php theme={null} use Illuminate\Auth\Notifications\VerifyEmail; use Illuminate\Notifications\Messages\MailMessage; public function boot(): void { VerifyEmail::toMailUsing(function (object $notifiable, string $url) { return (new MailMessage) ->subject('Verify Email Address') ->line('Click the button below to verify your email address.') ->action('Verify Email Address', $url); }); } ``` ## イベント メール認証時には `Illuminate\Auth\Events\Verified` イベントが発火されます。スターターキットでは自動的に扱われますが、手動実装では必要に応じてイベント処理を追加してください。 # バージョニング方針 Source: https://kawax.biz/jp/versioning Laravel のバージョニング方針、サポートポリシー概要をまとめます。 ## バージョニング方針 Laravel と公式パッケージは [Semantic Versioning](https://semver.org) に従います。メジャーバージョンは毎年(おおむね第1四半期)リリースされ、マイナーバージョンとパッチバージョンは毎週でもリリースされる可能性があります。マイナーバージョンとパッチバージョンに破壊的変更が含まれることは**ありません**。 アプリケーションやパッケージから Laravel を利用する場合は、`^13.0` のようなバージョン制約を指定してください。Laravel のメジャーバージョンには破壊的変更が含まれるため、制約を指定しないと意図しないアップデートが発生します。一方で、新しいメジャーバージョンへは1日以内に移行できるよう努めています。 ### 名前付き引数 [名前付き引数](https://www.php.net/manual/ja/functions.arguments.php#functions.named-arguments)は Laravel の後方互換性ガイドラインの対象外です。コードベースの改善のため、必要に応じて引数名が変更される可能性があります。Laravel のメソッドを呼び出す際に名前付き引数を使う場合は、将来引数名が変わる可能性を理解したうえで慎重に使ってください。 ## サポートポリシー すべての Laravel リリースについて、バグ修正は18か月間、セキュリティ修正は2年間提供されます。その他の追加ライブラリは、最新のメジャーバージョンだけがバグ修正の対象になります。また、Laravel がサポートするデータベースのバージョンも併せて確認してください([データベース](/jp/database))。 | バージョン | PHP (\*) | リリース日 | バグ修正の期限 | セキュリティ修正の期限 | | - | - | - | - | - | | 10 | 8.1 - 8.3 | 2023年2月14日 | 2024年8月6日 | 2025年2月4日 | | 11 | 8.2 - 8.4 | 2024年3月12日 | 2025年9月3日 | 2026年3月12日 | | 12 | 8.2 - 8.5 | 2025年2月24日 | 2026年8月13日 | 2027年2月24日 | | 13 | 8.3 - 8.5 | 2026年3月17日 | 2027年第3四半期 | 2028年3月17日 | (\*) サポートされている PHP のバージョン # ビュー Source: https://kawax.biz/jp/views Laravelのビューを作成してルートやコントローラーから返す方法、データの渡し方を解説します。 ## はじめに ルートやコントローラーからHTMLドキュメント全体を文字列で返すことは現実的ではありません。 ビューを使うと、すべてのHTMLを独立したファイルに記述できます。 ビューはコントローラー・アプリケーションロジックをプレゼンテーションロジックから分離し、`resources/views` ディレクトリに格納されます。 Laravelでは通常、[Bladeテンプレート言語](/jp/blade)を使ってビューを記述します。 ```blade theme={null}

    こんにちは、{{ $name }}

    ``` このビューは `resources/views/greeting.blade.php` に格納されており、グローバルな `view` ヘルパーを使って返せます。 ```php theme={null} Route::get('/', function () { return view('greeting', ['name' => '太郎']); }); ``` ## ビューの作成 ビューを作成するには、`resources/views` ディレクトリに `.blade.php` 拡張子のファイルを置くか、Artisanコマンドを使います。 ```shell theme={null} php artisan make:view greeting ``` `.blade.php` 拡張子はフレームワークに、そのファイルが[Bladeテンプレート](/jp/blade)を含むことを知らせます。 ## ビューの返し方 ビューを作成したら、ルートやコントローラーからグローバルな `view` ヘルパーを使って返せます。 ```php theme={null} Route::get('/', function () { return view('greeting', ['name' => '太郎']); }); ``` `View` ファサードを使っても返せます。 ```php theme={null} use Illuminate\Support\Facades\View; return View::make('greeting', ['name' => '太郎']); ``` `view` ヘルパーの第1引数は `resources/views` ディレクトリ内のビューファイル名に対応します。 第2引数はビューで利用可能にするデータの配列です。 ### ネストしたビューディレクトリ ビューは `resources/views` のサブディレクトリに入れ子にできます。 ネストしたビューを参照するには「ドット」記法を使います。 例えば `resources/views/admin/profile.blade.php` に格納したビューは次のように返せます。 ```php theme={null} return view('admin.profile', $data); ``` ビューのディレクトリ名には `.` 文字を含めないでください。 ### 最初に存在するビューの返し方 `View` ファサードの `first` メソッドを使うと、ビューの配列の中で最初に存在するビューを返せます。 ```php theme={null} use Illuminate\Support\Facades\View; return View::first(['custom.admin', 'admin'], $data); ``` ### ビューの存在確認 ビューが存在するか確認するには、`View` ファサードの `exists` メソッドを使います。 ```php theme={null} use Illuminate\Support\Facades\View; if (View::exists('admin.profile')) { // ... } ``` ## ビューへのデータの渡し方 前の例で見たように、データの配列をビューに渡してビュー内でそのデータを利用できます。 ```php theme={null} return view('greetings', ['name' => '花子']); ``` この方法では、データはキーと値のペアの配列である必要があります。 ビュー内ではデータのキーを使って各値にアクセスできます。 `with` メソッドを使って個別にデータを追加することもできます。 `with` メソッドはビューオブジェクトのインスタンスを返すのでメソッドチェーンが使えます。 ```php theme={null} return view('greeting') ->with('name', '花子') ->with('occupation', 'エンジニア'); ``` ## すべてのビューでデータを共有する すべてのビューでデータを共有したい場合は、`View` ファサードの `share` メソッドを使います。 通常はサービスプロバイダーの `boot` メソッド内に記述します。 ```php theme={null} Bladeの構文を使って動的なビューを構築する方法を学びます。 # AI SDKのカスタムプロバイダーを作る Source: https://kawax.biz/jp/advanced/ai-sdk-custom-provider Laravel AI SDKのソースコードを読み解き、標準で提供されていないAIサービスに対応するカスタムプロバイダーを実装する方法を解説します。 ## カスタムプロバイダーが必要な場面 Laravel AI SDKは OpenAI、Anthropic、Gemini、Mistral など主要なAIサービスを標準でサポートしています。しかし次のようなケースでは標準プロバイダーでは対応できません。 * まだ公式対応されていない新興のAIサービス * 社内のモデルゲートウェイや課金管理レイヤーを経由させたい * 独自プロトコルや認証方式を持つオンプレミス推論サーバー このような場合に、カスタムプロバイダーを実装してSDKの `AiManager` に登録することで、標準プロバイダーと同じAPIで利用できます。 **OpenAI互換APIの場合は組み込みドライバーを使う** SDK 0.9以降、`openai-compatible` ドライバーが標準で提供されています。社内のOpenAI互換推論サーバー(Ollama の OpenAI互換エンドポイントなど)には `config/ai.php` に設定を追加するだけで利用でき、カスタムプロバイダーの実装は不要です。 ```php theme={null} 'my-server' => [ 'driver' => 'openai-compatible', 'key' => env('MY_SERVER_API_KEY'), 'url' => env('MY_SERVER_URL', 'http://localhost:8080/v1'), 'models' => [ 'text' => ['default' => 'llama3.3-70b'], ], ], ``` ## アーキテクチャの概要 ### 2層構造 Laravel AI SDKはプロバイダーとゲートウェイの2層で構成されています。 | レイヤー | 役割 | 例 | | - | - | - | | **Provider** | アプリ側のインターフェース。モデル名の解決、設定の保持 | `OpenAiProvider`、`AnthropicProvider` | | **Gateway** | 実際のAPIリクエストを1ステップ分送信する | `AnthropicGateway`、`OpenAiCompatibleGateway` | すべてのプロバイダーは抽象クラス `Laravel\Ai\Providers\Provider` を継承し、機能ごとのコントラクト(インターフェース)を実装します。ツール呼び出しを含むマルチステップのループは `TextGenerationLoop` がゲートウェイを繰り返し呼び出すことで実現されます。 ### コントラクト一覧 提供したい機能に応じて必要なコントラクトだけを実装します。 | コントラクト | 名前空間 | 機能 | | - | - | - | | `TextProvider` | `Laravel\Ai\Contracts\Providers\TextProvider` | テキスト生成・エージェント | | `EmbeddingProvider` | `Laravel\Ai\Contracts\Providers\EmbeddingProvider` | ベクトル埋め込み生成 | | `ImageProvider` | `Laravel\Ai\Contracts\Providers\ImageProvider` | 画像生成 | | `AudioProvider` | `Laravel\Ai\Contracts\Providers\AudioProvider` | 音声合成(TTS) | | `TranscriptionProvider` | `Laravel\Ai\Contracts\Providers\TranscriptionProvider` | 音声認識(STT) | ほとんどの場合は `TextProvider` だけ実装すれば十分です。 ## TextProviderコントラクト テキスト生成プロバイダーが実装するインターフェースです(`src/Contracts/Providers/TextProvider.php`)。 ```php theme={null} interface TextProvider extends Provider { public function prompt(AgentPrompt $prompt): AgentResponse; public function stream(AgentPrompt $prompt): StreamableAgentResponse; public function useTextGateway(StepTextGateway $gateway): self; public function textGenerationLoop(): TextGenerationLoop; public function defaultTextModel(): string; public function cheapestTextModel(): string; public function smartestTextModel(): string; } ``` `prompt()` と `stream()` の実装は既存のトレイト(`GeneratesText`、`StreamsText`)に任せられるため、実際に実装が必要なのはモデル名を返す3つのメソッドと `textGateway()` の合計4つです。マルチステップのツールループは `TextGenerationLoop` が管理するため、ゲートウェイは1ステップ分のリクエストのみを処理します。 ## 実装例:カスタムプロバイダー 独自APIを持つ推論サービスを `my-inference` というプロバイダーとして登録する例です。 `app/Ai/Providers/MyInferenceProvider.php` を作成します。 ```php theme={null} $this->config['key'] ?? null]; } /** * Get the provider's text gateway. */ public function textGateway(): StepTextGateway { return $this->textGateway ??= new \App\Ai\Gateway\MyInferenceGateway($this->events); } public function defaultTextModel(): string { return $this->config['models']['text']['default'] ?? 'my-model-v1'; } public function cheapestTextModel(): string { return $this->config['models']['text']['cheapest'] ?? 'my-model-v1'; } public function smartestTextModel(): string { return $this->config['models']['text']['smartest'] ?? 'my-model-v1'; } } ``` `App\Providers\AppServiceProvider` の `boot` メソッドで `extend()` を使って登録します。 ```php theme={null} app->make(AiManager::class)->extend( 'my-inference', fn (array $config) => new MyInferenceProvider( $config, $this->app->make(Dispatcher::class) ) ); } } ``` ```php theme={null} 'providers' => [ // ...既存のプロバイダー... 'my-inference' => [ 'driver' => 'my-inference', 'key' => env('MY_INFERENCE_API_KEY'), 'url' => env('MY_INFERENCE_URL', 'http://localhost:8080'), ], ], ``` `.env` にも追加します。 ```ini theme={null} MY_INFERENCE_API_KEY=your-api-key MY_INFERENCE_URL=https://inference.example.internal ``` 登録後は `prompt()` の `provider` 引数にプロバイダー名を指定するだけで標準プロバイダーと同じように使えます。 ```php theme={null} use App\Ai\Agents\SummaryAgent; $response = SummaryAgent::make()->prompt('この記事を要約してください。', provider: 'my-inference'); echo $response->text; ``` デフォルトのプロバイダーとして使う場合は `config/ai.php` の `default` キーを変更します。 ```php theme={null} 'default' => 'my-inference', ``` ## カスタムゲートウェイの実装 OpenAI互換ではない独自APIを持つサービスには、`StepTextGateway` コントラクトを実装したカスタムゲートウェイが必要です。 ### StepTextGatewayコントラクト `src/Contracts/Gateway/StepTextGateway.php` が定義するインターフェースです。ゲートウェイは会話の**1ステップ分**のリクエストを処理し、`StepResponse` を返します。ツール呼び出しのループは呼び出し元の `TextGenerationLoop` が管理します。 ```php theme={null} interface StepTextGateway { public function generateTextStep( TextProvider $provider, string $model, ?string $instructions, array $messages, array $tools, ?array $schema, ?TextGenerationOptions $options, ?int $timeout, StepContext $stepContext, ): StepResponse; public function generateStreamStep( string $invocationId, TextProvider $provider, string $model, ?string $instructions, array $messages, array $tools, ?array $schema, ?TextGenerationOptions $options, ?int $timeout, StepContext $stepContext, ): Generator; } ``` 0.8以前の `TextGateway` コントラクト(`generateText()`、`stream()`、`onToolInvocation()`)は0.9で削除されました。カスタムゲートウェイを持っている場合は `StepTextGateway` への移行が必要です。ツール呼び出しの `onToolInvocation()` は `TextGenerationLoop` に移動しています。 ### カスタムゲートウェイの実装例 独自の推論APIにHTTPリクエストを送るシンプルなゲートウェイの骨格です。 ```php theme={null} providerCredentials(); $config = $provider->additionalConfiguration(); $response = Http::withToken($credentials['key']) ->baseUrl($config['url']) ->timeout($timeout ?? 30) ->post('/generate', [ 'model' => $model, 'instructions' => $instructions, 'messages' => $this->formatMessages($messages), ]); $data = $response->json(); return new StepResponse( text: $data['output']['text'] ?? '', toolCalls: [], finishReason: FinishReason::Stop, usage: new Usage( promptTokens: $data['usage']['input_tokens'] ?? 0, completionTokens: $data['usage']['output_tokens'] ?? 0, ), meta: new Meta( provider: $provider->name(), model: $model, ), ); } public function generateStreamStep( string $invocationId, TextProvider $provider, string $model, ?string $instructions, array $messages, array $tools, ?array $schema, ?TextGenerationOptions $options, ?int $timeout, StepContext $stepContext, ): Generator { // ストリーミングの実装(省略) yield from []; return null; } protected function formatMessages(array $messages): array { return array_map(fn ($message) => [ 'role' => $message->role->value, 'content' => $message->content, ], $messages); } } ``` ツール呼び出し(function calling)をサポートする場合は `generateTextStep()` でツールの呼び出し結果を `toolCalls` に含め、`finishReason` を `FinishReason::ToolCalls` にします。ツールの実行と次のステップへの移行は `TextGenerationLoop` が自動で処理します。実装の参考には `AnthropicGateway.php` を参照してください。 ## テスト方法 ### エージェントクラスの fake() を使う カスタムプロバイダーを使うエージェントのテストには、エージェントクラスの `fake()` メソッドを使います。カスタムプロバイダーかどうかに関係なく、フェイクゲートウェイがプロバイダーにセットされます。 ```php theme={null} prompt('長い記事のテキスト...', provider: 'my-inference'); $this->assertEquals('これは要約です。', $response->text); SummaryAgent::assertPrompted('長い記事のテキスト...'); } } ``` 0.9以降、`Agent::fake()` のレスポンスは実際のプロバイダーと同じ `TextGenerationLoop` を通ります。ツールを登録していないエージェントでフェイクのツール呼び出しを設定した場合、`NoSuchToolException` がスローされます。 ### extend() を使ったモックプロバイダー `extend()` を使って、テスト用のプロバイダーをコンテナから登録することもできます。 ```php theme={null} public function test_with_mock_provider(): void { $this->app->make(AiManager::class)->extend( 'my-inference', function (array $config) { $events = $this->app->make(\Illuminate\Contracts\Events\Dispatcher::class); return new MyInferenceProvider($config, $events); } ); // テストコード } ``` ## 参考リンク ローカルモデルサーバーに接続するプロバイダーの最小構成です。カスタムプロバイダー実装の参考になります。 `StepTextGateway` を実装したゲートウェイの実装例です。`generateTextStep()` と `generateStreamStep()` の実装が確認できます。 テキスト生成ゲートウェイが実装するインターフェースの定義です。 テキスト生成プロバイダーが実装するインターフェースの定義です。 # Laravel 11以降のアプリケーション構造 Source: https://kawax.biz/jp/advanced/app-structure Laravel 11で導入されたSlim Application Skeletonの全貌と、Application::configure()からApplicationBuilderまでの内部実装を解説します。 ## Laravel 10以前との比較 Laravel 11では「Slim Application Skeleton」として、アプリケーション構造が大幅に刷新されました。最大の変化は、設定の分散をなくし `bootstrap/app.php` 一か所に集約したことです。 | 項目 | Laravel 10以前 | Laravel 11以降 | | - | - | - | | HTTPカーネル | `app/Http/Kernel.php` | 廃止(フレームワーク内に統合) | | コンソールカーネル | `app/Console/Kernel.php` | 廃止(`routes/console.php` に移行) | | 例外ハンドラー | `app/Exceptions/Handler.php` | 廃止(`bootstrap/app.php` に集約) | | サービスプロバイダー | 5ファイル | `AppServiceProvider.php` 1ファイル | | ルートファイル | `web.php` / `api.php` がデフォルト | `web.php` のみデフォルト、`api.php` はオプトイン | | ブートストラップ | 設定が複数ファイルに分散 | `bootstrap/app.php` に集約 | | デフォルトDB | MySQL/PostgreSQL | SQLite | この変更は**新規プロジェクト向け**です。既存の Laravel 10 アプリケーションをアップグレードしても、古い構造はそのまま動作します。 ## 新しいディレクトリ・ファイル構成 ### スケルトンのディレクトリ構成 ``` laravel-app/ ├── app/ │ ├── Http/ │ │ └── Controllers/ │ ├── Models/ │ │ └── User.php │ └── Providers/ │ └── AppServiceProvider.php ├── bootstrap/ │ ├── app.php ← アプリケーション設定の中心 │ ├── cache/ │ └── providers.php ← サービスプロバイダー一覧 ├── config/ ├── database/ ├── public/ │ └── index.php ← エントリーポイント ├── resources/ ├── routes/ │ ├── web.php ← Webルート(デフォルト) │ └── console.php ← Artisanコマンドとスケジュール ├── storage/ └── tests/ ``` ### `bootstrap/app.php` — アプリ設定の中心 ```php theme={null} withRouting( web: __DIR__.'/../routes/web.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { // }) ->withExceptions(function (Exceptions $exceptions): void { // })->create(); ``` このファイル1つで、ルーティング・ミドルウェア・例外ハンドリングを設定できます。Laravel 10 以前では `app/Http/Kernel.php`・`app/Console/Kernel.php`・`app/Exceptions/Handler.php` の3ファイルに分散していた設定が、ここに集約されています。 ### `bootstrap/providers.php` — サービスプロバイダー一覧 ```php theme={null} `composer require` でパッケージをインストールすると、そのパッケージが `bootstrap/providers.php` を自動更新することがあります。`config/app.php` は参照されなくなったわけではありませんが、新規登録は `bootstrap/providers.php` が推奨場所になりました。 ### `routes/` ディレクトリの変更 ``` routes/ ├── web.php ← Webルート(デフォルトで読み込まれる) └── console.php ← Artisanコマンドとスケジュールを定義する ``` `api.php` と `channels.php` はデフォルトでは存在しません。必要に応じて Artisan コマンドで生成します。 ```shell theme={null} # APIルートを追加する(api.php + Sanctumをインストール) php artisan install:api # ブロードキャストを追加する(channels.php + Reverb などをインストール) php artisan install:broadcasting ``` `routes/console.php` ではスケジュールも定義できます。 ```php theme={null} comment(Inspiring::quote()); })->purpose('Display an inspiring quote'); Schedule::command('emails:send')->daily(); ``` ### 廃止されたファイル HTTP カーネルはフレームワーク内の `Illuminate\Foundation\Http\Kernel` に統合されました。ミドルウェアのカスタマイズは `bootstrap/app.php` の `withMiddleware()` で行います。 ```php theme={null} // Laravel 10 以前: app/Http/Kernel.php protected $middleware = [ \Illuminate\Http\Middleware\TrustProxies::class, // ... ]; protected $middlewareGroups = [ 'web' => [ \App\Http\Middleware\EncryptCookies::class, // ... ], ]; ``` ```php theme={null} // Laravel 11 以降: bootstrap/app.php ->withMiddleware(function (Middleware $middleware) { $middleware->web(append: [ EnsureUserIsSubscribed::class, ]); $middleware->validateCsrfTokens(except: ['stripe/*']); }) ``` コンソールカーネルの2つの役割が分離されました。Artisanコマンドは `app/Console/Commands/` に置き自動検出され、スケジュールは `routes/console.php` に記述します。 ```php theme={null} // Laravel 10 以前: app/Console/Kernel.php protected function schedule(Schedule $schedule): void { $schedule->command('emails:send')->daily(); } ``` ```php theme={null} // Laravel 11 以降: routes/console.php use Illuminate\Support\Facades\Schedule; Schedule::command('emails:send')->daily(); ``` 例外ハンドラーはフレームワーク内の `Illuminate\Foundation\Exceptions\Handler` に統合されました。カスタマイズは `bootstrap/app.php` の `withExceptions()` で行います。 ```php theme={null} // Laravel 10 以前: app/Exceptions/Handler.php public function register(): void { $this->reportable(function (InvalidOrderException $e) { // ... }); } ``` ```php theme={null} // Laravel 11 以降: bootstrap/app.php ->withExceptions(function (Exceptions $exceptions) { $exceptions->report(function (InvalidOrderException $e) { // ... }); }) ``` ## `Application::configure()` の仕組み ### フレームワーク内部での実装 `Application::configure()` は `Illuminate\Foundation\Application` の静的メソッドです。 ```php theme={null} // Illuminate\Foundation\Application より public static function configure(?string $basePath = null) { $basePath = match (true) { is_string($basePath) => $basePath, default => static::inferBasePath(), }; return (new Configuration\ApplicationBuilder(new static($basePath))) ->withKernels() ->withEvents() ->withCommands() ->withProviders(); } ``` このメソッドは次の処理を行います。 1. `basePath` からアプリケーションのルートディレクトリを決定する 2. `Application` インスタンスを生成する 3. `ApplicationBuilder` でラップし、デフォルトの設定を適用する 4. `ApplicationBuilder` インスタンスを返す 重要なのは、`configure()` 内で **すでに `withKernels()` / `withEvents()` / `withCommands()` / `withProviders()` が呼ばれている** 点です。`bootstrap/app.php` でこれらを改めて呼ぶ必要はありません。 ### メソッドチェーンで設定を行う流れ ```php theme={null} Application::configure(basePath: dirname(__DIR__)) // ApplicationBuilder を返す ->withRouting(...) // ルーティングを設定し $this を返す ->withMiddleware(...) // ミドルウェアを設定し $this を返す ->withExceptions(...) // 例外ハンドリングを設定し $this を返す ->create(); // Application インスタンスを返す ``` `create()` の呼び出しで `ApplicationBuilder` から `Application` インスタンスが取り出され、`bootstrap/app.php` が `return` するのはこの `Application` インスタンスです。 ## リクエストからアプリ起動までの流れ ```mermaid theme={null} flowchart TD A["public/index.php
    エントリーポイント"] --> B["bootstrap/app.php
    を require して Application を取得"] B --> C["Application::configure()
    ApplicationBuilder を生成"] C --> D["withKernels()
    HTTP/Consoleカーネルを登録"] D --> E["withEvents()
    イベントディスカバリを設定"] E --> F["withCommands()
    Artisanコマンドのパスを設定"] F --> G["withProviders()
    bootstrap/providers.php を読み込む"] G --> H["withRouting()
    ルーティングを設定"] H --> I["withMiddleware()
    ミドルウェアを設定"] I --> J["withExceptions()
    例外ハンドリングを設定"] J --> K["create()
    Application インスタンスを返す"] K --> L["HTTP Kernel がリクエストを処理
    ミドルウェア → ルーター → コントローラー"] L --> M["レスポンスを返す"] ``` `public/index.php` がエントリーポイントとなり、`bootstrap/app.php` を読み込んで `Application` を取得します。その後、HTTP Kernel がリクエストをミドルウェアのパイプラインに通し、ルーターがコントローラーにディスパッチします。 ## `ApplicationBuilder` の主要メソッド深掘り ### `withRouting()` — ルーティング登録の内部処理 ```php theme={null} public function withRouting( ?Closure $using = null, array|string|null $web = null, array|string|null $api = null, ?string $commands = null, ?string $channels = null, ?string $pages = null, ?string $health = null, string $apiPrefix = 'api', ?callable $then = null ) ``` 内部では `AppRouteServiceProvider::loadRoutesUsing()` にコールバックを登録し、アプリケーションの booting 時に `AppRouteServiceProvider` を登録します。 ```php theme={null} // withRouting() の内部処理(簡略版) protected function buildRoutingCallback(...) { return function () use ($web, $api, $pages, $health, $apiPrefix, $then) { if (is_string($api) || is_array($api)) { Route::middleware('api')->prefix($apiPrefix)->group($api); } if (is_string($health)) { Route::get($health, function () { Event::dispatch(new DiagnosingHealth); return response(View::file(...), status: 200); }); } if (is_string($web) || is_array($web)) { Route::middleware('web')->group($web); } if (is_callable($then)) { $then($this->app); } }; } ``` **ポイント:** * `api` ルートは `api` ミドルウェアグループと `/api` プレフィックスが自動的に適用される * `health` に文字列を渡すとヘルスチェックエンドポイント(デフォルト `/up`)が自動登録される * `health` のパスはメンテナンスモード中でも除外される(`PreventRequestsDuringMaintenance::except()` で設定) * `web` より先に `api` が登録されることに注意。同じパスに対してWebとAPIのルートを定義すると、APIルートが優先される * `pages` に文字列を渡すと [Laravel Folio](https://github.com/laravel/folio) のルーティングが有効になる ### `withMiddleware()` — ミドルウェアカスタマイズ ```php theme={null} public function withMiddleware(?callable $callback = null) { $this->app->afterResolving(HttpKernel::class, function ($kernel) use ($callback) { $middleware = (new Middleware) ->redirectGuestsTo(fn () => route('login')); if (! is_null($callback)) { $callback($middleware); } $kernel->setGlobalMiddleware($middleware->getGlobalMiddleware()); $kernel->setMiddlewareGroups($middleware->getMiddlewareGroups()); $kernel->setMiddlewareAliases($middleware->getMiddlewareAliases()); // ... }); return $this; } ``` `withMiddleware()` は `HttpKernel` が解決された**後**にコールバックを実行します。これは `afterResolving()` フックを使っているためです。コールバックに渡される `Middleware` オブジェクトには豊富なカスタマイズメソッドがあります。 ```php theme={null} ->withMiddleware(function (Middleware $middleware) { // グローバルミドルウェアを追加 $middleware->append(MyGlobalMiddleware::class); // web グループにミドルウェアを追加 $middleware->web(append: [EnsureUserIsSubscribed::class]); // api グループのミドルウェアを置き換え $middleware->api(replace: [ OldMiddleware::class => NewMiddleware::class, ]); // CSRF 除外パスを設定 $middleware->validateCsrfTokens(except: ['stripe/*', 'webhook/*']); // 未認証ユーザーのリダイレクト先を変更 $middleware->redirectGuestsTo('/custom-login'); // ミドルウェアの優先順位を設定 $middleware->priority([ \Illuminate\Session\Middleware\StartSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, ]); }) ``` ### `withExceptions()` — 例外ハンドリングの設定 ```php theme={null} public function withExceptions(?callable $using = null) { $this->app->singleton( \Illuminate\Contracts\Debug\ExceptionHandler::class, \Illuminate\Foundation\Exceptions\Handler::class ); if ($using !== null) { $this->app->afterResolving( \Illuminate\Foundation\Exceptions\Handler::class, fn ($handler) => $using(new Exceptions($handler)), ); } return $this; } ``` `withExceptions()` はフレームワークの `Handler` クラスをシングルトンとして登録した上で、コールバックを `afterResolving()` で設定します。コールバックには `Exceptions` ラッパーオブジェクトが渡されます。 ```php theme={null} ->withExceptions(function (Exceptions $exceptions) { // 特定の例外をレポートしない $exceptions->dontReport(MissedFlightException::class); // 特定の例外をカスタムレポート $exceptions->report(function (InvalidOrderException $e) { // Slack に通知など }); // 特定の例外のHTTPレスポンスをカスタマイズ $exceptions->render(function (NotFoundHttpException $e, Request $request) { if ($request->is('api/*')) { return response()->json(['message' => 'Not Found'], 404); } }); // スロットリング(同じ例外を連続でレポートしない) $exceptions->throttle(function (Throwable $e) { return Limit::perMinute(20); }); }) ``` ### `withProviders()` — サービスプロバイダー登録 ```php theme={null} public function withProviders(array $providers = [], bool $withBootstrapProviders = true) { RegisterProviders::merge( $providers, $withBootstrapProviders ? $this->app->getBootstrapProvidersPath() : null ); return $this; } ``` `withProviders()` は `Application::configure()` 内でデフォルト呼び出されるため、`bootstrap/providers.php` は自動的に読み込まれます。追加のプロバイダーを渡したい場合は `bootstrap/app.php` で明示的に呼ぶ必要があります。 ```php theme={null} Application::configure(basePath: dirname(__DIR__)) ->withProviders([ // デフォルトのbootstrap/providers.phpに加えてプロバイダーを追加する App\Providers\CustomServiceProvider::class, ]) ->withRouting(...) ->create(); ``` `withBootstrapProviders: false` を渡すと `bootstrap/providers.php` が読み込まれなくなります。特別な理由がない限り省略してください。 ### その他の主要メソッド | メソッド | 説明 | | - | - | | `withKernels()` | HTTP / Console カーネルをシングルトン登録する。`configure()` が自動呼び出し | | `withEvents()` | イベントディスカバリを有効化する。`configure()` が自動呼び出し | | `withCommands(array $commands)` | Artisan コマンドクラスまたはディレクトリを追加登録する | | `withSchedule(callable $callback)` | スケジュールを `bootstrap/app.php` で定義する | | `withBroadcasting(string $channels)` | ブロードキャストチャンネルファイルを登録する | | `withBindings(array $bindings)` | コンテナバインディングを登録する | | `withSingletons(array $singletons)` | シングルトンバインディングを登録する | | `registered(callable $callback)` | サービスプロバイダー登録後に実行されるコールバックを追加する | | `booting(callable $callback)` | booting 時に実行されるコールバックを追加する | | `booted(callable $callback)` | booted 時に実行されるコールバックを追加する | | `create()` | `Application` インスタンスを返す | ## 設計意図:なぜこうなっているか ### 「コードファースト」の設定 Laravel 10 以前の `app/Http/Kernel.php` には配列でミドルウェアを列挙するスタイルが使われていました。これは設定ファイルに近い書き方で、PHPの型システムや IDE のサポートが得られにくいという欠点がありました。 Laravel 11 では `withMiddleware(function (Middleware $middleware) { ... })` というコールバックスタイルに変わりました。これにより型補完が効き、条件分岐やループなどのロジックを使った動的な設定が自然に書けます。 ### 「規約より設定」から「明示的な設定」へ `api.php` をオプトインにしたのは、APIルートを使わないアプリケーションでも `api` ミドルウェアグループが常にロードされていたのを解消するためです。使わない機能はデフォルトで存在しない、という方針です。 ### `afterResolving()` フックの活用 `withMiddleware()` や `withExceptions()` で `afterResolving()` を使っているのは、設定の順序問題を避けるためです。`ApplicationBuilder` のメソッドはアプリケーションが完全に起動する前に呼ばれますが、実際の処理(カーネルへの設定適用)はカーネルが最初に解決されたときに遅延実行されます。 ## カスタマイズの実践例 ### APIとWebを共存させる ```php theme={null} return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', apiPrefix: 'api/v1', // デフォルトの /api から変更 ) ->withMiddleware(function (Middleware $middleware): void { // }) ->withExceptions(function (Exceptions $exceptions): void { // })->create(); ``` ### ミドルウェアのカスタマイズ ```php theme={null} ->withMiddleware(function (Middleware $middleware) { // Webルートに認証チェックミドルウェアを追加 $middleware->web(append: [ \App\Http\Middleware\EnsureEmailIsVerified::class, ]); // APIルートで特定のミドルウェアを除外 $middleware->api(remove: [ \Illuminate\Session\Middleware\StartSession::class, ]); // Webhook エンドポイントを CSRF から除外 $middleware->validateCsrfTokens(except: [ 'webhook/*', 'stripe/webhook', ]); // 特定のミドルウェアにエイリアスを設定 $middleware->alias([ 'subscribed' => \App\Http\Middleware\EnsureUserIsSubscribed::class, ]); }) ``` ### スケジュールを `bootstrap/app.php` に集約する スケジュールは `routes/console.php` に書くこともできますが、`withSchedule()` を使うと `bootstrap/app.php` にまとめられます。 ```php theme={null} use Illuminate\Console\Scheduling\Schedule; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) ->withSchedule(function (Schedule $schedule) { $schedule->command('emails:send')->daily(); $schedule->command('reports:generate')->weeklyOn(1, '8:00'); $schedule->job(new PruneOldRecords)->daily(); }) ->withMiddleware(function (Middleware $middleware): void { // }) ->withExceptions(function (Exceptions $exceptions): void { // })->create(); ``` ### 例外ハンドリングのカスタマイズ ```php theme={null} ->withExceptions(function (Exceptions $exceptions) { // APIリクエストには常にJSONで返す $exceptions->render(function (Throwable $e, Request $request) { if ($request->is('api/*') || $request->wantsJson()) { $status = match (true) { $e instanceof NotFoundHttpException => 404, $e instanceof AuthenticationException => 401, $e instanceof AuthorizationException => 403, $e instanceof ValidationException => 422, default => 500, }; return response()->json([ 'message' => $e->getMessage(), ], $status); } }); // 本番環境のみSlackに通知 if (app()->isProduction()) { $exceptions->report(function (Throwable $e) { app(SlackNotifier::class)->notify($e); })->stop(); } }) ``` ### コンテナバインディングを `bootstrap/app.php` で管理する 小規模なアプリケーションであれば、シンプルなバインディングを `AppServiceProvider` ではなく `bootstrap/app.php` に書くこともできます。 ```php theme={null} return Application::configure(basePath: dirname(__DIR__)) ->withRouting(...) ->withSingletons([ \App\Contracts\PaymentGateway::class => \App\Services\StripeGateway::class, \App\Contracts\MailService::class => \App\Services\SendgridMailService::class, ]) ->withMiddleware(...) ->withExceptions(...) ->create(); ``` ## Laravelの起動処理の順番 Laravelアプリケーションの起動時、サービスプロバイダーとApplicationのフックは以下の順番で実行されます。 1. すべてのServiceProviderの`register()`実行 2. Applicationの`registered()` 3. Applicationの`booting()` 4. すべてのServiceProviderの`boot()`実行 5. Applicationの`booted()` `AppServiceProvider`に次のコードを追加すると実行順を確認できます。 ```php theme={null} class AppServiceProvider extends ServiceProvider { public function register(): void { info('1. AppServiceProvider register'); $this->booting(function () { info('4. AppServiceProvider booting'); }); $this->booted(function () { info('5. AppServiceProvider booted'); }); $this->app->registered(function () { info('2. app registered'); }); $this->app->booting(function () { info('3. app booting'); }); $this->app->booted(function () { info('6. AppServiceProvider@register app booted'); }); } public function boot(): void { $this->app->booted(function () { info('7. AppServiceProvider@boot app booted'); }); } } ``` `ApplicationBuilder`の`registered()`、`booting()`、`booted()`はApplicationにコールバックを登録するだけのメソッドです。通常は使う必要がありませんが、起動準備が完了したKernelに対して`booted()`で変更を加えるといった特殊な操作が可能です。 ```php theme={null} // bootstrap/app.php use Illuminate\Contracts\Http\Kernel; return Application::configure(basePath: dirname(__DIR__)) ->withRouting(...) ->withMiddleware(...) ->withExceptions(...) ->booted(function (Application $app) { $kernel = $app->make(Kernel::class); // $app->instance(Kernel::class, $kernel); }) ->create(); ``` ## 次のステップ `ApplicationBuilder` が内部で使っているサービスコンテナの仕組みを理解します。 新しいアプリケーション構造に関するよくある疑問と回答をまとめています。 # Laravel 11以降の新アプリ構造 FAQ Source: https://kawax.biz/jp/advanced/app-structure-faq Laravel 11以降のSlim Application Skeletonに移行した際によくある疑問と回答をまとめました。旧構造のままアップグレードしたプロジェクトへの途中参加や、古い書籍・チュートリアルで学習した方向けです。 Laravel 11 では「Slim Application Skeleton」としてアプリケーション構造が大幅に変わりました。旧構造のままアップグレードしたプロジェクトへ途中参加した場合や、Laravel 10 以前の書籍・チュートリアルで学習した場合に戸惑いやすい点をまとめています。Laravel 11 リリース当時にLaracastsのフォーラムやStack Overflowでよくされていた質問です。 この FAQ は**新規プロジェクト**(Laravel 11 以降)を対象としています。既存プロジェクトのアップグレード手順は[移行ガイド](/jp/advanced/app-structure-migration)を参照してください。 新構造では変更頻度の低い config ファイルがプロジェクトから削除されています。これらのファイルはフレームワーク内の `config/` が使われます。 プロジェクト側の `config/` とフレームワーク側の `config/` はマージされ、**プロジェクト側が優先**されます。カスタマイズが必要になった時点でファイルを作成すれば反映されます。 ```bash theme={null} # フレームワーク内の config を確認する cat vendor/laravel/framework/config/cors.php # プロジェクト側にコピーしてカスタマイズする php artisan config:publish cors ``` Laravel 11 の新構造では `App\Http\Controllers\Controller` が空のクラスになっており、`Illuminate\Routing\Controller` を継承せず、`ValidatesRequests` / `AuthorizesRequests` トレイトも使用していません。 | Laravel 10 | Laravel 11 | | - | - | | `Illuminate\Routing\Controller` を継承 | 空のクラス | | `ValidatesRequests` トレイト使用 | なし | | `AuthorizesRequests` トレイト使用 | なし | 参考: [Laravel 10 の Controller](https://github.com/laravel/laravel/blob/10.x/app/Http/Controllers/Controller.php) vs [Laravel 11 の Controller](https://github.com/laravel/laravel/blob/11.x/app/Http/Controllers/Controller.php) **代替方法:** ```php theme={null} // $request->validate() を使う public function store(Request $request) { $validated = $request->validate([ 'title' => 'required|string|max:255', ]); } // Gate::authorize() を使う use Illuminate\Support\Facades\Gate; public function update(Request $request, Post $post) { Gate::authorize('update', $post); } ``` 頻繁に使う場合は `App\Http\Controllers\Controller` を Laravel 10 と同様に戻す方法もあります。 ```php theme={null} 新構造では `app/Http/Kernel.php` が廃止され、ミドルウェアの設定は `bootstrap/app.php` の `withMiddleware()` で行います。 ```php theme={null} // bootstrap/app.php ->withMiddleware(function (Middleware $middleware) { // TrustProxiesミドルウェアなどで設定していた項目はすべてMiddlewareクラスのメソッドで設定できます $middleware->trustProxies(at: '*'); // グローバルミドルウェアの追加 $middleware->append(\App\Http\Middleware\MyMiddleware::class); // ルートグループへのエイリアス設定 $middleware->alias([ 'my-middleware' => \App\Http\Middleware\MyMiddleware::class, ]); // Web / API ミドルウェアグループのカスタマイズ $middleware->web(append: [ \App\Http\Middleware\HandleInertiaRequests::class, ]); }) ``` カスタムミドルウェアのファイル自体は `app/Http/Middleware/` に引き続き作成します。 `$this->middleware()` は `Illuminate\Routing\Controller` の機能であり、新構造の空のベースコントローラーでは使えません。 代わりに `HasMiddleware` インターフェースを実装して `middleware()` メソッドを定義します。 ```php theme={null} use Illuminate\Routing\Controllers\HasMiddleware; use Illuminate\Routing\Controllers\Middleware; class UserController extends Controller implements HasMiddleware { public static function middleware(): array { return [ 'auth', new Middleware('log', only: ['index']), new Middleware('subscribed', except: ['index']), ]; } } ``` Laravel 13 以降では `#[Middleware]` Attribute も使用できます。 ```php theme={null} use Illuminate\Routing\Controllers\HasMiddleware; use App\Http\Middleware\EnsureTokenIsValid; #[Middleware(EnsureTokenIsValid::class)] class UserController extends Controller { // ... } ``` 参考: [コントローラーミドルウェア](https://laravel.com/docs/13.x/controllers#controller-middleware) `authorizeResource()` は `AuthorizesRequests` トレイトに依存しているため、新構造の空のベースコントローラーでは使えません。 **対応方法はいくつかあります。** **1. ベースコントローラーを Laravel 10 仕様に戻す(最も手軽)** ```php theme={null} // app/Http/Controllers/Controller.php use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Routing\Controller as BaseController; abstract class Controller extends BaseController { use AuthorizesRequests; } ``` これにより `$this->authorizeResource()` が使えるようになります。 **2. Laravel 13 の `#[Authorize]` Attribute を各メソッドに付与する** ```php theme={null} use Illuminate\Routing\Attributes\Controllers\Authorize; use App\Models\Post; class PostController extends Controller { #[Authorize('view', 'post')] public function show(Post $post) { /* ... */ } #[Authorize('update', 'post')] public function update(Request $request, Post $post) { /* ... */ } } ``` 新構造では `app/Console/Kernel.php` が廃止され、スケジュールの設定場所が変わりました。 **`routes/console.php` に記述する(推奨):** ```php theme={null} // routes/console.php use Illuminate\Support\Facades\Schedule; Schedule::command('emails:send')->daily(); Schedule::job(new SendEmails)->everyFiveMinutes(); ``` **`bootstrap/app.php` に記述する:** ```php theme={null} ->withSchedule(function (Schedule $schedule) { $schedule->command('emails:send')->daily(); }) ``` 新構造では `EventServiceProvider` が廃止され、イベントとリスナーの手動登録が不要になりました。 **自動登録の仕組み:** リスナークラスの `handle()` メソッドの引数にイベントクラスを型宣言すると自動で登録されます。 ```php theme={null} namespace App\Listeners; use App\Events\OrderShipped; class SendShipmentNotification { // handle() の引数にイベントクラスを指定するだけで自動登録される public function handle(OrderShipped $event): void { // ... } } ``` 手動登録が必要な場合は `AppServiceProvider::boot()` で行います。 ```php theme={null} use Illuminate\Support\Facades\Event; public function boot(): void { Event::listen( OrderShipped::class, SendShipmentNotification::class, ); } ``` 新構造では、サービスプロバイダーの一覧管理が `config/app.php` から `bootstrap/providers.php` に変わりました。 ```php theme={null} // bootstrap/providers.php return [ App\Providers\AppServiceProvider::class, App\Providers\MyCustomServiceProvider::class, // ← ここに追加 ]; ``` `artisan make:provider` で作成したプロバイダーは自動的にここへ追記されます。 新構造では `app/Exceptions/Handler.php` が廃止され、例外処理の設定は `bootstrap/app.php` の `withExceptions()` で行います。 ```php theme={null} // bootstrap/app.php ->withExceptions(function (Exceptions $exceptions) { // 特定の例外をレポートしない $exceptions->dontReport(InvalidOrderException::class); // カスタムレンダリング $exceptions->render(function (InvalidOrderException $e, Request $request) { return response()->view('errors.invalid-order', status: 500); }); // 未認証時のリダイレクト先変更 $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) { return $request->is('api/*'); }); }) ``` ルートのカスタマイズは `bootstrap/app.php` の `withRouting()` で行います。 **ルートファイルを追加する:** ```php theme={null} ->withRouting( web: __DIR__.'/../routes/web.php', then: function () { Route::middleware('web') ->prefix('admin') ->group(base_path('routes/admin.php')); }, ) ``` **完全に制御する(Laravel 10 の `RouteServiceProvider` と同様):** `using` を指定すると Laravel のデフォルトのルート登録が完全に無効になり、すべてを自分で制御できます。 ```php theme={null} ->withRouting( using: function () { Route::middleware('web') ->group(base_path('routes/web.php')); Route::middleware('api') ->prefix('api') ->group(base_path('routes/api.php')); }, ) ``` `config/app.php` に `providers` キーを追加すると、フレームワーク内の `config/app.php` とマージされて反映されます。 ```php theme={null} // config/app.php use Illuminate\Support\ServiceProvider; return [ // ... 'providers' => ServiceProvider::defaultProviders()->replace([ Illuminate\Foo\FooServiceProvider::class => Bar\BarServiceProvider::class, ])->toArray(), ]; ``` `replace()` メソッドでデフォルトプロバイダーを独自実装に差し替えられます。 Laravel 11 以降、API 機能はデフォルトでは含まれておらず、必要な場合に個別インストールします。 ```bash theme={null} php artisan install:api ``` このコマンドで以下が作成・設定されます。 * `routes/api.php` * API 認証用の [Laravel Sanctum](https://laravel.com/docs/sanctum) の設定 * `bootstrap/app.php` への API ルート登録 Laravel 11 以降、ブロードキャスト機能もデフォルトでは含まれておらず、必要な場合に個別インストールします。 ```bash theme={null} php artisan install:broadcasting ``` このコマンドで以下が作成・設定されます。 * `routes/channels.php` * [Laravel Reverb](https://laravel.com/docs/reverb) のインストール設定 * ブロードキャスト設定ファイル `bootstrap/app.php` の内容を確認してください。 **Laravel 11 以降の新構造(または移行済み):** ```php theme={null} // bootstrap/app.php return Application::configure(basePath: dirname(__DIR__)) ->withRouting(...) ->withMiddleware(...) ->withExceptions(...) ->create(); ``` **Laravel 10 以前の旧構造のままアップグレードしたプロジェクト:** ```php theme={null} // bootstrap/app.php $app = new Illuminate\Foundation\Application( $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__) ); $app->singleton( Illuminate\Contracts\Http\Kernel::class, App\Http\Kernel::class ); // ... ``` `return Application::configure(...` の形式なら新構造、そうでなければ旧構造のままアップグレードしたプロジェクトです。旧構造のプロジェクトを新構造に移行する手順は[移行ガイド](/jp/advanced/app-structure-migration)を参照してください。 ## 関連ページ 新しいアプリケーション構造の全貌と `ApplicationBuilder` の内部実装を解説します。 Laravel 10 の旧アプリ構造から Laravel 11 以降の新構造へ移行する手順を解説します。 # 旧構造から新構造への移行ガイド Source: https://kawax.biz/jp/advanced/app-structure-migration Laravel 10の旧アプリ構造からLaravel 11以降のSlim Application Skeletonへ移行する手順を解説します。 ## はじめに このガイドでは、Laravel 10 以前の旧アプリケーション構造(`Kernel` クラスや複数のサービスプロバイダーを持つ構造)から、Laravel 11 以降の Slim Application Skeleton へ移行する手順を解説します。 **公式ドキュメントはこの移行を推奨していません。** * Laravel 10 の旧構造は Laravel 11 以降でも**そのまま動作**します。Laravel 13 を含む現在のバージョンに至るまで、サポート終了の予定はありません。 * 移行は**完全に任意**です。強制でも推奨でもありません。 * 旧構造と新構造の違いを深く理解していない場合は、移行を避けることを強くお勧めします。フレームワーク内部に精通した開発者向けの作業です。 * 移行前に**必ずバックアップを取り**、テストが全て通ることを確認してください。 ## 移行が必要になる場面 以下のような場合に移行を検討することがあります。 * 新規参加メンバーが公式ドキュメントと照合しやすいように、プロジェクトを最新の標準構造に合わせたい * Laravel 11 以降で新規作成したパッケージやスターターキットとの整合性を保ちたい * 旧構造由来の設定ファイルやクラスを減らし、コードベースをシンプルにしたい ## 前提条件 このガイドでは、以下の状態を前提とします。 * **Laravel バージョンアップが完了している**(`laravel/framework ^11.0` 以降) * 既存のテストがすべて通っている * Laravel 11 以降のアプリケーション構造([Laravel 11 以降のアプリケーション構造](/jp/advanced/app-structure)を参照)を理解している ## 移行例 Laravel 10 + Breeze(Blade スタック)で作成したプロジェクトを Breeze のまま維持しつつ、アプリケーション構造だけを新構造へ移行する手順を示します。 旧 `bootstrap/app.php` は `$app` インスタンスを生成してカーネルを登録する形式でした。これを `Application::configure()` チェーンに置き換えます。 **旧(Laravel 10):** ```php theme={null} singleton( Illuminate\Contracts\Http\Kernel::class, App\Http\Kernel::class ); $app->singleton( Illuminate\Contracts\Console\Kernel::class, App\Console\Kernel::class ); $app->singleton( Illuminate\Contracts\Debug\ExceptionHandler::class, App\Exceptions\Handler::class ); return $app; ``` **新(Laravel 11 以降):** ```php theme={null} withRouting( web: __DIR__.'/../routes/web.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware) { // }) ->withExceptions(function (Exceptions $exceptions) { // })->create(); ``` `withMiddleware()` と `withExceptions()` のコールバックは、次のステップで削除するカーネルファイルの設定を移植する場所です。まずは空のままにして、後続のステップで追記します。 `app/Http/Kernel.php` にはグローバルミドルウェア、ミドルウェアグループ、ミドルウェアエイリアスが定義されていました。 **旧(app/Http/Kernel.php):** ```php theme={null} [ \App\Http\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, ], 'api' => [ \Illuminate\Routing\Middleware\ThrottleRequests::class.':api', \Illuminate\Routing\Middleware\SubstituteBindings::class, ], ]; protected $middlewareAliases = [ 'auth' => \App\Http\Middleware\Authenticate::class, // ... ]; } ``` Laravel 11 では、上記のミドルウェアはフレームワーク内部にデフォルト値として組み込まれています。カスタマイズが**ない**場合は、`app/Http/Kernel.php` をそのまま削除できます。 **カスタマイズがある場合**(独自ミドルウェアを追加・除外している場合)は、`bootstrap/app.php` の `withMiddleware()` に移植してから削除してください。 ```php theme={null} ->withMiddleware(function (Middleware $middleware) { // グローバルミドルウェアの追加 $middleware->append(\App\Http\Middleware\MyCustomMiddleware::class); // web グループにミドルウェアを追加 $middleware->web(append: [ \App\Http\Middleware\HandleInertiaRequests::class, ]); // ミドルウェアエイリアスの追加 $middleware->alias([ 'role' => \App\Http\Middleware\CheckRole::class, ]); }) ``` 移植が完了したら `app/Http/Kernel.php` を削除します。 Laravel 11 では `TrustProxies`、`EncryptCookies`、`VerifyCsrfToken` などのデフォルトミドルウェアクラスも `app/Http/Middleware/` から削除できます。フレームワーク側に内蔵されているため、カスタマイズが不要ならファイル自体が不要になります。 `app/Console/Kernel.php` はスケジュールの定義とコマンドの自動ロードを担っていました。 **旧(app/Console/Kernel.php):** ```php theme={null} command('inspire')->hourly(); } protected function commands(): void { $this->load(__DIR__.'/Commands'); require base_path('routes/console.php'); } } ``` **コマンドの自動ロード**は Laravel 11 以降では `app/Console/Commands/` ディレクトリが自動的にスキャンされるため、`$this->load()` の記述は不要になりました。 **スケジュールの定義**は `routes/console.php` または `bootstrap/app.php` の `withSchedule()` に移行します。 ```php theme={null} // routes/console.php(Laravel 11 以降) use Illuminate\Support\Facades\Schedule; Schedule::command('emails:send')->daily(); ``` 移行後に `app/Console/Kernel.php` を削除します。 `app/Console/` ディレクトリ内のカスタム Artisan コマンドファイルは削除しないでください。コマンドファイルはそのまま残し、カーネルクラスのファイルだけを削除します。 `app/Exceptions/Handler.php` は例外レポートとレンダリングの設定を担っていました。 **旧(app/Exceptions/Handler.php):** ```php theme={null} reportable(function (Throwable $e) { // }); } } ``` カスタマイズがある場合は `bootstrap/app.php` の `withExceptions()` に移植してから削除します。 ```php theme={null} use Throwable; use Illuminate\Http\Request; ->withExceptions(function (Exceptions $exceptions) { // 例外レポートのカスタマイズ $exceptions->report(function (Throwable $e) { // ... }); // 例外レンダリングのカスタマイズ $exceptions->render(function (\App\Exceptions\InvalidOrderException $e, Request $request) { return response()->view('errors.invalid-order', status: 500); }); }) ``` `$dontFlash` に独自の項目を追加していた場合は同様に移植できます。 ```php theme={null} ->withExceptions(function (Exceptions $exceptions) { $exceptions->dontFlash([ 'my_sensitive_field', ]); }) ``` 移植が完了したら `app/Exceptions/Handler.php` を削除します。 `app/Providers/RouteServiceProvider.php` はルートファイルのロードとレート制限の設定を行っていました。 **旧(app/Providers/RouteServiceProvider.php):** ```php theme={null} by($request->user()?->id ?: $request->ip()); }); $this->routes(function () { Route::middleware('api') ->prefix('api') ->group(base_path('routes/api.php')); Route::middleware('web') ->group(base_path('routes/web.php')); }); } } ``` **ルートファイルの登録**を `bootstrap/app.php` の `withRouting()` に移行します。 ```php theme={null} ->withRouting( web: __DIR__.'/../routes/web.php', api: __DIR__.'/../routes/api.php', // API ルートを使う場合 commands: __DIR__.'/../routes/console.php', health: '/up', ) ``` **レート制限**は `AppServiceProvider::boot()` に移行します。 ```php theme={null} // app/Providers/AppServiceProvider.php use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Http\Request; use Illuminate\Support\Facades\RateLimiter; public function boot(): void { RateLimiter::for('api', function (Request $request) { return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); }); } ``` `HOME` 定数を使っている箇所がある場合は、直接 URL 文字列に置き換えるか、`AppServiceProvider` に定数を移動してください。 移行後に `app/Providers/RouteServiceProvider.php` を削除します。 Laravel 10 ではデフォルトで 5 つのサービスプロバイダーが用意されていました。これらを `AppServiceProvider.php` 1 つに統合します。 **削除するプロバイダー(内容を AppServiceProvider に移してから削除):** | ファイル | 移行先 | | - | - | | `app/Providers/AuthServiceProvider.php` | `AppServiceProvider::boot()` の `Gate::policy()` 等 | | `app/Providers/BroadcastServiceProvider.php` | Broadcasting を使う場合は `bootstrap/app.php` の `withBroadcasting()` | | `app/Providers/EventServiceProvider.php` | `AppServiceProvider::boot()` の `Event::listen()` 等 | | `app/Providers/RouteServiceProvider.php` | 前のステップで対応済み | **旧(app/Providers/AuthServiceProvider.php)の内容を移行する例:** ```php theme={null} // 旧: app/Providers/AuthServiceProvider.php use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; class AuthServiceProvider extends ServiceProvider { protected $policies = [ Post::class => PostPolicy::class, ]; public function boot(): void { $this->registerPolicies(); } } ``` ```php theme={null} // 新: app/Providers/AppServiceProvider.php // Laravelの標準的な命名規則に従っているなら自動検出されるので不要です use Illuminate\Support\Facades\Gate; class AppServiceProvider extends ServiceProvider { public function boot(): void { Gate::policy(Post::class, PostPolicy::class); } } ``` 不要になったプロバイダーファイルを削除したら、`config/app.php` の `providers` 配列を削除します。 ```php theme={null} // config/app.php の providers 配列 // 全て削除して構いません 'providers' => ServiceProvider::defaultProviders()->merge([ App\Providers\AppServiceProvider::class, // 削除したプロバイダーのエントリも削除 // App\Providers\AuthServiceProvider::class, // 削除 // App\Providers\EventServiceProvider::class, // 削除 // App\Providers\RouteServiceProvider::class, // 削除 ])->toArray(), ``` さらに、`bootstrap/providers.php` を作成して新構造に対応します。 ```php theme={null} `bootstrap/providers.php` が存在する場合、Laravel はこちらをプロバイダー一覧として優先して読み込みます。 Laravel 10 の `Controller` ベースクラスは `AuthorizesRequests` と `ValidatesRequests` トレイトを使用していました。Laravel 11 の新しいベースクラスはこれらのトレイトを持たないシンプルな抽象クラスです。 **旧(Laravel 10):** ```php theme={null} validate($request, $rules)` | `$request->validate($rules)` | | `$this->authorize('update', $post)` | `Gate::authorize('update', $post)` | | `$this->authorizeResource(Post::class)` | Laravel11では簡単な代替手段はないので`App\Http\Controllers\Controller`をLaravel10仕様のままにします | 既存のコントローラーがトレイトのメソッドを使っている場合は、各コントローラーを修正するか、トレイトを `Controller` ベースクラスに残すかを選択できます。一度にすべてを変更しなくても動作はします。 Breeze や Jetstream が生成した認証コントローラーが `$this->validate()` や `$this->authorize()` を呼び出している場合は、ベースクラスを変更する前に必ず動作確認してください。 `config/cors.php`、`config/hashing.php`、`config/view.php`などデフォルトから変更してないファイルは削除できます。変更しているファイルは残してください。 新構造に合わせて変更されているので全面的に書き換えます。 ```php theme={null} handleRequest(Request::capture()); ``` artisanファイルも同様に全て書き換えます。 ```php theme={null} #!/usr/bin/env php handleCommand(new ArgvInput); exit($status); ``` `CreatesApplication`トレイトが不要になっているので変更します。 `tests/CreatesApplication.php`は削除して構いません。 ```php theme={null} `CACHE_DRIVER`から`CACHE_STORE`に変わったり項目が増えているので必要なら変更します。 この辺りの変更はconfigファイルや本番環境も関わるので慎重に行ってください。 無理に追従する必要はありません。 移行が完了したら、以下の順序で動作確認を行います。 ```shell theme={null} # 設定キャッシュのクリア php artisan config:clear php artisan route:clear php artisan cache:clear # ルートが正しく登録されているか確認 php artisan route:list # テストを実行 php artisan test ``` 問題が発生した場合は、削除したファイルをバックアップから復元し、エラーメッセージを確認してください。 ## 移行後のファイル構成 移行後のプロジェクトは、以下のような構成になります(旧構造からの変更点を示す)。 ``` app/ ├── Console/ │ └── Commands/ ← カスタムコマンドはそのまま残す │ (Kernel.php は削除) ├── Exceptions/ │ (Handler.php は削除) ├── Http/ │ ├── Controllers/ │ │ └── Controller.php ← シンプルな抽象クラスに変更 │ └── Middleware/ ← カスタムミドルウェアはそのまま残す │ (Kernel.php は削除) ├── Models/ └── Providers/ └── AppServiceProvider.php ← 1 ファイルに統合 (AuthServiceProvider.php は削除) (BroadcastServiceProvider.php は削除) (EventServiceProvider.php は削除) (RouteServiceProvider.php は削除) bootstrap/ ├── app.php ← Application::configure() 形式に置き換え └── providers.php ← 新規作成 routes/ ├── web.php ├── api.php ← 必要な場合のみ └── console.php ← スケジュール定義もここに ``` ## まとめ | 移行内容 | 旧の場所 | 新の場所 | | - | - | - | | HTTP ミドルウェア | `app/Http/Kernel.php` | `bootstrap/app.php` の `withMiddleware()` | | 例外ハンドリング | `app/Exceptions/Handler.php` | `bootstrap/app.php` の `withExceptions()` | | スケジュール定義 | `app/Console/Kernel.php` | `routes/console.php` | | ルート登録 | `app/Providers/RouteServiceProvider.php` | `bootstrap/app.php` の `withRouting()` | | サービスプロバイダー一覧 | `config/app.php` の `providers` 配列 | `bootstrap/providers.php` | | ポリシー登録 | `app/Providers/AuthServiceProvider.php` | 自動登録。`AppServiceProvider::boot()`で手動登録 | | イベントリスナー | `app/Providers/EventServiceProvider.php` | 自動登録。`AppServiceProvider::boot()`で手動登録 | ## 参考リンク * [Laravel 11 以降のアプリケーション構造](/jp/advanced/app-structure) * [新アプリ構造 FAQ](/jp/advanced/app-structure-faq) * [公式アップグレードガイド(11.x)](https://github.com/laravel/docs/blob/11.x/upgrade.md) * [laravel/laravel リポジトリ 10.x → 11.x 差分](https://github.com/laravel/laravel/compare/10.x...11.x) * [Laravel 10 の Controller.php](https://github.com/laravel/laravel/blob/10.x/app/Http/Controllers/Controller.php) * [Laravel 11 の Controller.php](https://github.com/laravel/laravel/blob/11.x/app/Http/Controllers/Controller.php) # Boostのカスタムエージェントを作る Source: https://kawax.biz/jp/advanced/boost-custom-agent Laravel Boostの拡張アーキテクチャを深掘りし、SupportsGuidelines・SupportsMcp・SupportsSkillsコントラクトを実装したカスタムエージェントを作成する方法を解説します。 ## なぜカスタムエージェントが必要か Laravel Boostは Claude Code、Cursor、Codex、GitHub Copilot(VS Code) など主要なAIコーディングツールを標準でサポートしています。しかし社内ツール、新興のAIエージェント、独自ワークフローへ対応するには、Boostの拡張機能を使ってカスタムエージェントを実装する必要があります。 カスタムエージェントが役立つ場面: * Boostが未対応のIDEやAIツールを使っている * 社内開発ワークフローや独自CI/CDパイプラインにBoostを統合したい * エージェント固有の設定ファイル形式やインストール手順がある ## Boostの拡張アーキテクチャ ### Agent基底クラス すべてのエージェントは `Laravel\Boost\Install\Agents\Agent` を継承します。この抽象クラスが提供する機能: * エージェントの検出(システム全体・プロジェクト単位) * MCPサーバーのインストール(ファイルベース・シェルコマンドベース) * ガイドライン変換フック(`transformGuidelines`) 実装が必須の抽象メソッドは2つです。 | メソッド | 説明 | | - | - | | `name()` | エージェントの内部識別子(例: `claude_code`) | | `displayName()` | インストール時に表示される名前(例: `Claude Code`) | | `systemDetectionConfig(Platform $platform)` | システム全体へのインストール検出設定 | | `projectDetectionConfig()` | プロジェクト単位のインストール検出設定 | ### 3つのコントラクト Boostの機能を選択的に有効化するために、以下のコントラクトを必要な分だけ実装します。 | コントラクト | 役割 | | - | - | | `SupportsGuidelines` | AIガイドラインファイルの書き出しパスと変換処理 | | `SupportsMcp` | MCPサーバー設定のインストール | | `SupportsSkills` | Agent Skillsのインストールパス | 3つのコントラクトはすべて任意です。ガイドラインだけサポートしてMCPは不要、という実装も可能です。 ### SupportsGuidelinesコントラクト ```php theme={null} interface SupportsGuidelines { // AIガイドラインを書き出すファイルパス public function guidelinesPath(): string; // ガイドラインファイルにフロントマターが必要か public function frontmatter(): bool; // 生成されたガイドラインのMarkdownを後処理する public function transformGuidelines(string $markdown): string; } ``` `guidelinesPath()` は生成したガイドラインを書き込むファイルパスを返します。Claude Codeなら `CLAUDE.md`、Cursorなら `.cursor/rules/laravel-boost.mdc` のように、エージェントが読み込む形式に合わせます。 `transformGuidelines()` は生成後のMarkdownを加工するフックです。エージェント固有のヘッダーを追加したり、特殊なフォーマットに変換したりできます。`Agent` 基底クラスに実装済みのデフォルトはそのまま文字列を返します。 ### SupportsMcpコントラクト ```php theme={null} interface SupportsMcp { // MCPコマンドに絶対パスを使うか public function useAbsolutePathForMcp(): bool; // PHP実行ファイルのパスを返す public function getPhpPath(bool $forceAbsolutePath = false): string; // artisanのパスを返す public function getArtisanPath(bool $forceAbsolutePath = false): string; // MCPサーバー設定をインストールする public function installMcp(string $key, string $command, array $args = [], array $env = []): bool; // HTTP経由のMCPサーバー設定をインストールする public function installHttpMcp(string $key, string $url): bool; } ``` `Agent` 基底クラスが `installMcp()` と `installHttpMcp()` のデフォルト実装を持っているため、多くの場合は `mcpInstallationStrategy()` と `mcpConfigPath()` をオーバーライドするだけで済みます。 インストール戦略は2種類あります。 | `McpInstallationStrategy` | 動作 | | - | - | | `FILE` | JSON/TOMLファイルに設定を書き込む(デフォルト) | | `SHELL` | シェルコマンドを実行してMCPを登録する | | `NONE` | MCPインストールをスキップする | ### SupportsSkillsコントラクト ```php theme={null} interface SupportsSkills { // Agent Skillsを書き出すディレクトリパス public function skillsPath(): string; } ``` エージェントが読み込むスキルディレクトリのパスを返します。Claude Codeなら `.claude/skills`、Cursorなら `.cursor/skills` のようにエージェントの仕様に合わせます。 ## カスタムエージェントの作成 実際に独自エージェントクラスを実装します。ここでは独自のCI/CDワークフローに組み込まれた仮想エージェント「MyAgent」を例にします。 `app/Boost/MyAgent.php` を作成します。 ```php theme={null} [ 'command' => 'command -v myagent', ], Platform::Windows => [ 'command' => 'cmd /c where myagent 2>nul', ], }; } public function projectDetectionConfig(): array { return [ 'files' => ['.myagent.json'], ]; } // --- SupportsGuidelines --- public function guidelinesPath(): string { return 'MYAGENT.md'; } // frontmatter() は Agent 基底クラスのデフォルト (false) を使用 // transformGuidelines() も基底クラスのデフォルト(そのまま返す)を使用 // --- SupportsMcp --- // useAbsolutePathForMcp(), getPhpPath(), getArtisanPath(), // installMcp(), installHttpMcp() はすべて Agent 基底クラスが実装済み public function mcpInstallationStrategy(): McpInstallationStrategy { return McpInstallationStrategy::FILE; } public function mcpConfigPath(): string { return '.myagent.json'; } // --- SupportsSkills --- public function skillsPath(): string { return '.myagent/skills'; } } ``` `systemDetectionConfig()` と `projectDetectionConfig()` はBoostがエージェントをシステム・プロジェクトで自動検出するために使います。`boost:install` 実行時に対象エージェントを自動的に候補として表示します。 `App\Providers\AppServiceProvider` の `boot` メソッドにカスタムエージェントを登録します。 ```php theme={null} ```shell theme={null} php artisan boost:install ``` インストール対象のエージェントを選択する画面でMyAgentが表示されます。選択すると `MYAGENT.md`、`.myagent.json`、`.myagent/skills/` が生成されます。 ## ガイドラインのカスタマイズ ### guidelinesPathの設定 エージェントによってガイドラインファイルの読み込み場所は異なります。 ```php theme={null} // 単一ファイルとして出力する場合 public function guidelinesPath(): string { return 'MYAGENT.md'; } ``` 設定ファイルから上書きできるようにすると柔軟性が高まります。 ```php theme={null} public function guidelinesPath(): string { return config('boost.agents.my_agent.guidelines_path', 'MYAGENT.md'); } ``` ### フロントマターが必要なエージェント Cursorの `.cursor/rules/*.mdc` のようにフロントマターが必要なフォーマットには `frontmatter()` で `true` を返します。 ```php theme={null} public function frontmatter(): bool { return true; } ``` ### ガイドラインの後処理 `transformGuidelines()` を使って生成後のMarkdownを加工できます。 ```php theme={null} public function transformGuidelines(string $markdown): string { // エージェント独自のヘッダーを追加する $header = "\n\n"; return $header . $markdown; } ``` ## カスタムAIガイドラインの追加 プロジェクト固有のルールをBoostのガイドラインに追加するには、`.ai/guidelines/` ディレクトリに Blade ファイルを配置します。 ``` .ai/ └── guidelines/ ├── my-domain-rules.blade.php └── coding-standards.blade.php ``` ファイル例: ```php theme={null} ## プロジェクト固有のルール このプロジェクトでは以下の規約に従うこと: - モデルは必ず `App\Models\` 名前空間に配置する - すべてのAPIエンドポイントは `App\Http\Controllers\Api\` 以下に作成する - DBトランザクションは `DB::transaction()` を使いサービスクラス内に閉じ込める @verbatim DB::transaction(function () use ($data) { $order = Order::create($data); $order->items()->createMany($data['items']); event(new OrderCreated($order)); }); @endverbatim ``` `boost:install` を実行すると、このガイドラインがBoost組み込みのガイドラインと自動的に結合されて出力されます。 ### 組み込みガイドラインのオーバーライド Boostの組み込みガイドラインと同じパスにカスタムファイルを置くと、そちらが優先されます。 ``` # Boostの "Inertia React v2 Form Guidance" をオーバーライドする .ai/guidelines/inertia-react/2/forms.blade.php ``` ## カスタムスキルの追加 ### SKILL.mdの作成 `.ai/skills/{スキル名}/SKILL.md` にスキルを定義します。 ``` .ai/ └── skills/ └── creating-invoices/ └── SKILL.md ``` `SKILL.md` はYAMLフロントマターとMarkdownの指示で構成します。 ````markdown theme={null} --- name: creating-invoices description: 請求書(Invoice)の作成・更新・送信に関する処理を実装する際に使うスキル。 --- # 請求書作成スキル ## このスキルを使う場面 請求書モデルの作成、請求書のPDF生成、メール送信を実装するときに使います。 ## ファイル構造 - `app/Models/Invoice.php` — 請求書モデル - `app/Services/InvoiceService.php` — ビジネスロジック - `app/Jobs/SendInvoiceEmail.php` — メール送信ジョブ ## 実装パターン ### 請求書の作成 ```php // $items はコントローラーや他のサービスから渡された配列 $invoice = InvoiceService::create([ 'user_id' => $user->id, 'items' => $items, 'due_date' => now()->addDays(30), ]); ```` ### 請求書のPDF生成 PDFは `barryvdh/laravel-dompdf` を使って生成する: ```php theme={null} use Barryvdh\DomPDF\Facade\Pdf; use Illuminate\Support\Facades\Storage; $pdf = Pdf::loadView('invoices.pdf', ['invoice' => $invoice]); Storage::put("invoices/{$invoice->id}.pdf", $pdf->output()); ``` スキルは「必要なときだけ読み込む」設計です。常に必要な情報はガイドラインに、タスク固有の詳細なパターンはスキルに分けることで、AIのコンテキスト使用量を最適化できます。 ### 組み込みスキルのオーバーライド Boostの組み込みスキルと同じ名前でカスタムスキルを作ると上書きされます。 ``` # livewire-development スキルをカスタマイズする .ai/skills/livewire-development/SKILL.md ``` ## サードパーティパッケージへのBoost対応追加 自作パッケージにBoostサポートを追加するには、パッケージの `resources/boost/` ディレクトリに設定ファイルを配置します。 ### ガイドラインの追加 ``` resources/ └── boost/ └── guidelines/ └── core.blade.php ``` ```php theme={null} ## MyPackage このパッケージは [機能の概要] を提供します。 ### 基本的な使い方 @verbatim $result = MyPackage::create([ 'option' => 'value', ]); @endverbatim ``` ### スキルの追加 ``` resources/ └── boost/ └── skills/ └── mypackage-development/ └── SKILL.md ``` ```markdown theme={null} --- name: mypackage-development description: MyPackageの機能を実装するスキル。 --- # MyPackage Development ## このスキルを使う場面 MyPackageを使った機能実装時に使います。 ## 主な機能 - 機能1: ... - 機能2: ... ``` パッケージユーザーが `php artisan boost:install` を実行すると、これらのガイドラインとスキルが自動的に検出されてインストール対象として表示されます。 ## 実践例:独自CI/CDエージェント向けカスタムエージェント 社内のCI/CDパイプラインに組み込まれた仮想エージェント「PipelineAgent」の完全な実装例です。このエージェントはガイドラインのみサポートし、MCPとスキルはサポートしません。 ```php theme={null} match ($platform) { Platform::Windows => 'cmd /c echo %CI_AGENT_VERSION% 2>nul', default => 'echo $CI_AGENT_VERSION', }, ]; } public function projectDetectionConfig(): array { // プロジェクトルートの設定ファイルで検出する return [ 'files' => ['.pipeline-agent.yml'], ]; } public function guidelinesPath(): string { // CI/CDエージェントが読み込む規約のパス return '.pipeline/CODING_GUIDELINES.md'; } public function frontmatter(): bool { return false; } public function transformGuidelines(string $markdown): string { // パイプライン用のメタデータヘッダーを追加する $timestamp = now()->toIso8601String(); return "\n\n{$markdown}"; } } ``` `AppServiceProvider` に登録します: ```php theme={null} use App\Boost\PipelineAgent; use Laravel\Boost\Boost; public function boot(): void { Boost::registerAgent('pipeline_agent', PipelineAgent::class); } ``` CI環境では以下のコマンドでガイドラインのみを生成できます: ```shell theme={null} php artisan boost:install --agent=pipeline_agent ``` ## 参考リンク Boostに同梱されているClaudeCodeエージェントの完全な実装をソースコードで確認できます。 SKILL.mdのフォーマット仕様とベストプラクティスを参照できます。 Copilot CLI と Testbench 両対応の公開パッケージ実装例 PhpStorm の GitHub Copilot プラグイン向け公開パッケージ実装例 # Collection Deep Dive Source: https://kawax.biz/jp/advanced/collection-deep-dive Laravel Collectionの実装を、歴史・クラス構造・PHPDoc Generics・読み進め方の観点で解説します。 ## このページの目的 このページは、`collect()` の使い方ではなく、Laravel本体の実装コードを読むための地図を提供します。 対象は、`Collection` のメソッド一覧はすでに知っていて、次に「なぜこの設計なのか」を理解したい人です。 ## 歴史的変遷 Collectionの構造は、`LazyCollection` 導入時に大きく整理されました。 ### Laravel 5.8 まで * `Illuminate\Support\Collection` * `Illuminate\Database\Eloquent\Collection`(`Collection` を継承) この時点では `LazyCollection`・`Enumerable`・`EnumeratesValues` は存在しません。 * [5.8 の Collection.php](https://github.com/laravel/framework/blob/5.8/src/Illuminate/Support/Collection.php) * [5.8 の Eloquent Collection.php](https://github.com/laravel/framework/blob/5.8/src/Illuminate/Database/Eloquent/Collection.php) ### Laravel 6.0 での変更 `LazyCollection` の追加に合わせて、共通APIを `Enumerable`(インターフェース)と `EnumeratesValues`(トレイト)へ分離する構造になりました。 * [6.x の LazyCollection.php](https://github.com/laravel/framework/blob/6.x/src/Illuminate/Support/LazyCollection.php) * [6.x の Enumerable.php](https://github.com/laravel/framework/blob/6.x/src/Illuminate/Support/Enumerable.php) * [6.x の EnumeratesValues.php](https://github.com/laravel/framework/blob/6.x/src/Illuminate/Support/Traits/EnumeratesValues.php) ## 現在の全体構造(Laravel 13) > 参照: [laravel/framework v13.x](https://github.com/laravel/framework/tree/13.x/src/Illuminate) ```mermaid theme={null} classDiagram class Enumerable { <> } class EnumeratesValues { <> } class Collection class LazyCollection class EloquentCollection Collection ..|> Enumerable : implements LazyCollection ..|> Enumerable : implements Collection ..> EnumeratesValues : use LazyCollection ..> EnumeratesValues : use EloquentCollection --|> Collection : extends note for Enumerable "共通APIの
    コントラクト" note for EnumeratesValues "共通メソッドの
    実装本体" note for Collection "メモリ上で
    即時評価" note for LazyCollection "Generatorベースの
    遅延評価" note for EloquentCollection "Eloquentモデル向けの
    拡張" ``` * [Collection.php](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Collections/Collection.php) * [LazyCollection.php](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Collections/LazyCollection.php) * [EnumeratesValues.php](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Collections/Traits/EnumeratesValues.php) * [Enumerable.php](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Collections/Enumerable.php) * [Eloquent Collection.php](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Database/Eloquent/Collection.php) 現在の実ファイルパスは `src/Illuminate/Collections/*` ですが、名前空間は `Illuminate\\Support` のまま維持されています。コードを読むときは「パス」と「namespace」を分けて確認してください。 ## PHPDoc と PHPStan スタイルの Generics PHP本体にはGenericsがありません。 それでもCollection周辺では、PHPDocで強い型情報を表現しています。 主な目的は2つです。 * IDE補完を正確にする * PHPStan / Larastan などの静的解析精度を上げる ### よく出る記法 ```php theme={null} /** * @template TKey of array-key * @template-covariant TValue * @implements \Illuminate\Support\Enumerable */ class Collection implements Enumerable { /** * @use \Illuminate\Support\Traits\EnumeratesValues */ use EnumeratesValues; } ``` | 記法 | 意味 | | - | - | | `@template TKey of array-key` | キー型を `int\|string` に制約 | | `@template-covariant TValue` | 値型を共変として扱う(より具体的な型へ安全に置換しやすい) | | `@implements ...` | 実装しているインターフェースへ型引数を渡す | | `@extends ...` | 親クラス継承時の型引数を宣言 | | `@use ...` | トレイト適用時の型引数を宣言 | ### Eloquent Collectionでの見え方 ```php theme={null} /** * @template TKey of array-key * @template TModel of \Illuminate\Database\Eloquent\Model * @extends \Illuminate\Support\Collection */ class Collection extends BaseCollection { } ``` `TValue` が `TModel` に具体化されるので、`map()`・`filter()` などの型推論がEloquentモデル寄りに強化されます。 ## 実装を読むときの順番 ### 1. 入口として `Enumerable` を読む まずは「何を提供する契約か」を掴みます。 ここでメソッド一覧を把握すると、後続の実装読みが速くなります。 ### 2. `EnumeratesValues` で共通メソッドを追う `map`・`filter`・`reduce` など、共通ロジックの多くはここにあります。 `Collection` と `LazyCollection` の差分だけを後で見ると、読み飛ばしが減ります。 ### 3. `Collection` と `LazyCollection` の差分を見る * `Collection`: 配列を保持して即時評価 * `LazyCollection`: `Generator` を使って遅延評価 同名メソッドでも、評価タイミングやメモリ特性が違います。 ### 4. 最後に `Eloquent\Collection` を読む `find`・`load`・`modelKeys` など、モデル集合向けの拡張に集中します。 基底の `Collection` を理解してから読むと意図が見えやすくなります。 1メソッドを深掘りするときは、`Enumerable` の宣言 → `EnumeratesValues` の本体 → `Collection` / `LazyCollection` のオーバーライド有無、の順で追うと迷いません。 ## 関連ページ * [コレクション](/jp/collections) * [コレクションの高階メッセージ](/jp/advanced/higher-order-messages) * [Macroableトレイト](/jp/advanced/macroable) # Conditionableトレイト Source: https://kawax.biz/jp/advanced/conditionable when()/unless()メソッドの内部実装と活用パターンを解説します ## Conditionableトレイトとは `Illuminate\Support\Traits\Conditionable` トレイトは、オブジェクトに `when()` と `unless()` メソッドを追加します。条件に応じて処理を分岐しながらメソッドチェーンを続けられるのが特徴です。 実際のソースは `src/Illuminate/Conditionable/Traits/Conditionable.php` にあります。`Illuminate\Support\Traits\Conditionable` というエイリアスから参照されています。 QueryBuilder・EloquentBuilder・Mail・Notification など、Laravelの多くのクラスがこのトレイトを使っています。 ## 基本的な使い方 ### when() — 条件が真のとき実行する ```php theme={null} use Illuminate\Support\Collection; $results = collect([1, 2, 3, 4, 5]) ->when(true, function (Collection $collection) { return $collection->filter(fn ($n) => $n > 2); }); // [3, 4, 5] ``` 第1引数の値が真のとき、第2引数のコールバックが実行されます。偽のときは第3引数のコールバック(デフォルト)が実行されます。 ```php theme={null} $results = collect([1, 2, 3, 4, 5]) ->when(false, function (Collection $collection) { return $collection->filter(fn ($n) => $n > 2); }, function (Collection $collection) { return $collection->filter(fn ($n) => $n < 3); }); // [1, 2] ``` ### unless() — 条件が偽のとき実行する `unless()` は `when()` の逆です。条件が偽のときにコールバックが実行されます。 ```php theme={null} $results = collect([1, 2, 3, 4, 5]) ->unless(false, function (Collection $collection) { return $collection->take(3); }); // [1, 2, 3] ``` ## メソッドチェーンが続く理由 コールバックの戻り値が `null` の場合、`$this`(トレイトを使っているオブジェクト)が返ります。コールバックが `null` を返さない場合はその戻り値が返ります。 ```php theme={null} // $this が返るのでチェーンが続く $query = User::query() ->when($request->has('active'), function ($query) { $query->where('active', true); // void / null を返す }) ->when($request->filled('name'), function ($query) use ($request) { $query->where('name', 'like', "%{$request->name}%"); }) ->orderBy('created_at', 'desc'); ``` ソースコードでは次のように実装されています。 ```php theme={null} if ($value) { return $callback($this, $value) ?? $this; } elseif ($default) { return $default($this, $value) ?? $this; } return $this; ``` コールバックが明示的な値を返した場合はその値がチェーンの次に渡ります。何も返さない(`null`)場合は `$this` が返ります。 ## 引数なしで呼ぶ — HigherOrderWhenProxy 引数ゼロで `when()` を呼ぶと `HigherOrderWhenProxy` が返ります。これを使うと条件を後から設定できます。 ```php theme={null} $query = User::query() ->when()->isActive() // isActive() を条件として評価 ->where('role', 'admin'); ``` 引数1つで呼ぶと、その値を条件として持つプロキシが返ります。 ```php theme={null} // 引数1つ: 条件だけを渡してプロキシを得る $proxy = collect([1, 2, 3])->when($request->has('filter')); // $proxy->methodName() で条件が真のときだけ methodName() を呼び出せる ``` ## クロージャを値として渡す 第1引数にクロージャを渡すと、そのクロージャが実行された戻り値が条件として使われます。 ```php theme={null} $results = User::query() ->when( fn ($query) => $request->filled('role'), fn ($query) => $query->where('role', $request->role) ) ->get(); ``` これにより条件の評価ロジックをコールバックに切り出せます。 ## QueryBuilderでの典型パターン `when()` を使った動的クエリ構築は最も一般的なユースケースです。 ```php theme={null} public function index(Request $request) { $users = User::query() ->when($request->filled('search'), function ($query) use ($request) { $query->where('name', 'like', "%{$request->search}%") ->orWhere('email', 'like', "%{$request->search}%"); }) ->when($request->filled('role'), fn ($q) => $q->where('role', $request->role)) ->when($request->boolean('verified'), fn ($q) => $q->whereNotNull('email_verified_at')) ->when( $request->filled('sort'), fn ($q) => $q->orderBy($request->sort, $request->get('direction', 'asc')), fn ($q) => $q->latest() ) ->paginate(); return UserResource::collection($users); } ``` ## Conditionableを自前のクラスに適用する トレイトを `use` するだけで `when()` / `unless()` が使えるようになります。 ```php theme={null} namespace App\Services; use Illuminate\Support\Traits\Conditionable; class ReportBuilder { use Conditionable; protected array $filters = []; protected bool $includeArchived = false; protected ?string $groupBy = null; public function withArchived(): static { $this->includeArchived = true; return $this; } public function groupBy(string $column): static { $this->groupBy = $column; return $this; } public function addFilter(string $column, mixed $value): static { $this->filters[$column] = $value; return $this; } public function build(): \Illuminate\Database\Eloquent\Builder { return Report::query() ->when($this->includeArchived, fn ($q) => $q->withTrashed()) ->when($this->groupBy, fn ($q) => $q->groupBy($this->groupBy)) ->when($this->filters, function ($q) { foreach ($this->filters as $column => $value) { $q->where($column, $value); } }); } } ``` ```php theme={null} // 使用例 $query = (new ReportBuilder) ->when($request->boolean('archived'), fn ($b) => $b->withArchived()) ->when($request->filled('group'), fn ($b) => $b->groupBy($request->group)) ->addFilter('status', 'published') ->build(); ``` ## Mail・Notification・Responseでの活用 `when()` はメール・通知・レスポンス構築でも使えます。 ```php theme={null} use Illuminate\Mail\Mailable; use Illuminate\Support\Traits\Conditionable; class OrderConfirmation extends Mailable { public function build(): static { return $this ->subject('ご注文を承りました') ->view('emails.order.confirmation') ->when($this->order->hasDiscount(), function (Mailable $mail) { $mail->attach(storage_path('discounts/coupon.pdf')); }) ->when(app()->environment('production'), function (Mailable $mail) { $mail->bcc('archive@example.com'); }); } } ``` ## tap() との使い分け `tap()` と `when()` は似ていますが目的が異なります。 | | `tap()` | `when()` | | - | - | - | | 目的 | 副作用(ログ・デバッグ) | 条件分岐 | | 戻り値 | 常に `$this` | 条件付きで `$this` またはコールバックの戻り値 | | 条件 | なし | あり | ```php theme={null} // tap: 副作用のために使う。戻り値は常に $this $user = User::find($id) ->tap(fn ($user) => Log::info("User {$user->id} loaded")); // when: 条件分岐に使う $user = User::query() ->when($isAdmin, fn ($q) => $q->where('role', 'admin')) ->first(); ``` デバッグや副作用のためにチェーン中で何かしたいだけなら `tap()` を使います。条件によって処理を切り替えたいなら `when()` / `unless()` を使います。 ## 次のステップ `$collection->map->method()` のような構文の仕組みと実践的な使い方を学びます。 # コントローラーの PHPアトリビュート Source: https://kawax.biz/jp/advanced/controller-attributes Laravel 13 で追加された #[Middleware]、#[WithoutMiddleware]、#[Authorize] アトリビュートを使い、コントローラーへのミドルウェアや認可の設定を宣言的に記述する方法を解説します。 ## 概要 Laravel 13 では、コントローラーへのミドルウェア割り当てと認可チェックを PHP アトリビュートで宣言できるようになりました。従来の `middleware()` メソッドや `can` ミドルウェアの代わりに、クラスやメソッドに直接アトリビュートを付与するだけで設定が完結します。 ```php theme={null} use Illuminate\Routing\Attributes\Controllers\Authorize; use Illuminate\Routing\Attributes\Controllers\Middleware; #[Middleware('auth')] class PostController { #[Middleware('subscribed')] #[Authorize('create', Post::class)] public function store(Request $request): Response { // ... } } ``` コントローラーアトリビュートはすべて `Illuminate\Routing\Attributes\Controllers` 名前空間にあります。 ## `#[Middleware]` — ミドルウェアを割り当てる ### クラスへの適用 クラスレベルに `#[Middleware]` を付与すると、そのコントローラーの全アクションにミドルウェアが適用されます。 ```php theme={null} use Illuminate\Routing\Attributes\Controllers\Middleware; #[Middleware('auth')] class UserController { public function index(): View { /* ... */ } public function show(User $user): View { /* ... */ } public function store(Request $request): Response { /* ... */ } } ``` 複数のミドルウェアを付与する場合は、アトリビュートを繰り返します。 ```php theme={null} #[Middleware('auth')] #[Middleware('verified')] class ProfileController { // ... } ``` ### メソッドへの適用 メソッドレベルに付与したミドルウェアは、クラスレベルのミドルウェアとマージされます。 ```php theme={null} #[Middleware('auth')] class UserController { // 全員に auth のみ public function index(): View { /* ... */ } // auth + subscribed の両方が適用される #[Middleware('subscribed')] public function create(): View { /* ... */ } } ``` ### `only` / `except` による絞り込み クラスレベルのアトリビュートに `only` または `except` を指定すると、対象メソッドを絞り込めます。 ```php theme={null} #[Middleware('auth')] #[Middleware('subscribed', only: ['create', 'store', 'edit', 'update'])] class ArticleController { // auth のみ public function index(): View { /* ... */ } public function show(Article $article): View { /* ... */ } // auth + subscribed public function create(): View { /* ... */ } public function store(Request $request): Response { /* ... */ } public function edit(Article $article): View { /* ... */ } public function update(Request $request, Article $article): Response { /* ... */ } // auth のみ(except で除外された場合と同等) public function destroy(Article $article): Response { /* ... */ } } ``` ### クロージャーミドルウェア アトリビュートにはクロージャーも使えます。インラインで処理を書きたい場合に便利です。 ```php theme={null} use Closure; use Illuminate\Http\Request; use Illuminate\Routing\Attributes\Controllers\Middleware; class ReportController { #[Middleware(static function (Request $request, Closure $next) { if (! $request->user()->hasRole('analyst')) { abort(403); } return $next($request); })] public function generate(): Response { // ... } } ``` ### 従来の `middleware()` メソッドとの比較 ```php theme={null} // 従来の書き方(HasMiddleware インターフェイスを実装) use Illuminate\Routing\Controllers\HasMiddleware; use Illuminate\Routing\Controllers\Middleware; class UserController implements HasMiddleware { public static function middleware(): array { return [ 'auth', new Middleware('log', only: ['index']), new Middleware('subscribed', except: ['store']), ]; } } // アトリビュートを使った書き方 #[Middleware('auth')] #[Middleware('log', only: ['index'])] #[Middleware('subscribed', except: ['store'])] class UserController { // HasMiddleware の実装不要 } ``` アトリビュートを使う場合は `HasMiddleware` インターフェイスを実装する必要はありません。ただし、`middleware()` メソッドとアトリビュートを混在させると意図しない動作になる可能性があるため、どちらか一方に統一することを推奨します。 ## `#[WithoutMiddleware]` — ミドルウェアを除外する クラスレベルで適用されたミドルウェアを、特定のメソッドまたはクラス全体から除外します。 ### メソッドへの適用 ```php theme={null} use App\Http\Middleware\EnsureTokenIsValid; use Illuminate\Routing\Attributes\Controllers\Middleware; use Illuminate\Routing\Attributes\Controllers\WithoutMiddleware; #[Middleware('auth')] #[Middleware(EnsureTokenIsValid::class)] class ApiController { // auth と EnsureTokenIsValid が適用される public function show(Resource $resource): JsonResponse { /* ... */ } // auth は適用されるが EnsureTokenIsValid は除外される #[WithoutMiddleware(EnsureTokenIsValid::class)] public function index(): JsonResponse { /* ... */ } } ``` ### クラスへの適用と `only` / `except` `#[WithoutMiddleware]` をクラスレベルに付与すると、子クラスを含む全アクションからミドルウェアが除外されます。`only` / `except` で除外範囲を限定できます。 ```php theme={null} #[Middleware('auth')] #[WithoutMiddleware('subscribed', except: ['index'])] class AdminController { // subscribed が適用される(except: ['index'] で除外されない) public function index(): View { /* ... */ } // subscribed は除外される public function dashboard(): View { /* ... */ } public function settings(): View { /* ... */ } } ``` `#[WithoutMiddleware]` は**ルートミドルウェア**にのみ有効です。`app/Http/Kernel.php` に登録されたグローバルミドルウェアは除外できません。 ## `#[Authorize]` — ポリシーによる認可 `can` ミドルウェアの糖衣構文として、ポリシーによる認可チェックをアトリビュートで宣言できます。 ### 基本的な使い方 ```php theme={null} use App\Models\Post; use Illuminate\Routing\Attributes\Controllers\Authorize; class PostController { // 'viewAny' アビリティをチェック(Post ポリシーの viewAny メソッド) #[Authorize('viewAny', Post::class)] public function index(): View { /* ... */ } // 'view' アビリティをチェック(ルートパラメーター 'post' を渡す) #[Authorize('view', 'post')] public function show(Post $post): View { /* ... */ } // 'create' アビリティをチェック #[Authorize('create', Post::class)] public function create(): View { /* ... */ } #[Authorize('create', Post::class)] public function store(Request $request): Response { /* ... */ } // 'update' アビリティをチェック(ルートパラメーターを渡す) #[Authorize('update', 'post')] public function edit(Post $post): View { /* ... */ } #[Authorize('update', 'post')] public function update(Request $request, Post $post): Response { /* ... */ } #[Authorize('delete', 'post')] public function destroy(Post $post): Response { /* ... */ } } ``` ### ポリシーの引数 第2引数には以下を渡せます。 | 渡す値 | 説明 | | - | - | | `Post::class` | モデルクラス(`viewAny` などモデルインスタンス不要の場合) | | `'post'` | ルートパラメーター名(モデルバインディングされたインスタンスが渡される) | | `[Comment::class, 'post']` | 複数の引数(ポリシーメソッドに配列で渡される) | ```php theme={null} use App\Models\Comment; use App\Models\Post; use Illuminate\Routing\Attributes\Controllers\Authorize; class CommentController { // $post(ルートパラメーター)と Comment クラスをポリシーに渡す #[Authorize('create', [Comment::class, 'post'])] public function store(Post $post, Request $request): Response { // ... } #[Authorize('delete', 'comment')] public function destroy(Comment $comment): Response { // ... } } ``` ### `can` ミドルウェアとの比較 ```php theme={null} // 従来の書き方(Route ファサード) Route::get('/posts', [PostController::class, 'index'])->middleware('can:viewAny,App\Models\Post'); Route::put('/posts/{post}', [PostController::class, 'update'])->middleware('can:update,post'); // アトリビュートを使った書き方 class PostController { #[Authorize('viewAny', Post::class)] public function index(): View { /* ... */ } #[Authorize('update', 'post')] public function update(Request $request, Post $post): Response { /* ... */ } } ``` ルートファイルにミドルウェアを書く必要がなくなり、認可ロジックがコントローラーに集約されます。 ## 実践的な例 ### リソースコントローラーへの適用 ```php theme={null} use App\Models\Article; use Illuminate\Routing\Attributes\Controllers\Authorize; use Illuminate\Routing\Attributes\Controllers\Middleware; #[Middleware('auth')] class ArticleController { #[Authorize('viewAny', Article::class)] public function index(): View { return view('articles.index', [ 'articles' => Article::paginate(), ]); } #[Authorize('view', 'article')] public function show(Article $article): View { return view('articles.show', compact('article')); } #[Authorize('create', Article::class)] public function create(): View { return view('articles.create'); } #[Authorize('create', Article::class)] public function store(StoreArticleRequest $request): RedirectResponse { $article = Article::create($request->validated()); return redirect()->route('articles.show', $article); } #[Authorize('update', 'article')] public function edit(Article $article): View { return view('articles.edit', compact('article')); } #[Authorize('update', 'article')] public function update(UpdateArticleRequest $request, Article $article): RedirectResponse { $article->update($request->validated()); return redirect()->route('articles.show', $article); } #[Authorize('delete', 'article')] public function destroy(Article $article): RedirectResponse { $article->delete(); return redirect()->route('articles.index'); } } ``` ### API コントローラーへの適用 ```php theme={null} use Illuminate\Routing\Attributes\Controllers\Middleware; use Illuminate\Routing\Attributes\Controllers\WithoutMiddleware; #[Middleware('auth:sanctum')] #[Middleware('throttle:api')] class ApiPostController { // 一覧取得は認証なしで可能 #[WithoutMiddleware('auth:sanctum')] public function index(): JsonResponse { return response()->json(Post::paginate()); } public function store(Request $request): JsonResponse { // 認証済みユーザーのみ } } ``` ## アトリビュートの処理順序 複数のアトリビュートが付与されている場合、処理順序は以下のとおりです。 ```mermaid theme={null} flowchart LR A["リクエスト"] --> B["クラスレベルの
    #[Middleware]"] B --> C["メソッドレベルの
    #[Middleware]"] C --> D["#[WithoutMiddleware]
    の除外処理"] D --> E["#[Authorize]
    の認可チェック"] E --> F["コントローラー
    メソッド実行"] ``` `#[Authorize]` は内部的に `can` ミドルウェアとして動作するため、`#[Middleware]` と同じパイプラインで処理されます。 ## まとめ:どちらを使うべきか | 状況 | 推奨アプローチ | | - | - | | 新規コントローラー(Laravel 13) | アトリビュートを使う | | 既存の `middleware()` メソッドを持つコントローラー | 段階的にアトリビュートへ移行 | | 動的なミドルウェア設定が必要 | `middleware()` メソッドを継続使用 | | ルートファイルで一元管理したい | `Route::middleware()` を継続使用 | ## 次のステップ キュージョブや Eloquent モデルで使える PHP アトリビュートの解説。 Laravel コントローラーの基本的な使い方を学びます。 # カスタム認証ガードの実装 Source: https://kawax.biz/jp/advanced/custom-auth-guard Laravelの認証システムの内部構造を理解し、Guard・StatefulGuardインターフェースを実装してカスタム認証ガードを作る方法をフレームワークのソースコードレベルで解説します。 ## Laravelの認証システムの内部構造 ### `Auth` ファサードと `AuthManager` `Auth` ファサードは `Illuminate\Auth\AuthManager` のプロキシです。`AuthManager` はドライバーパターンで複数のガードを管理し、`config/auth.php` の設定に基づいて適切なガードインスタンスを生成・キャッシュします。 ```php theme={null} // Auth::guard('web') の内部動作(AuthManager::guard() の簡略版) public function guard($name = null) { $name = $name ?: $this->getDefaultDriver(); return $this->guards[$name] ?? ($this->guards[$name] = $this->resolve($name)); } ``` `resolve()` は `config/auth.php` の `guards` 配列から `driver` キーを読み取り、対応するファクトリークロージャを呼び出します。組み込みの `session` や `token` ドライバーも同じ仕組みで登録されています。 ### `Guard` インターフェースと `StatefulGuard` インターフェースの違い Laravelの認証ガードは `Illuminate\Contracts\Auth\Guard` を最低限実装する必要があります。セッションを維持する必要がある場合は `StatefulGuard` を実装します。 ```php theme={null} interface Guard { // 認証済みユーザーが存在するか確認する public function check(); // ゲスト(未認証)かどうか確認する public function guest(); // 現在の認証済みユーザーを返す(未認証なら null) public function user(); // 現在の認証済みユーザーのIDを返す public function id(); // 指定したクレデンシャルが有効かどうか確認する(ログインは行わない) public function validate(array $credentials = []); // ユーザーがセットされているか確認する(未ログインでも setUser で注入できる) public function hasUser(); // 認証済みユーザーを手動でセットする public function setUser(Authenticatable $user); } ``` `StatefulGuard` は `Guard` を継承し、セッションやクッキーを使ったログイン状態の維持に必要なメソッドを追加します。 ```php theme={null} interface StatefulGuard extends Guard { // クレデンシャルを検証してログインする(remember フラグあり) public function attempt(array $credentials = [], $remember = false); // セッションを保存せずに1リクエストだけ認証する public function once(array $credentials = []); // ユーザーインスタンスを直接ログインさせる public function login(Authenticatable $user, $remember = false); // IDを指定してログインする public function loginUsingId($id, $remember = false); // IDを指定して1リクエストだけ認証する public function onceUsingId($id); // 「ログイン状態を保持」クッキーでログインしたか確認する public function viaRemember(); // ログアウトする public function logout(); } ``` API認証や独自トークン認証など、セッションが不要なガードは `Guard` だけを実装すればよいです。管理者ログインのようにセッションが必要な場合は `StatefulGuard` を実装します。 ## カスタムガードの実装 ### `GuardHelpers` トレイト `Guard` インターフェースの `check()`、`guest()`、`id()`、`hasUser()` はほぼ共通の実装になるため、Laravelは `Illuminate\Auth\GuardHelpers` トレイトを提供しています。このトレイトを使うと、必須実装を `user()` と `validate()` の2メソッドに絞れます。 ```php theme={null} // GuardHelpers が提供するデフォルト実装(抜粋) trait GuardHelpers { protected $user; public function check(): bool { return ! is_null($this->user()); } public function guest(): bool { return ! $this->check(); } public function id(): mixed { return $this->user()?->getAuthIdentifier(); } public function hasUser(): bool { return ! is_null($this->user); } public function setUser(Authenticatable $user): static { $this->user = $user; return $this; } } ``` ### APIトークン認証ガードの実装例 `TokenGuard` の設計に倣い、シンプルなAPIトークン認証ガードを実装します。リクエストヘッダーまたはクエリパラメータからトークンを取得し、`UserProvider` を通してユーザーを解決します。 `app/Auth` ディレクトリにガードクラスを作成します。 ```php theme={null} provider = $provider; $this->request = $request; } /** * 現在の認証済みユーザーを返す */ public function user(): ?\Illuminate\Contracts\Auth\Authenticatable { // キャッシュ済みのユーザーがあれば返す if (! is_null($this->user)) { return $this->user; } $token = $this->getTokenForRequest(); if (empty($token)) { return null; } // UserProvider 経由でユーザーを取得する $this->user = $this->provider->retrieveByCredentials([ 'api_token' => $token, ]); return $this->user; } /** * クレデンシャルの検証のみ行う(ログインはしない) */ public function validate(array $credentials = []): bool { if (empty($credentials['api_token'])) { return false; } return (bool) $this->provider->retrieveByCredentials($credentials); } /** * リクエストからトークンを取得する * * 優先順位: Bearer ヘッダー → クエリパラメータ → リクエストボディ */ protected function getTokenForRequest(): ?string { $token = $this->request->bearerToken(); if (empty($token)) { $token = $this->request->query('api_token'); } if (empty($token)) { $token = $this->request->input('api_token'); } return $token ?: null; } /** * リクエストインスタンスを差し替える */ public function setRequest(Request $request): static { $this->request = $request; return $this; } } ``` `AppServiceProvider` の `boot()` メソッドで `Auth::extend()` を使ってガードを登録します。 ```php theme={null} make('request')); }); } } ``` `Auth::createUserProvider()` は `config/auth.php` の `providers` 設定を読み取り、対応する `UserProvider` インスタンスを返します。独自プロバイダーを作らない限り、この呼び出し方で標準の `EloquentUserProvider` を使えます。 `config/auth.php` に新しいガードを追加します。 ```php theme={null} 'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'users', ], // 追加したカスタムガード 'api' => [ 'driver' => 'api-token', // Auth::extend() の第1引数と一致させる 'provider' => 'users', ], ], ``` `auth` ミドルウェアにガード名を指定します。 ```php theme={null} // routes/api.php use Illuminate\Support\Facades\Route; Route::middleware('auth:api')->group(function () { Route::get('/user', function () { return auth()->user(); }); Route::get('/posts', [\App\Http\Controllers\PostController::class, 'index']); }); ``` コントローラーやコード内で特定のガードを使うには `Auth::guard('api')` または `auth('api')` を呼び出します。 ```php theme={null} $user = Auth::guard('api')->user(); ``` ## クロージャによる簡易ガード `Auth::viaRequest()` を使うと、クラスを作らずクロージャだけでシンプルなガードを定義できます。プロトタイプや非常にシンプルな認証に向いています。 ```php theme={null} use Illuminate\Http\Request; use App\Models\User; // AppServiceProvider::boot() 内 Auth::viaRequest('custom-token', function (Request $request): ?User { $token = $request->bearerToken(); if (empty($token)) { return null; } return User::where('api_token', $token)->first(); }); ``` `config/auth.php` での設定: ```php theme={null} 'guards' => [ 'api' => [ 'driver' => 'custom-token', ], ], ``` `Auth::viaRequest()` で定義したガードは `UserProvider` を使わないため、`retrieveById()` などのプロバイダーメソッドが機能しません。本番環境では `Auth::extend()` を使ったクラスベースのガードを推奨します。 ## カスタム `UserProvider` の実装 ユーザー情報をデータベース以外のソース(外部API、LDAPなど)から取得する場合は、`Illuminate\Contracts\Auth\UserProvider` インターフェースを実装します。 ```php theme={null} model)::find($identifier); } /** * 「ログイン状態を保持」トークンでユーザーを取得する * セッションを使わないガードでは null を返すだけでよい */ public function retrieveByToken(mixed $identifier, string $token): ?Authenticatable { return null; } /** * 「ログイン状態を保持」トークンを更新する * セッションを使わないガードでは何もしなくてよい */ public function updateRememberToken(Authenticatable $user, string $token): void {} /** * クレデンシャルでユーザーを取得する * ガードの validate() と user() から呼ばれる */ public function retrieveByCredentials(array $credentials): ?Authenticatable { if (empty($credentials['api_token'])) { return null; } // 外部APIでトークンを検証してユーザー情報を取得する例 $response = \Illuminate\Support\Facades\Http::withToken($credentials['api_token']) ->get("{$this->apiBaseUrl}/auth/me"); if (! $response->successful()) { return null; } $data = $response->json(); // ローカルDBのユーザーと紐付けるか、動的にモデルを生成する return User::firstOrCreate( ['external_id' => $data['id']], ['name' => $data['name'], 'email' => $data['email']], ); } /** * 取得したユーザーのクレデンシャルを検証する * トークン認証では retrieveByCredentials が成功すれば true でよい */ public function validateCredentials(Authenticatable $user, array $credentials): bool { return true; } /** * パスワード再ハッシュが必要かどうか確認する * トークン認証では常に false でよい */ public function rehashPasswordIfRequired(Authenticatable $user, array $credentials, bool $force = false): void {} } ``` ### カスタム `UserProvider` の登録 ```php theme={null} // AppServiceProvider::boot() Auth::provider('api-user', function (Application $app, array $config) { return new \App\Auth\ApiUserProvider( config('services.auth_api.base_url'), $config['model'] ?? \App\Models\User::class, ); }); ``` `config/auth.php` の `providers` セクションに追加します: ```php theme={null} 'providers' => [ 'users' => [ 'driver' => 'eloquent', 'model' => App\Models\User::class, ], // カスタムプロバイダー 'api-users' => [ 'driver' => 'api-user', 'model' => App\Models\User::class, ], ], ``` ガードとプロバイダーを組み合わせます: ```php theme={null} 'guards' => [ 'api' => [ 'driver' => 'api-token', 'provider' => 'api-users', // カスタムプロバイダーを指定 ], ], ``` ## 実践的なユースケース ### マルチ認証(管理者と一般ユーザーで別ガード) 管理者用のEloquentモデルを用意します。`Authenticatable` を継承することで `Auth` システムと連携できます。 ```bash theme={null} php artisan make:model Admin -m ``` ```php theme={null} ```php theme={null} 'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'users', ], 'admin' => [ 'driver' => 'session', 'provider' => 'admins', // 管理者用プロバイダー ], ], 'providers' => [ 'users' => [ 'driver' => 'eloquent', 'model' => App\Models\User::class, ], 'admins' => [ 'driver' => 'eloquent', 'model' => App\Models\Admin::class, // 管理者モデル ], ], ``` ```php theme={null} // routes/web.php // 一般ユーザー用ルート(デフォルトの web ガード) Route::middleware('auth')->group(function () { Route::get('/dashboard', [DashboardController::class, 'index']); }); // 管理者用ルート(admin ガード) Route::prefix('admin')->middleware('auth:admin')->group(function () { Route::get('/dashboard', [AdminDashboardController::class, 'index']); }); ``` ```php theme={null} validate([ 'email' => 'required|email', 'password' => 'required', ]); // admin ガードを明示して attempt する if (Auth::guard('admin')->attempt($credentials)) { $request->session()->regenerate(); return redirect()->intended('/admin/dashboard'); } return back()->withErrors(['email' => 'ログイン情報が正しくありません。']); } public function destroy(Request $request) { Auth::guard('admin')->logout(); $request->session()->invalidate(); $request->session()->regenerateToken(); return redirect('/admin/login'); } } ``` ### JWTトークンによる外部API認証 外部のJWT認証サービスを使う場合のカスタムガード実装例です。 ```php theme={null} provider = $provider; $this->request = $request; } public function user(): ?\Illuminate\Contracts\Auth\Authenticatable { if (! is_null($this->user)) { return $this->user; } $token = $this->request->bearerToken(); if (empty($token)) { return null; } // 外部サービスでJWTを検証する $payload = $this->verifyToken($token); if (is_null($payload)) { return null; } $this->payload = $payload; $this->user = $this->provider->retrieveById($payload['sub']); return $this->user; } public function validate(array $credentials = []): bool { if (empty($credentials['token'])) { return false; } return ! is_null($this->verifyToken($credentials['token'])); } /** * JWTの検証と payload の取得 */ protected function verifyToken(string $token): ?array { $response = Http::withToken($token) ->get(config('services.auth.verify_url')); if (! $response->successful()) { return null; } return $response->json(); } /** * 検証済みのJWT payload を返す */ public function payload(): ?array { return $this->payload; } } ``` `AppServiceProvider` での登録: ```php theme={null} Auth::extend('jwt', function (Application $app, string $name, array $config) { return new \App\Auth\JwtGuard( Auth::createUserProvider($config['provider'] ?? 'users'), $app->make('request'), ); }); ``` `Auth::guard('jwt')->payload()` のように、カスタムガード固有のメソッドにもアクセスできます。`Auth::guard()` が返すのはガードインスタンスそのものなので、インターフェースにないメソッドも呼び出せます。 ## テスト カスタムガードのユニットテストでは、`UserProvider` をモックしてガードの動作を確認します。 ```php theme={null} provider = $this->createMock(UserProvider::class); } public function test_user_returns_null_when_no_token(): void { $request = Request::create('/'); $guard = new ApiTokenGuard($this->provider, $request); $this->assertNull($guard->user()); } public function test_user_returns_user_with_valid_bearer_token(): void { $mockUser = $this->createMock(Authenticatable::class); $this->provider ->expects($this->once()) ->method('retrieveByCredentials') ->with(['api_token' => 'valid-token']) ->willReturn($mockUser); $request = Request::create('/'); $request->headers->set('Authorization', 'Bearer valid-token'); $guard = new ApiTokenGuard($this->provider, $request); $this->assertSame($mockUser, $guard->user()); } public function test_check_returns_false_when_no_token(): void { $request = Request::create('/'); $guard = new ApiTokenGuard($this->provider, $request); $this->assertFalse($guard->check()); } public function test_validate_returns_false_without_api_token_credential(): void { $request = Request::create('/'); $guard = new ApiTokenGuard($this->provider, $request); $this->assertFalse($guard->validate([])); } } ``` `ActingAs` を使った機能テストでは、特定のガードにユーザーをセットできます。 ```php theme={null} // 特定のガードでユーザーを認証してテストする $this->actingAs($user, 'api') ->getJson('/api/user') ->assertOk(); ``` ## 関連ページ スターターキットや標準的な認証フローを確認します。 ガード登録で使うサービスコンテナの仕組みを理解します。 # カスタムバリデーションルール Source: https://kawax.biz/jp/advanced/custom-validation-rules Laravelのバリデーション機能をルールオブジェクトやクロージャで拡張する方法を、フレームワークの内部実装まで踏み込んで解説します。 ## カスタムバリデーションルールとは Laravelには豊富な組み込みバリデーションルールが用意されていますが、アプリケーション固有の検証ロジックが必要な場面もあります。カスタムバリデーションルールを使うと、再利用可能な検証ロジックをクラスやクロージャとして定義し、標準のルールと同じように使えます。 カスタムルールを定義する方法は主に2つあります。 * **ルールオブジェクト** — 再利用性が高く、テストしやすい * **クロージャ** — 一度だけ使うシンプルなルールに向いている ## ルールオブジェクト ### ルールクラスを生成する `make:rule` Artisanコマンドで新しいルールクラスを生成します。生成されたクラスは `app/Rules` ディレクトリに配置されます。 ```bash theme={null} php artisan make:rule Uppercase ``` ### ValidationRuleインターフェースを実装する 生成されたクラスに `validate` メソッドを実装します。このメソッドはバリデーション失敗時に `$fail` クロージャを呼び出します。 ```php theme={null} validate([ 'name' => ['required', 'string', new Uppercase], ]); ``` フォームリクエストの `rules()` メソッドでも同様に使えます。 ```php theme={null} public function rules(): array { return [ 'name' => ['required', 'string', new Uppercase], ]; } ``` ### 翻訳キーを使ったエラーメッセージ エラーメッセージをハードコードする代わりに、翻訳キーを使うこともできます。 ```php theme={null} public function validate(string $attribute, mixed $value, Closure $fail): void { if (strtoupper($value) !== $value) { $fail('validation.uppercase')->translate(); } } ``` 翻訳ファイル `lang/ja/validation.php` にメッセージを追加します。 ```php theme={null} return [ 'uppercase' => ':attributeは大文字で入力してください。', // ... ]; ``` ### 複数のエラーメッセージを追加する 一つのフィールドに複数のエラーを報告するには、`$fail` を複数回呼び出します。 ```php theme={null} public function validate(string $attribute, mixed $value, Closure $fail): void { if (! is_string($value)) { $fail('The :attribute must be a string.'); return; } if (strlen($value) < 8) { $fail('The :attribute must be at least 8 characters.'); } if (! preg_match('/[A-Z]/', $value)) { $fail('The :attribute must contain at least one uppercase letter.'); } } ``` ## クロージャベースのルール アプリケーション内で一度だけ使うシンプルなルールは、クラスを作らずクロージャで定義できます。 ```php theme={null} use Illuminate\Support\Facades\Validator; use Closure; $validator = Validator::make($request->all(), [ 'title' => [ 'required', 'max:255', function (string $attribute, mixed $value, Closure $fail) { if ($value === 'foo') { $fail("The {$attribute} is invalid."); } }, ], ]); ``` クロージャルールはインラインで定義するため手軽ですが、再利用や独立したテストが難しいため、複数箇所で使うロジックはルールオブジェクトに切り出すことをおすすめします。 ## Implicitルール(値が空でも実行する) デフォルトでは、フィールドが空または存在しない場合、カスタムルールは実行されません。空の値に対してもルールを実行したい場合は、`--implicit` オプションを付けてクラスを生成します。 ```bash theme={null} php artisan make:rule Uppercase --implicit ``` 生成されたクラスは `ImplicitRule` インターフェースを実装しています。このインターフェース自体は追加のメソッドを持たず、Laravelへのシグナルとして機能します。 ```php theme={null} `ImplicitRule` は「属性が必須である」ことをLaravelに示すだけです。値が空のときに実際にバリデーションを失敗させるかどうかは、`validate` メソッドの実装次第です。 ## データアクセス ### DataAwareRule — フォーム全体のデータにアクセスする 他のフィールドの値に基づいてバリデーションしたい場合は、`DataAwareRule` インターフェースを実装します。`setData` メソッドがバリデーション開始前に自動で呼び出されます。 ```php theme={null} */ protected array $data = []; public function __construct( protected string $table, protected string $column = 'value', ) {} /** * バリデーションデータをセットする * * @param array $data */ public function setData(array $data): static { $this->data = $data; return $this; } public function validate(string $attribute, mixed $value, Closure $fail): void { $tenantId = $this->data['tenant_id'] ?? null; $exists = DB::table($this->table) ->where('tenant_id', $tenantId) ->where($this->column, $value) ->exists(); if ($exists) { $fail('The :attribute has already been taken for this tenant.'); } } } ``` 使用例: ```php theme={null} $request->validate([ 'tenant_id' => 'required|integer', 'email' => ['required', 'email', new UniqueForTenant('users', 'email')], ]); ``` ### ValidatorAwareRule — バリデーターインスタンスにアクセスする バリデーターが持つすべての情報(失敗したルール、カスタムメッセージなど)にアクセスするには、`ValidatorAwareRule` インターフェースを実装します。 ```php theme={null} validator = $validator; return $this; } public function validate(string $attribute, mixed $value, Closure $fail): void { // 他のフィールドがすでにバリデーション失敗している場合はスキップ if ($this->validator->errors()->has('type')) { return; } $type = $this->validator->getData()['type'] ?? null; if ($type === 'phone' && ! preg_match('/^\+?[0-9\-\s]+$/', $value)) { $fail('The :attribute must be a valid phone number.'); } if ($type === 'email' && ! filter_var($value, FILTER_VALIDATE_EMAIL)) { $fail('The :attribute must be a valid email address.'); } } } ``` ## 実践的なユースケース ### 日本語文字チェック 全角・半角の文字種を検証するルールです。 ```bash theme={null} php artisan make:rule JapaneseOnly ``` ```php theme={null} validate([ 'name_kana' => ['required', 'string', new JapaneseOnly], ]); ``` ### 電話番号フォーマット検証 日本の電話番号形式を検証するルールです。 ```bash theme={null} php artisan make:rule JapanesePhone ``` ```php theme={null} ```bash theme={null} php artisan make:rule TenantUnique ``` ```php theme={null} data = $data; return $this; } public function validate(string $attribute, mixed $value, Closure $fail): void { $tenantId = $this->data['tenant_id'] ?? null; $query = DB::table($this->table) ->where('tenant_id', $tenantId) ->where($this->column, $value); if ($this->ignoreId !== null) { $query->where('id', '!=', $this->ignoreId); } if ($query->exists()) { $fail(':attributeはすでに使用されています。'); } } } ``` ```php theme={null} 'required|integer|exists:tenants,id', 'name' => [ 'required', 'string', 'max:100', new TenantUnique('projects', 'name'), ], ]; } public function authorize(): bool { return true; } } ``` 既存レコードを更新する場合は、自分自身のIDを除外して重複チェックします。 ```php theme={null} class UpdateProjectRequest extends FormRequest { public function rules(): array { $projectId = $this->route('project'); return [ 'tenant_id' => 'required|integer|exists:tenants,id', 'name' => [ 'required', 'string', 'max:100', new TenantUnique('projects', 'name', ignoreId: $projectId), ], ]; } } ``` ## サービスプロバイダーでのルール登録 ### Validator::extend() でルールを追加する `Validator::extend()` を使うと、文字列形式(`'rule_name'`)でカスタムルールを使えるようになります。`AppServiceProvider` の `boot()` メソッドで登録します。 ```php theme={null} validate([ 'name_kana' => 'required|japanese_only', ]); ``` `Validator::extend()` はルールオブジェクトより古い登録方式です。新規開発では `ValidationRule` インターフェースを実装したルールオブジェクトを使うことを推奨します。 ### Rule クラスにスタティックメソッドとして追加する `Rule` ファサードにマクロを追加することで、`Rule::myRule()` のような流暢なAPI(Fluent API)を提供できます。 ```php theme={null} use Illuminate\Validation\Rule; Rule::macro('tenantUnique', function (string $table, string $column, ?int $ignoreId = null) { return new \App\Rules\TenantUnique($table, $column, $ignoreId); }); ``` ```php theme={null} // 使用例 $request->validate([ 'name' => ['required', Rule::tenantUnique('projects', 'name')], ]); ``` ## 内部実装の詳細 `Illuminate\Validation\Validator` がカスタムルールを呼び出す仕組みを見てみましょう。 バリデーター内では `validateAttribute()` が各フィールドを処理します。ルールが `ValidationRule` インターフェースを実装している場合、`validateUsingCustomRule()` メソッドが呼び出されます。 ```php theme={null} // Illuminate\Validation\Validator::validateUsingCustomRule() の簡略版 protected function validateUsingCustomRule($attribute, $value, $rule) { // DataAwareRule の場合、全データを注入する if ($rule instanceof DataAwareRule) { $rule->setData($this->getData()); } // ValidatorAwareRule の場合、バリデーター自身を注入する if ($rule instanceof ValidatorAwareRule) { $rule->setValidator($this); } // validate() を呼び出す $rule->validate($attribute, $value, function ($message, $translate = false) use ($attribute, $rule) { // $fail クロージャ — エラーメッセージを追加する $this->errors()->add($attribute, $this->makeReplacements( $message, $attribute, get_class($rule), [], // 追加のプレースホルダー置換(例: ['min' => '8'] など) )); }); } ``` `ImplicitRule` の処理は `isImplicit()` メソッドで判断され、値が空の場合でもルールのチェックを実行します。 ```php theme={null} // 空の値に対してルールを実行するかどうかを判断する protected function isImplicit($rule): bool { return $rule instanceof ImplicitRule || in_array($rule, $this->implicitRules); } ``` `DataAwareRule` と `ValidatorAwareRule` を同時に実装することもできます。両インターフェースを持つクラスでは、どちらの注入も行われます。 ## 関連ページ コントローラーやフォームリクエストでの標準的なバリデーション方法を確認します。 # 遅延サービスプロバイダー Source: https://kawax.biz/jp/advanced/deferred-provider DeferrableProvider インターフェースを実装してサービスの遅延読み込みを行う方法を解説します。パッケージのパフォーマンス最適化に直結する地味ながら重要な機能です。 Laravelの起動時に登録されるサービスプロバイダーは、たとえその機能をリクエスト中に一度も使わなくても毎回読み込まれます。遅延サービスプロバイダー(Deferred Service Provider)はこの問題を解決し、サービスが実際に使われるまで読み込みを後回しにできます。 このページは[パッケージ開発の基礎](/jp/advanced/package-development)の知識を前提としています。サービスプロバイダーの基本的な仕組みを理解してから読むことをおすすめします。 ## なぜ遅延プロバイダーが必要か 通常のサービスプロバイダーはすべてのリクエストで `register()` と `boot()` が呼ばれます。メール送信、キュー、キャッシュなど、すべてのページで使わない機能まで毎回初期化するのは無駄です。 ```php theme={null} // このプロバイダーはすべてのリクエストで register() が呼ばれる class ReportServiceProvider extends ServiceProvider { public function register(): void { // 重いレポートサービスを毎回バインドする $this->app->singleton(ReportGenerator::class, function ($app) { return new ReportGenerator( $app->make(PdfRenderer::class), $app->make(ChartRenderer::class), $app->make(DataExporter::class), ); }); } } ``` このプロバイダーを遅延化すると、レポートを実際に生成するリクエストでのみ初期化されます。 *** ## DeferrableProvider インターフェース `Illuminate\Contracts\Support\DeferrableProvider` は `provides()` メソッドひとつを持つシンプルなインターフェースです。 ```php theme={null} namespace Illuminate\Contracts\Support; interface DeferrableProvider { public function provides(); } ``` このインターフェースを実装するだけで、プロバイダーは遅延モードになります。 *** ## 基本的な実装 遅延プロバイダーの実装は3ステップです。 ```php theme={null} use Illuminate\Contracts\Support\DeferrableProvider; use Illuminate\Support\ServiceProvider; class ReportServiceProvider extends ServiceProvider implements DeferrableProvider { // ... } ``` 通常のプロバイダーと同様に `register()` にバインドを記述します。 ```php theme={null} public function register(): void { $this->app->singleton(ReportGenerator::class, function ($app) { return new ReportGenerator( $app->make(PdfRenderer::class), $app->make(ChartRenderer::class), $app->make(DataExporter::class), ); }); $this->app->singleton(ReportRepository::class, function ($app) { return new ReportRepository($app->make('db')); }); } ``` `provides()` は `register()` でバインドした**すべてのサービス**を返す必要があります。Laravelはこのリストを元に「どのサービスを要求したときにこのプロバイダーを読み込むか」を判断します。 ```php theme={null} public function provides(): array { return [ ReportGenerator::class, ReportRepository::class, ]; } ``` *** ## サービスマニフェストの仕組み Laravel は起動時に `bootstrap/cache/services.php` というマニフェストファイルを生成します。このファイルに遅延プロバイダーが提供するサービスの一覧が保存されています。 ```php theme={null} // bootstrap/cache/services.php の例(自動生成) return [ 'providers' => [ // bootstrap/providers.php と同じリスト ], 'eager' => [ // 即時読み込みのプロバイダー App\Providers\AppServiceProvider::class, ], 'deferred' => [ // サービス名 => プロバイダークラス のマッピング 'App\Services\ReportGenerator' => ReportServiceProvider::class, 'App\Repositories\ReportRepository' => ReportServiceProvider::class, 'cache' => Illuminate\Cache\CacheServiceProvider::class, 'cache.store' => Illuminate\Cache\CacheServiceProvider::class, 'queue' => Illuminate\Queue\QueueServiceProvider::class, ], 'when' => [], ]; ``` このマニフェストにより、Laravelはファイルを読み込まずに「このサービスはどのプロバイダーが提供するか」を把握できます。実際のプロバイダーは対象サービスが初めて解決されたときだけ読み込まれます。 プロバイダーを追加・変更した場合はマニフェストを再生成してください。 ```shell theme={null} php artisan optimize:clear # または php artisan clear-compiled ``` ### 内部動作のフロー ```mermaid theme={null} sequenceDiagram participant App as Laravel
    アプリ participant Container as サービス
    コンテナ participant Manifest as services.php
    マニフェスト participant Provider as ReportService
    Provider App->>Manifest: 起動時にマニフェストを読み込む Manifest-->>App: deferred サービス一覧を返す Note over App: この時点ではプロバイダーは読み込まれていない App->>Container: $app->make(ReportGenerator::class) Container->>Manifest: deferred サービスか確認 Manifest-->>Container: ReportServiceProvider が提供する Container->>Provider: register() + boot() を呼び出す Provider-->>Container: バインディングを登録 Container-->>App: ReportGenerator のインスタンスを返す ``` *** ## provides() メソッドの重要性 `provides()` に**登録漏れ**があると、そのサービスは永遠に解決されません。 ```php theme={null} public function register(): void { $this->app->singleton(ReportGenerator::class, fn ($app) => new ReportGenerator()); // 追加したバインディング $this->app->singleton('report', fn ($app) => $app->make(ReportGenerator::class)); } public function provides(): array { return [ ReportGenerator::class, 'report', // ← 文字列キーでバインドした場合も忘れずに記載する ]; } ``` `$bindings` / `$singletons` プロパティを使っている場合も同様に `provides()` に含めます。 ```php theme={null} class AnalyticsServiceProvider extends ServiceProvider implements DeferrableProvider { public $singletons = [ AnalyticsClient::class => DefaultAnalyticsClient::class, ]; public function provides(): array { // $singletons に列挙したキーをすべて返す return [ AnalyticsClient::class, ]; } } ``` *** ## 遅延プロバイダーの制約 遅延プロバイダーは**コンテナへのバインディング登録だけ**を目的としたプロバイダーに向いています。以下のようなことを `boot()` で行うプロバイダーは遅延にできません。 | 制約 | 理由 | | - | - | | ルート登録 | ルートはアプリ起動時に解決されるため、遅延では機能しない | | グローバルミドルウェア登録 | リクエスト処理前に登録が必要 | | イベントリスナー登録(常時必要なもの) | イベントが発火するより前に登録されていないと機能しない | | Blade ディレクティブの追加 | ビューのコンパイル前に登録が必要 | 遅延プロバイダーに `boot()` メソッドを書くことは可能ですが、その内容はサービスが解決されるまで実行されません。ルートやミドルウェアのような「常に必要な処理」を `boot()` に書くと、予期しない動作になります。 *** ## when() メソッド — イベントトリガーによる登録 `when()` メソッドを使うと、特定のイベントが発火したときにプロバイダーを登録できます。これは、ジョブ処理系など特定のコンテキストでのみ必要なプロバイダーに使えます。 ```php theme={null} class ReportServiceProvider extends ServiceProvider implements DeferrableProvider { public function register(): void { $this->app->singleton(ReportGenerator::class, fn () => new ReportGenerator()); } public function provides(): array { return [ReportGenerator::class]; } /** * サービスのバインディング解決ではなく、 * 指定したイベントが発火したときにもプロバイダーを読み込む。 */ public function when(): array { return [ \App\Events\ReportRequested::class, ]; } } ``` `when()` に返したイベントが発火すると、サービスが直接解決されていなくてもプロバイダーが読み込まれます。 *** ## パッケージ開発での活用 サードパーティパッケージとして配布するときも、遅延プロバイダーはユーザーのアプリケーションのパフォーマンスに貢献します。 ### 推奨パターン ```php theme={null} namespace Acme\Analytics; use Illuminate\Contracts\Support\DeferrableProvider; use Illuminate\Support\ServiceProvider; class AnalyticsServiceProvider extends ServiceProvider implements DeferrableProvider { public function register(): void { $this->mergeConfigFrom(__DIR__.'/../config/analytics.php', 'analytics'); $this->app->singleton(AnalyticsManager::class, function ($app) { return new AnalyticsManager($app->make('config')->get('analytics')); }); $this->app->singleton('analytics', fn ($app) => $app->make(AnalyticsManager::class)); } public function boot(): void { // boot() を使うのはパブリッシュの登録だけにする if ($this->app->runningInConsole()) { $this->publishes([ __DIR__.'/../config/analytics.php' => config_path('analytics.php'), ], 'analytics-config'); } } public function provides(): array { return [ AnalyticsManager::class, 'analytics', ]; } } ``` `mergeConfigFrom()` は設定のキャッシュ有無を内部でチェックしているため、遅延プロバイダーの `register()` の中で呼んでも安全です。ただし設定がキャッシュ済みの場合は何もしません。 ### `runningInConsole()` で Artisan コマンド登録を分離する コマンド登録は Artisan 起動時のみ必要なので、`runningInConsole()` で条件分岐します。ただしコマンドを提供するプロバイダーを遅延にする場合は、コマンドクラスも `provides()` に含めるか、コマンド専用のプロバイダーを別に用意してください。 ```php theme={null} public function boot(): void { if ($this->app->runningInConsole()) { $this->commands([ AnalyticsFlushCommand::class, ]); } } ``` *** ## Laravel コアでの使用例 Laravelのコアプロバイダーの多くは遅延プロバイダーです。毎リクエストで使わないサービスをすべて即時読み込みしないための設計です。 | プロバイダー | 提供するサービス | | - | - | | `CacheServiceProvider` | `cache`, `cache.store`, `RateLimiter` | | `QueueServiceProvider` | `queue`, `queue.worker`, `queue.failer` | | `MailServiceProvider` | `mail.manager`, `mailer` | | `RedisServiceProvider` | `redis`, `redis.connection` | | `HashServiceProvider` | `hash`, `hash.driver` | | `ValidationServiceProvider` | `validator`, `validation.presence` | | `TranslationServiceProvider` | `translator` | | `BroadcastServiceProvider` | `Broadcast` | API エンドポイントのみのアプリケーションでは、`MailServiceProvider` や `BroadcastServiceProvider` がリクエスト中に一度も読み込まれないこともあります。 *** ## 遅延化すべきかの判断基準 * すべてのリクエストで使われるわけではないサービス(メール、レポート、外部 API クライアントなど) * 初期化に外部接続やファイル読み込みが必要なサービス * 重いオブジェクトグラフを持つサービス * CLI でのみ使うコマンドを提供するプロバイダー * ルートを登録するプロバイダー(`loadRoutesFrom` など) * 常時機能するミドルウェアや例外ハンドラーを登録するプロバイダー * Eloquent のグローバルスコープやオブザーバーを登録するプロバイダー * リクエストの大半で使われる軽量なサービス(遅延のオーバーヘッドの方が大きくなる場合) *** ## 関連ページ サービスプロバイダーを核としたLaravelパッケージの開発方法を解説します。 LaravelとPHPのメジャーバージョンアップに対応するパッケージのメンテナンス戦略を解説します。 # Dumpableトレイト Source: https://kawax.biz/jp/advanced/dumpable Illuminate\Support\Traits\Dumpable で任意のクラスに dump() / dd() を追加し、チェーンを崩さずにデバッグする方法を解説します。 ## Dumpableトレイトとは `Illuminate\Support\Traits\Dumpable` は、任意のクラスに `dump()` と `dd()` を追加するための小さなトレイトです。Laravel 10で導入され、Collection・Eloquent Builder・Request などLaravelコアのデバッグ体験を統一するために広く使われています。 目的はシンプルです。オブジェクトを `var_dump` 相当で確認しながら、必要ならその場で処理を止めることです。 ```mermaid theme={null} flowchart TD A["オブジェクトで dump()/dd() を呼ぶ"] --> B{"どちらを呼んだか?"} B -- "dump()" --> C["$this と追加引数を出力"] C --> D["処理を続行"] B -- "dd()" --> E["$this と追加引数を出力"] E --> F["処理を終了"] ``` ## 実装を確認する 実装はとてもシンプルで、`dump($this, ...$args)` と `dd($this, ...$args)` を呼び出しているだけです。 ```php theme={null} trait Dumpable { public function dump(...$args): static { dump($this, ...$args); return $this; } public function dd(...$args): never { dd($this, ...$args); } } ``` Laravel 13 の実装は `src/Illuminate/Support/Traits/Dumpable.php` にあります。実際のシグネチャはPHPDocで `@return $this` と `@return never` を表現しています。 ## 自分のクラスで使う `use Dumpable;` を追加するだけで、インスタンスメソッドとして `dump()` / `dd()` が使えます。 ```php theme={null} use Illuminate\Support\Traits\Dumpable; class UserData { use Dumpable; public function __construct( public readonly string $name, public readonly string $email, ) {} } $user = new UserData('Taro', 'taro@example.com'); $user->dump(); // dumpして続行 $user->dd(); // dumpして終了 ``` ## チェーン中にデバッグを挟む `dump()` は `static`(実装上は `$this`)を返すので、メソッドチェーンの途中で安全に使えます。 ```php theme={null} $result = collect([1, 2, 3]) ->map(fn ($n) => $n * 2) ->dump() // ここで中身を確認 ->filter(fn ($n) => $n > 2) ->values(); ``` `dd()` に置き換えると、その地点で処理が止まるため、重い後続処理の前に状態を確認したいときに便利です。 ## パッケージ開発での活用 独自の Value Object・DTO・Builder に `Dumpable` を入れておくと、利用者が追加ツールなしで状態確認できます。 ```php theme={null} use Illuminate\Support\Traits\Dumpable; final class InvoiceData { use Dumpable; public function __construct( public readonly string $number, public readonly int $total, ) {} } ``` ```php theme={null} $payload = (new PackageRequestBuilder) ->forUser($userId) ->withLocale('ja') ->dump('before send') ->toArray(); ``` ## 追加引数を渡す `Dumpable` は内部で `dump($this, ...$args)` / `dd($this, ...$args)` を呼ぶため、文脈情報を一緒に出力できます。 ```php theme={null} $user->dump('デバッグポイントA'); // $this と 'デバッグポイントA' が同時に出力される ``` これは複数箇所で `dump()` を使うときに有効です。どの地点で出力されたのかをすぐ判別できます。 ## 関連トレイト 副作用を挟みつつ値を返すパターンを学びます。 条件付きでチェーン処理を分岐する設計を学びます。 既存クラスを拡張して独自メソッドを追加する方法を学びます。 # Eloquent Bootable Traits Source: https://kawax.biz/jp/advanced/eloquent-bootable-traits Eloquentモデルのトレイト自動起動の仕組みを解説します。`bootXxx()`・`initializeXxx()`メソッドのネーミング規約と、PHP Attributeによる明示的な指定方法を学びます。パッケージ開発でよく使うパターンです。 ## 概要 Eloquentモデルはインスタンス化のたびに「起動処理(boot)」と「初期化処理(initialize)」を実行します。トレイトにこれらのメソッドを定義しておくと、モデルがそのトレイトを `use` するだけで自動的に呼び出されます。 この仕組みを使うことで、モデルに機能を注入するトレイトをクリーンに実装できます。Laravelフレームワーク自体も `SoftDeletes` などの多くのトレイトでこのパターンを活用しています。 ```mermaid theme={null} sequenceDiagram participant App as アプリケーション participant Model as Eloquent Model participant Trait as トレイト App->>Model: new Post() または Post::find() Model->>Model: bootIfNotBooted() Model->>Trait: bootXxx() (クラス初回のみ) Model->>Model: initializeTraits() Model->>Trait: initializeXxx() (インスタンスごと) Model-->>App: モデルインスタンス ``` *** ## boot と initialize の違い | | `bootXxx()` | `initializeXxx()` | | - | - | - | | **呼び出しタイミング** | モデルクラスの最初の起動時(クラスごとに1回) | インスタンス生成のたび | | **メソッド種別** | `static` | インスタンスメソッド | | **用途** | イベントリスナー登録、グローバルスコープ追加など | デフォルト値設定、プロパティ初期化など | *** ## ネーミング規約 ### bootXxx() トレイト名の末尾に `boot` プレフィックスをつけた **`static`** メソッドを定義すると、モデル起動時に一度だけ呼ばれます。 ```php theme={null} namespace App\Models\Concerns; trait HasSlug { public static function bootHasSlug(): void { static::creating(function ($model) { if (empty($model->slug)) { $model->slug = str($model->name)->slug()->toString(); } }); } } ``` モデル側での使用: ```php theme={null} use App\Models\Concerns\HasSlug; class Article extends Model { use HasSlug; // bootHasSlug() は自動的に呼び出される } ``` ### initializeXxx() トレイト名の末尾に `initialize` プレフィックスをつけたインスタンスメソッドを定義すると、モデルの `new` のたびに呼ばれます。 ```php theme={null} namespace App\Models\Concerns; trait HasDefaultStatus { public function initializeHasDefaultStatus(): void { $this->attributes['status'] ??= 'draft'; } } ``` `$casts` への動的な追加にも使えます: ```php theme={null} trait HasMetadata { public function initializeHasMetadata(): void { $this->mergeCasts([ 'metadata' => 'array', ]); } } ``` *** ## PHP Attribute による明示的な指定 Laravel 12 以降、ネーミング規約に頼らず PHP Attribute で明示的にメソッドを指定できます。 | Attribute | 対象 | | - | - | | `#[Boot]` | クラス起動時に一度だけ実行するメソッド | | `#[Initialize]` | インスタンス生成のたびに実行するメソッド | ```php theme={null} use Illuminate\Database\Eloquent\Attributes\Boot; use Illuminate\Database\Eloquent\Attributes\Initialize; trait HasAuditLog { #[Boot] public static function registerAuditListeners(): void { static::created(function ($model) { AuditLog::record('created', $model); }); static::updated(function ($model) { AuditLog::record('updated', $model); }); } #[Initialize] public function setAuditDefaults(): void { $this->attributes['audit_enabled'] ??= true; } } ``` PHP Attribute を使うと、メソッド名がトレイト名に縛られないため、意味のある名前をつけられます。複数の `#[Boot]` メソッドや `#[Initialize]` メソッドを同じトレイトに定義することも可能です。 *** ## 実用例:パッケージ開発での活用 ### グローバルスコープの自動追加 ```php theme={null} trait HasTenant { public static function bootHasTenant(): void { static::addGlobalScope(new TenantScope); } } ``` ### デフォルトキャスト追加 ```php theme={null} trait HasJsonSettings { public function initializeHasJsonSettings(): void { $this->mergeCasts([ 'settings' => 'array', ]); } } ``` ### Eloquentイベントで自動処理 ```php theme={null} trait Archivable { #[Boot] public static function bootArchivable(): void { static::deleting(function ($model) { Archive::store($model->toArray()); }); } } ``` *** ## Laravelフレームワーク内の例 Laravelフレームワーク自身も多くのトレイトでこのパターンを採用しています。 | トレイト | メソッド | 処理内容 | | - | - | - | | `SoftDeletes` | `bootSoftDeletes()` | ソフトデリート用グローバルスコープの追加 | | `HasFactory` | `bootHasFactory()` | 特になし(スコープ外の仕組み) | | `HasUlids` | `bootHasUlids()` | `creating` イベントで ULID を自動セット | | `HasUuids` | `bootHasUuids()` | `creating` イベントで UUID を自動セット | これらを参考実装として読むと、パターンの理解が深まります。 *** ## 注意点 ### モデルキャッシュのクリア `boot*` はクラスごとに一度しか実行されないため、テスト環境では注意が必要です。`Model::clearBootedModels()` で起動済みキャッシュをリセットできます。 ```php theme={null} // テスト後にモデルのブート状態をリセット protected function tearDown(): void { Model::clearBootedModels(); parent::tearDown(); } ``` ### 実行順序 同一モデルに複数のトレイトが `use` されている場合、`bootXxx()` / `initializeXxx()` はPHPのトレイト解決順(宣言順)で呼ばれます。依存関係がある場合は `use` の順序に注意してください。 *** ## まとめ | パターン | 方法 | 適用場面 | | - | - | - | | クラス起動時処理 | `static bootXxx()` または `#[Boot]` | イベント登録、グローバルスコープ追加 | | インスタンス初期化 | `initializeXxx()` または `#[Initialize]` | デフォルト値、キャスト追加 | トレイトに `bootXxx()` / `initializeXxx()` を実装しておくと、モデルへの機能注入が `use` 一行で済むようになります。パッケージでモデルに機能を追加する際の定番パターンです。 # Eloquentのカスタムキャスト Source: https://kawax.biz/jp/advanced/eloquent-casts CastsAttributes インターフェースを実装してカスタムキャストを作成する方法を解説します。Value Objectパターン、インバウンドキャスト、Castablesなど上級のパターンも紹介します。 ## キャストとは Eloquentのキャストは、データベースから取得した生の値をPHPのデータ型に変換し、保存時にはその逆変換をする仕組みです。`casts` メソッドで定義します。 ```php theme={null} protected function casts(): array { return [ 'is_admin' => 'boolean', 'settings' => 'array', 'created_at' => 'datetime', ]; } ``` ## 組み込みキャストの種類 Laravelが標準で提供するキャスト一覧です。 | キャスト | 説明 | | - | - | | `integer` / `int` | 整数に変換 | | `float` / `double` | 浮動小数点数に変換 | | `string` | 文字列に変換 | | `boolean` / `bool` | 真偽値に変換(`0`/`1` を含む) | | `array` | JSON文字列 ↔ PHP配列 | | `collection` | JSON文字列 ↔ Collectionインスタンス | | `object` | JSON文字列 ↔ stdObjectインスタンス | | `datetime` | 文字列 ↔ Carbonインスタンス | | `immutable_datetime` | 文字列 ↔ CarbonImmutableインスタンス | | `date` | 文字列 ↔ Carbon(時刻なし) | | `timestamp` | 文字列 ↔ UNIXタイムスタンプ | | `encrypted` | 保存時に暗号化、取得時に復号 | | `hashed` | 保存時にハッシュ化(読み取り専用キャストと組み合わせる) | | `AsStringable::class` | 文字列 ↔ Stringableオブジェクト | | `AsArrayObject::class` | JSON ↔ ArrayObjectインスタンス | | `AsCollection::class` | JSON ↔ Collectionインスタンス | `AsArrayObject` や `AsCollection` は、配列の特定のオフセットを直接変更できるように、Laravel内部でカスタムキャストとして実装されています。 ## カスタムキャストクラスの作成 組み込みキャストでは対応できない変換が必要な場合、`CastsAttributes` インターフェースを実装したカスタムキャストを作成します。 ### インターフェースの定義 フレームワーク本体のコントラクトは次のように定義されています。 ```php theme={null} // src/Illuminate/Contracts/Database/Eloquent/CastsAttributes.php interface CastsAttributes { /** * DBの生の値をPHPの値に変換する(読み取り時) * * @param array $attributes モデルのすべての属性値 */ public function get(Model $model, string $key, mixed $value, array $attributes); /** * PHPの値をDBに保存できる形に変換する(書き込み時) * * @param array $attributes モデルのすべての属性値 */ public function set(Model $model, string $key, mixed $value, array $attributes); } ``` `$attributes` 引数にはモデルの全属性が入っているため、複数のカラムをまたいだ変換も可能です(後述のValue Objectパターン参照)。 ### 基本的なカスタムキャストの実装 `make:cast` コマンドで雛形を生成します。 ```bash theme={null} php artisan make:cast AsMoney ``` `app/Casts/AsMoney.php` が生成されます。例として金額(整数で保存)を `Money` Value Objectに変換するキャストを実装します。 ```php theme={null} amount; } return (int) $value; } } ``` モデルにキャストを適用します。 ```php theme={null} AsMoney::class, ]; } } ``` これで `$order->price` は `Money` インスタンスを返します。 ## Value Objectキャスト 複数のDBカラムをまとめて1つのValue Objectとして扱うパターンです。 ### 実装例: 住所キャスト `address_line_one` と `address_line_two` の2カラムを `Address` Value Objectにまとめます。 ```php theme={null} $this->lineOne, 'line_two' => $this->lineTwo, ]; } public function jsonSerialize(): array { return $this->toArray(); } } ``` ```php theme={null} */ public function set( Model $model, string $key, mixed $value, array $attributes, ): array { if (! $value instanceof Address) { throw new InvalidArgumentException('The given value is not an Address instance.'); } return [ 'address_line_one' => $value->lineOne, 'address_line_two' => $value->lineTwo, ]; } } ``` `set` メソッドで配列を返すと、Eloquentはキーをカラム名として、値をそれぞれのカラムに保存します。単一カラムのキャストでは文字列や整数を返します。 モデルへの適用と使い方は次のとおりです。 ```php theme={null} AsAddress::class, ]; } } ``` ```php theme={null} $user = User::find(1); // Address オブジェクトとして取得できる echo $user->address->lineOne; // Value Object を変更すると保存時に自動的にDBに反映される $user->address = new Address('123 Main St', 'Apt 4B'); $user->save(); ``` ### Value Objectのキャッシュ Value Objectに変換された属性値はEloquentによってキャッシュされます。同じ属性に2回アクセスしても、同じオブジェクトインスタンスが返ります。 キャッシュを無効化したい場合は `$withoutObjectCaching` プロパティをキャストクラスに追加します。 ```php theme={null} class AsAddress implements CastsAttributes { public bool $withoutObjectCaching = true; // ... } ``` ## インバウンドキャスト(書き込み専用) DBへの書き込み時にのみ変換を行い、読み取り時には変換しないキャストです。`CastsInboundAttributes` インターフェースを実装します。 典型的な用途はハッシュ化です。パスワードや秘密値を保存するときだけ変換し、読み取り時はハッシュ値をそのまま返します。 ```bash theme={null} php artisan make:cast AsHash --inbound ``` ```php theme={null} algorithm) ? bcrypt($value) : hash($this->algorithm, $value); } } ``` ## キャストパラメータ カスタムキャストにパラメータを渡す場合は、クラス名の後にコロン区切りで指定します。複数のパラメータはカンマ区切りです。 ```php theme={null} protected function casts(): array { return [ 'secret' => AsHash::class.':sha256', 'data' => AsHash::class.':sha512', ]; } ``` パラメータはキャストクラスのコンストラクタに渡されます。 ```php theme={null} class AsHash implements CastsInboundAttributes { public function __construct( protected string|null $algorithm = null, // ':sha256' が渡される ) {} } ``` ## Castables: Value Object側にキャストロジックを持たせる `Castable` インターフェースを実装した Value Object は、自身のキャストクラスを返す `castUsing` メソッドを持ちます。モデル側でキャストクラスを知らなくて済むため、ドメインロジックが整理されます。 ```php theme={null} $arguments */ public static function castUsing(array $arguments): string { return AsAddress::class; } } ``` モデル側はキャストクラスの代わりに Value Object クラスを指定します。 ```php theme={null} protected function casts(): array { return [ 'address' => Address::class, ]; } ``` `Castable` と無名クラスを組み合わせると、Value Objectとキャストロジックをひとつのファイルにまとめられます。 ```php theme={null} class Address implements Castable { public static function castUsing(array $arguments): CastsAttributes { return new class implements CastsAttributes { public function get(Model $model, string $key, mixed $value, array $attributes): Address { return new Address( $attributes['address_line_one'], $attributes['address_line_two'], ); } public function set(Model $model, string $key, mixed $value, array $attributes): array { return [ 'address_line_one' => $value->lineOne, 'address_line_two' => $value->lineTwo, ]; } }; } } ``` ## `$appends` と `$hidden` との相互作用 キャストと `$appends`、`$hidden` は独立した仕組みですが、組み合わせるときに注意が必要です。 ```php theme={null} class User extends Model { protected function casts(): array { return [ 'address' => AsAddress::class, ]; } // toArray() / toJson() 時に address を除外する protected $hidden = ['address_line_one', 'address_line_two']; // キャスト済みの address をシリアライズ結果に含める protected $appends = ['address']; } ``` `$hidden` に指定するのはDBのカラム名です。キャストを通じて作られる属性名(`address`)ではなく、元のカラム名(`address_line_one`, `address_line_two`)を指定します。 ## 実行時キャストの追加 特定のクエリやリクエストだけキャストを追加したいときは `mergeCasts` メソッドを使います。 ```php theme={null} $user = User::find(1); $user->mergeCasts([ 'extra_data' => 'array', ]); ``` ## 次のステップ モデルの保存・削除などのライフサイクルイベントにフックして処理を追加する方法を学びます。 # カスタムピボットモデルとchaperone Source: https://kawax.biz/jp/advanced/eloquent-custom-pivot belongsToManyの中間テーブルをカスタムPivotモデルとして扱う方法と、Laravel 13で追加されたchaperoneによる自動Eagerロードを解説します。 ## カスタムピボットモデルとは [belongsToMany(多対多)](/jp/eloquent-relationships#belongsToMany多対多)の中間テーブルは、デフォルトでは素の `Illuminate\Database\Eloquent\Relations\Pivot` インスタンスとして扱われます。中間テーブルに追加のカラム(承認日時、ロールの種類など)を持たせたり、アクセサ・ミューテタ・独自メソッドを追加したりしたい場合は、`Pivot` を継承したカスタムモデルを作成します。 ```php theme={null} 'boolean', ]; } } ``` `belongsToMany` の定義で `using()` を呼び出し、このカスタムモデルを使うようリレーションに伝えます。 ```php theme={null} belongsToMany(User::class) ->using(RoleUser::class); } } ``` カスタムピボットモデルを保存するときは、モデル名を必ず**アルファベット順の単数形**で命名してください(`RoleUser` であって `UserRole` ではありません)。ただしこれは表示名の慣習であり、実際のクラス名は自由に決められます。 ## `as()`で取得したい属性を指定する デフォルトでは中間テーブルの値は `pivot` プロパティ経由でアクセスします。`as()` メソッドを使うと、この名前を変更できます。 ```php theme={null} return $this->belongsToMany(Role::class) ->using(RoleUser::class) ->as('membership') ->withTimestamps() ->withPivot('approved'); ``` ```php theme={null} foreach ($user->roles as $role) { echo $role->membership->approved; echo $role->membership->created_at; } ``` ## 追加カラムとタイムスタンプ 中間テーブルに `approved` のような追加カラムがある場合は `withPivot()` で明示的に取得対象に含めます。`created_at` / `updated_at` を管理する場合は `withTimestamps()` を呼び出します。 ```php theme={null} return $this->belongsToMany(Role::class) ->using(RoleUser::class) ->withPivot('approved') ->withTimestamps(); ``` Eloquentが中間テーブルの `updated_at` を自動更新するのは、そのピボットモデルが `using()` で明示的に指定されている場合のみです。デフォルトの `Pivot` クラスをそのまま使う場合でも `withTimestamps()` は動作しますが、`using()` でカスタムモデルを指定するとカスタムイベント・カスタムキャストなどが利用できるようになります。 ## ピボットモデルからの逆参照 カスタムピボットモデルには、宣言元モデル・関連先モデルへの `belongsTo` リレーションを自由に定義できます。 ```php theme={null} belongsTo(Role::class); } public function user(): BelongsTo { return $this->belongsTo(User::class); } } ``` これにより、ピボットモデル単体を取得した場合でも `$roleUser->role` や `$roleUser->user` で関連モデルにアクセスできます。ただし、これらのリレーションを親クエリ実行時に自動的にEagerロードしたい場合は、以下の `chaperone()` を使う方法があります。 ## `chaperone()`による自動Eagerロード(Laravel 13) Laravel 13で、ピボットモデルに定義した `role()` / `user()` のような `belongsTo` リレーションを、`belongsToMany` クエリの実行時に自動でハイドレート(Eagerロード相当の紐付け)できる `chaperone()` メソッドが追加されました。 ```php theme={null} belongsToMany(User::class) ->using(RoleUser::class) ->chaperone(); } } ``` `chaperone()` を呼び出すと、Eloquentはピボットモデル(`RoleUser`)が持つ `belongsTo` リレーションの名前を自動的に推測し、`Role::with('users')` のようにコレクションを取得した際、各ピボットに紐づく宣言元モデル・関連先モデルへの参照を追加のクエリなしで設定します。 ```php theme={null} $role = Role::with('users')->first(); foreach ($role->users as $user) { // 追加クエリなしで、ピボット経由の親モデルにアクセスできる echo $user->pivot->role->name; } ``` ### 非標準なリレーション名を使う場合 ピボットモデルの `belongsTo` リレーションのメソッド名が標準的な命名(宣言元・関連先モデル名のキャメルケース単数形)と異なる場合は、`chaperone()` の引数で明示的に指定します。 ```php theme={null} return $this->belongsToMany(User::class) ->using(RoleUser::class) ->chaperone(declaring: 'role', related: 'user'); ``` `chaperone()` は「N+1問題」を中間テーブル経由の参照についても解消してくれる仕組みです。ピボットモデルから親モデルの情報(承認日時とユーザー名を一緒に表示する、など)を頻繁に参照するアプリケーションでは、通常の `with()` によるEagerロードと組み合わせて活用すると効果的です。 ## 次のステップ belongsToManyを含む基本的なリレーションの定義方法に戻って復習します。 モデルイベントを使ってピボットモデルの保存・更新をフックする方法を学びます。 # Eloquent Observers とモデルイベント Source: https://kawax.biz/jp/advanced/eloquent-observers Eloquentモデルが発火するイベントの仕組みと、Observerクラスを使ってイベントを一元管理する方法を解説します。`#[ObservedBy]` アトリビュートなどLaravel 13の最新機能も含みます。 ## モデルイベントとは Eloquentモデルはライフサイクルの各タイミングで自動的にイベントを発火します。これらのイベントにフックすることで、モデルの保存・削除などの前後に処理を差し込めます。 Eloquentが発火するイベントは以下のとおりです。 | イベント | 発生タイミング | | - | - | | `retrieved` | DBからモデルを取得したとき | | `creating` | 新規モデルを保存する直前 | | `created` | 新規モデルを保存した直後 | | `updating` | 既存モデルを更新する直前 | | `updated` | 既存モデルを更新した直後 | | `saving` | 新規作成・更新のどちらかで保存する直前 | | `saved` | 新規作成・更新のどちらかで保存した直後 | | `deleting` | モデルを削除する直前 | | `deleted` | モデルを削除した直後 | | `trashed` | ソフトデリートした直後 | | `forceDeleting` | 物理削除する直前 | | `forceDeleted` | 物理削除した直後 | | `restoring` | ソフトデリートを復元する直前 | | `restored` | ソフトデリートを復元した直後 | | `replicating` | `replicate()` を呼び出したとき | `-ing` で終わるイベントは変更がDBに永続化される**前**に、`-ed` で終わるイベントは**後**に発火します。 マスアップデートやマスデリート(`User::where(...)->update(...)` など)では、`saving`、`saved`、`updating`、`updated`、`deleting`、`deleted` イベントは発火しません。モデルが実際には取得されないためです。 ## クロージャを使ったイベントリスナー イベントをシンプルに扱いたい場合は、モデルの `booted` メソッド内でクロージャを登録できます。 ```php theme={null} */ protected $dispatchesEvents = [ 'saved' => UserSaved::class, 'deleted' => UserDeleted::class, ]; } ``` マッピングしたイベントクラスには、コンストラクタでモデルのインスタンスを受け取ります。 ```php theme={null} `make:observer` コマンドで雛形を生成します。`--model` オプションでモデルを指定すると、対応するメソッドが自動で追加されます。 ```bash theme={null} php artisan make:observer UserObserver --model=User ``` `app/Observers/UserObserver.php` が生成されます。 メソッド名がイベント名に対応します。引数にはモデルのインスタンスが渡されます。 ```php theme={null} 登録方法は2つあります。Laravel 13では `#[ObservedBy]` アトリビュートを使う方法が推奨されます。 **方法1: `#[ObservedBy]` アトリビュート(推奨)** モデルクラスにアトリビュートを付与するだけで登録が完了します。`AppServiceProvider` の変更が不要です。 ```php theme={null} `#[ObservedBy]` アトリビュートは `Illuminate\Database\Eloquent\Attributes` 名前空間にあります。PHP 8.0以降のネイティブ構文で、Laravel 13で積極的に採用されています。 ## データベーストランザクション内でのObserver モデルがトランザクション内で作成・更新される場合、トランザクションのコミット後にObserverを実行したいことがあります。`ShouldHandleEventsAfterCommit` インターフェースを実装するとその挙動になります。 ```php theme={null} delete(); return User::find(2); }); ``` ### `saveQuietly` で保存時のイベントを止める イベントを発火させずにモデルを保存したいときは `saveQuietly` を使います。 ```php theme={null} $user = User::findOrFail(1); $user->name = 'Victoria Faith'; $user->saveQuietly(); ``` 同様のメソッドが削除・復元・レプリケートにも用意されています。 ```php theme={null} $user->deleteQuietly(); $user->restoreQuietly(); ``` ## 実践的なユースケース ### キャッシュの自動クリア モデルが更新・削除されたときに関連するキャッシュを自動でクリアします。 ```php theme={null} id}"); Cache::forget('posts:latest'); } public function deleted(Post $post): void { Cache::forget("post:{$post->id}"); Cache::forget('posts:latest'); } } ``` ### 監査ログの記録 モデルの変更履歴を自動で記録します。`getDirty()` で変更前後の値を取得できます。 ```php theme={null} User::class, 'model_id' => $user->id, 'changes' => $user->getDirty(), 'user_id' => auth()->id(), ]); } public function deleted(User $user): void { AuditLog::create([ 'model_type' => User::class, 'model_id' => $user->id, 'changes' => ['deleted' => true], 'user_id' => auth()->id(), ]); } } ``` `updating` イベントはDBへの保存**前**に発火するため、`getDirty()` で変更予定の値を取得できます。`updated` イベント後に呼ぶと `getDirty()` は空になります。 ### 関連モデルの自動更新 注文が完了したときに在庫数を自動で更新する例です。 ```php theme={null} items as $item) { $item->product->decrement('stock', $item->quantity); } } public function deleted(Order $order): void { foreach ($order->items as $item) { $item->product->increment('stock', $item->quantity); } } } ``` ## 次のステップ `#[ObservedBy]` を含む、Laravel 13のPHPアトリビュートをまとめて学びます。 # Eloquentのスコープ Source: https://kawax.biz/jp/advanced/eloquent-scopes ローカルスコープとグローバルスコープの仕組みを解説します。SoftDeletingScope などのフレームワーク内部実装を読み解きながら、マルチテナントや公開/非公開フィルタなどの実践的なユースケースを紹介します。 ## スコープとは Eloquentのスコープは、クエリに対する制約をまとめて再利用できる仕組みです。スコープには2種類あります。 | 種類 | 適用タイミング | 用途 | | - | - | - | | **グローバルスコープ** | 常に自動で適用 | ソフトデリート、マルチテナント、公開フィルタ | | **ローカルスコープ** | 明示的に呼び出したときだけ適用 | 「人気の投稿」「アクティブユーザー」などの共通フィルタ | ## ローカルスコープ ### 定義 ローカルスコープは、モデルのメソッドに `#[Scope]` アトリビュートを付与して定義します。 ```php theme={null} where('status', 'published'); } /** * 人気の投稿に絞り込む */ #[Scope] protected function popular(Builder $query): void { $query->where('views', '>', 1000); } } ``` `#[Scope]` アトリビュートは `Illuminate\Database\Eloquent\Attributes` 名前空間にあります。PHP 8.0以降のネイティブ構文です。 ### 使い方 定義したスコープは、メソッドとして呼び出せます。チェーンもできます。 ```php theme={null} use App\Models\Post; // 公開済み投稿のみ取得 $posts = Post::published()->get(); // 公開済みかつ人気の投稿を最新順で取得 $posts = Post::published()->popular()->latest()->get(); ``` ### パラメータの受け渡し スコープメソッドの第2引数以降に追加のパラメータを定義できます。 ```php theme={null} #[Scope] protected function ofStatus(Builder $query, string $status): void { $query->where('status', $status); } ``` 呼び出しのときにそのまま引数を渡します。 ```php theme={null} $posts = Post::ofStatus('draft')->get(); $posts = Post::ofStatus('published')->get(); ``` ### `orWhere` との組み合わせ スコープを `orWhere` でつなぐとき、論理グループが必要になる場合があります。 ```php theme={null} // クロージャを使う方法(確実だが冗長) $users = User::popular()->orWhere(function (Builder $query) { $query->active(); })->get(); // 高階メソッドを使うとシンプルに書ける $users = User::popular()->orWhere->active()->get(); ``` ## グローバルスコープ ### 仕組み グローバルスコープは `Illuminate\Database\Eloquent\Scope` インターフェースを実装したクラスです。このインターフェースは `apply` メソッド1つだけを要求します。 ```php theme={null} // フレームワーク本体のインターフェース定義 // src/Illuminate/Database/Eloquent/Scope.php interface Scope { public function apply(Builder $builder, Model $model); } ``` `apply` メソッドの中でクエリビルダーに制約を追加します。 ### グローバルスコープクラスの作成 `make:scope` コマンドで雛形を生成します。 ```bash theme={null} php artisan make:scope ActiveScope ``` `app/Models/Scopes/ActiveScope.php` が生成されます。 ```php theme={null} where('is_active', true); } } ``` ### モデルへの適用 Laravel 13では `#[ScopedBy]` アトリビュートを使うのが最もシンプルです。 ```php theme={null} `booted` メソッドをオーバーライドして `addGlobalScope` を呼び出す方法もあります。 ```php theme={null} グローバルスコープを追加すると、`User::all()` など全クエリに自動で `WHERE is_active = 1` が付きます。 ### 無名クロージャによるグローバルスコープ クラスを別ファイルに作るほどでもない単純なスコープは、クロージャで定義できます。 ```php theme={null} protected static function booted(): void { static::addGlobalScope('active', function (Builder $builder) { $builder->where('is_active', true); }); } ``` クロージャで定義したスコープを後から除外するには、クラス名ではなくスコープ名(文字列)を使う必要があります。 ### グローバルスコープの除外 特定のクエリではスコープを無効にしたい場面があります。 ```php theme={null} use App\Models\Scopes\ActiveScope; // 特定のスコープを除外 User::withoutGlobalScope(ActiveScope::class)->get(); // クロージャで定義したスコープを除外 User::withoutGlobalScope('active')->get(); // すべてのグローバルスコープを除外 User::withoutGlobalScopes()->get(); // 複数のスコープを除外 User::withoutGlobalScopes([ActiveScope::class, TenantScope::class])->get(); // 指定したスコープ以外をすべて除外 User::withoutGlobalScopesExcept([TenantScope::class])->get(); ``` ## フレームワーク内部: SoftDeletingScope Laravel標準の `SoftDeletes` トレイトがグローバルスコープをどう活用しているかを見ると、実装パターンが分かります。 `SoftDeletingScope` は `Scope` インターフェースを実装しています。 ```php theme={null} // src/Illuminate/Database/Eloquent/SoftDeletingScope.php class SoftDeletingScope implements Scope { protected $extensions = [ 'Restore', 'RestoreOrCreate', 'CreateOrRestore', 'WithTrashed', 'WithoutTrashed', 'OnlyTrashed', ]; /** * スコープをクエリビルダーに適用する * deleted_at が NULL のレコードだけを取得するよう制約を追加 */ public function apply(Builder $builder, Model $model) { $builder->whereNull($model->getQualifiedDeletedAtColumn()); } /** * クエリビルダーにマクロを拡張する * withTrashed() / onlyTrashed() などが使えるようになる */ public function extend(Builder $builder) { foreach ($this->extensions as $extension) { $this->{"add{$extension}"}($builder); } // delete() 操作をオーバーライドして deleted_at を更新する処理にする $builder->onDelete(function (Builder $builder) { $column = $this->getDeletedAtColumn($builder); return $builder->update([ $column => $builder->getModel()->freshTimestampString(), ]); }); } } ``` `withTrashed()` は実際には `withoutGlobalScope($this)` を呼び出しています。つまり `SoftDeletingScope` 自身を除外することで、削除済みレコードも取得できるようにしています。 ```php theme={null} protected function addWithTrashed(Builder $builder) { $builder->macro('withTrashed', function (Builder $builder, $withTrashed = true) { if (! $withTrashed) { return $builder->withoutTrashed(); } return $builder->withoutGlobalScope($this); }); } ``` `onlyTrashed()` も同様に、スコープ自身を除外した上で `whereNotNull('deleted_at')` を追加しています。 ```php theme={null} protected function addOnlyTrashed(Builder $builder) { $builder->macro('onlyTrashed', function (Builder $builder) { $model = $builder->getModel(); $builder->withoutGlobalScope($this)->whereNotNull( $model->getQualifiedDeletedAtColumn() ); return $builder; }); } ``` `Scope` インターフェースに `extend` メソッドは定義されていませんが、Eloquentのビルダーはスコープが `extend` メソッドを持っていれば自動的に呼び出します。カスタムマクロを追加したい場合に活用できます。 ## 実践的なユースケース ### マルチテナント: テナントIDによる自動絞り込み SaaSアプリケーションでは、全クエリにテナントIDのフィルタを自動適用することが重要です。 ```php theme={null} user()?->tenant_id) { $builder->where('tenant_id', $tenantId); } } } ``` ```php theme={null} use App\Models\Scopes\TenantScope; use Illuminate\Database\Eloquent\Attributes\ScopedBy; #[ScopedBy([TenantScope::class])] class Post extends Model { // } ``` これで `Post::all()` を呼ぶだけで、認証中のユーザーのテナントデータだけが返ります。 ### 公開/非公開フィルタ 管理画面では非公開の投稿も表示したいが、フロントエンドでは公開済みのみ表示したい場合です。 ```php theme={null} where('status', 'published') ->where('published_at', '<=', now()); } } ``` 管理画面では `withoutGlobalScope` でスコープを除外します。 ```php theme={null} // フロントエンド: 公開済みのみ(PublishedScope が自動適用) $posts = Post::latest()->get(); // 管理画面: 全投稿を表示 $posts = Post::withoutGlobalScope(PublishedScope::class)->latest()->get(); ``` ### `select` ではなく `addSelect` を使う グローバルスコープ内でカラムを追加するときは `select` ではなく `addSelect` を使ってください。`select` を使うと、呼び出し元のクエリが `select` しているカラムを上書きしてしまいます。 ```php theme={null} // 悪い例: 呼び出し元の select を上書きする public function apply(Builder $builder, Model $model): void { $builder->select('id', 'tenant_id', 'name'); } // 良い例: 既存の select に追記する public function apply(Builder $builder, Model $model): void { $builder->addSelect('tenant_id'); } ``` ## 次のステップ 属性の変換ロジックをカスタムキャストとして実装し、Value Objectパターンを活用する方法を学びます。 # PHP FFI Source: https://kawax.biz/jp/advanced/ffi PHP FFI の基本、利用できる環境、そして VOICEVOX Core for PHP を題材にした実践的なラップパターンを解説します。 ## はじめに PHP FFI(Foreign Function Interface)は、共有ライブラリを読み込み、C 関数を呼び出し、C のデータ構造へアクセスするための PHP 拡張です。PHP 拡張を自作しなくても、既存の C API を PHP から直接使えます。 PHP 公式ドキュメントは、FFI を低レベルで危険な機能として説明しています。C と対象ライブラリの API を理解した開発者だけが使うべき機能です。 FFI は安全に抽象化された通常の PHP API ではありません。ポインタ、所有権、解放関数を誤るとクラッシュやメモリ破壊につながります。 FFI は「速くするための機能」でもありません。PHP 公式ドキュメントでも、FFI のデータ構造アクセスはネイティブな PHP 配列やオブジェクトより遅いと説明されています。FFI を選ぶ理由は速度ではなく、既存の C ライブラリを PHP から使いたいことです。 ## FFI が使える環境と制限 PHP 公式ドキュメントでは、FFI 拡張を有効にする方法を次のように説明しています。 * PHP を `--with-ffi` 付きでビルドする * Windows では `php.ini` で `php_ffi.dll` を有効にする * `php.ini` の `ffi.enable` で使用可否を制御する `ffi.enable` は次の 3 つの値を取ります。 | 設定値 | 意味 | | - | - | | `true` | FFI API を有効にする | | `false` | FFI API を無効にする | | `preload` | CLI SAPI と preload 済みファイルだけで FFI API を許可する | Web サーバー環境では `ffi.enable=false` や `ffi.enable=preload` が使われることが多いです。Laravel Cloud を含むホスティング環境でも、通常の Web アプリとして FFI を使えるとは限りません。 そのため、Laravel アプリで FFI を使う場合でも、まずは CLI ツールやバッチ処理で成立するかを考えてください。FPM や Apache mod\_php で常時有効にする前提は避けたほうが安全です。 ## 基本的な使い方 PHP FFI の基本は `FFI::cdef()`、`FFI::new()`、`FFI::addr()`、`FFI::string()` の 4 つです。 ```php theme={null} new('struct timeval'); $tz = $ffi->new('struct timezone'); $ffi->gettimeofday(FFI::addr($tv), FFI::addr($tz)); echo $tv->tv_sec.PHP_EOL; ``` この例で押さえる点は次のとおりです。 * `FFI::cdef()` は C の宣言文字列と共有ライブラリ名から FFI オブジェクトを作る * `$ffi->new('struct timeval')` は C のデータ構造を確保する * `FFI::addr($tv)` は `struct timeval *` のようなポインタ引数を作る * `$tv->tv_sec` のように構造体フィールドへアクセスできる 文字列やバイナリを受け取る API では `FFI::string()` を使います。 ```php theme={null} new('char*'); // C 側が $jsonPtr に結果ポインタを書き込む想定 $result = $ffi->some_function(FFI::addr($jsonPtr)); if ($result !== 0) { throw new RuntimeException('C API call failed.'); } $json = FFI::string($jsonPtr); ``` `FFI::new()` で確保したメモリは、通常は PHP の参照カウントで解放されます。一方で、C ライブラリが返したポインタはライブラリ専用の `free` 関数で解放する場合があります。どちらの所有権なのかを API ごとに確認してください。 ## ヘッダーファイルの読み込み `FFI::load()` を使うと、C ヘッダーファイルから宣言を読み込めます。ヘッダーファイルには `FFI_SCOPE` と `FFI_LIB` を書けます。 ```c theme={null} #define FFI_SCOPE "mylib" #define FFI_LIB "/absolute/path/to/libmylib.so" typedef struct MyContext MyContext; MyContext *mylib_new(void); void mylib_delete(MyContext *context); ``` ```php theme={null} mylib_new(); ``` ただし、PHP 公式ドキュメントは `FFI::cdef()` と `FFI::load()` のどちらでも通常の C プリプロセッサ命令は使えないと説明しています。`#include`、`#define`、条件付きコンパイルをそのまま渡すことはできません。 そのため、実運用では次のどちらかを選ぶことが多いです。 依存する型や関数が少ないなら、必要な宣言だけを PHP 側へ埋め込みます。 元のヘッダーからマクロや条件付きコンパイルを取り除いた、FFI 用のヘッダーを別ファイルで管理します。 ## 実際のユースケース: VOICEVOX Core for PHP [VOICEVOX Core for PHP](/jp/packages/voicevox-core-php) は、VOICEVOX CORE の C 動的ライブラリを Pure PHP から使う実例です。FFI の実用パターンがまとまっているので、抽象的な説明より理解しやすい題材です。 ### 1. FFI 用に整形したヘッダーを持つ VOICEVOX Core の元ヘッダーはそのままでは使わず、`headers/voicevox_core_ffi.h` に FFI 向けの宣言を切り出しています。ここでは不透明ポインタ、構造体、free 関数を明示しています。 ```c theme={null} typedef struct VoicevoxSynthesizer VoicevoxSynthesizer; typedef struct VoicevoxInitializeOptions { int32_t acceleration_mode; uint16_t cpu_num_threads; } VoicevoxInitializeOptions; int32_t voicevox_synthesizer_new( const struct VoicevoxOnnxruntime *onnxruntime, const struct OpenJtalkRc *open_jtalk, struct VoicevoxInitializeOptions options, struct VoicevoxSynthesizer **out_synthesizer ); void voicevox_synthesizer_delete(struct VoicevoxSynthesizer *synthesizer); void voicevox_json_free(char *json); void voicevox_wav_free(uint8_t *wav); ``` この設計で重要なのは、C の詳細な内部構造を PHP 側へ見せずに、不透明ハンドルと関数呼び出しに閉じ込めていることです。PHP から扱う面積が小さくなり、ラッパークラスも整理しやすくなります。 ### 2. `FFI::cdef()` を 1 箇所へ集約する `src/VoicevoxFFI.php` は、ヘッダー読み込みとライブラリパス解決を 1 箇所へ集約しています。 ```php theme={null} 'libvoicevox_core.dylib', 'Windows' => 'voicevox_core.dll', default => 'libvoicevox_core.so', }; } } ``` この形にすると、あなたのアプリ側は `FFI::cdef()` を直接呼ばずに済みます。ライブラリパスの差分も、環境変数と OS 判定で吸収できます。 ### 3. out parameter をラップしてオブジェクト化する `src/Synthesizer.php` のコンストラクタは、`struct VoicevoxSynthesizer*` を確保し、`voicevox_synthesizer_new()` にそのアドレスを渡しています。 ```php theme={null} ffi->voicevox_make_default_initialize_options(); $options->acceleration_mode = $accelerationMode->value; $options->cpu_num_threads = $cpuNumThreads; $ptr = $this->ffi->new('struct VoicevoxSynthesizer*'); $result = $this->ffi->voicevox_synthesizer_new( $onnxruntime->handle(), $openJtalk->handle(), $options, FFI::addr($ptr), ); $this->handle = $ptr; ``` このパターンは、C API の `Type **out_value` を PHP で受ける基本形です。 * `new('struct VoicevoxSynthesizer*')` でポインタ変数を作る * `FFI::addr($ptr)` で `Type **` を渡す * 取得したハンドルを PHP オブジェクトのプロパティに保持する ### 4. C が返したメモリは PHP 文字列へコピーしてすぐ解放する VOICEVOX Core for PHP は、JSON 文字列や WAV バイナリを受け取ったら、まず `FFI::string()` で PHP 文字列へコピーし、その後で C 側の free 関数を呼んでいます。 ```php theme={null} ffi->voicevox_synthesizer_create_metas_json($this->handle); $json = FFI::string($jsonPtr); $this->ffi->voicevox_json_free($jsonPtr); return $json; ``` ```php theme={null} ffi->new('uint64_t'); $wavPtr = $this->ffi->new('uint8_t*'); $result = $this->ffi->voicevox_synthesizer_tts( $this->handle, $text, $styleId, $options, FFI::addr($wavSize), FFI::addr($wavPtr), ); $wav = FFI::string($wavPtr, (int) $wavSize->cdata); $this->ffi->voicevox_wav_free($wavPtr); return $wav; ``` この「コピーして即解放」の流れは、FFI で最も重要な実装パターンの 1 つです。C 側のバッファを PHP のライフサイクルへ混ぜないことで、所有権を単純化できます。 ## 注意点とベストプラクティス | 観点 | 実践ポイント | | - | - | | 実行環境 | まず CLI 前提で設計する | | ヘッダー管理 | 元ヘッダーをそのまま渡さず、FFI 用に整形した宣言を別管理する | | ライブラリパス | 絶対パスや環境変数で切り替えられるようにする | | メモリ管理 | `FFI::string()` で PHP にコピーし、ライブラリ専用の解放関数を必ず呼ぶ | | ラッパー設計 | 生の `CData` をアプリ全体へ広げず、専用クラスへ閉じ込める | | 互換性 | PHP バージョンと対象ライブラリの ABI 変更を一緒に確認する | Laravel アプリの request/response サイクルに FFI を直接混ぜると、環境差分と障害切り分けが急に難しくなります。まずは CLI コマンドやキュー以外の単発プロセスで成立させてください。 FFI が向いているのは、すでに安定した C API があり、PHP 拡張を自作するほどではないケースです。逆に、通常の Web ホスティングで動かしたい機能や、PHP だけで完結できる処理には向いていません。 ## まとめ PHP FFI を使うと、あなたは既存の C ライブラリを Pure PHP から呼び出せます。ただし、FFI は低レベルで危険な仕組みです。Web サーバーで常に使えるとも限りません。 VOICEVOX Core for PHP の実装を見ると、実践では次の 4 点が重要だと分かります。 1. FFI 用のヘッダーを別に用意する 2. `FFI::cdef()` とライブラリパス解決を 1 箇所へ集約する 3. 不透明ポインタを専用クラスでラップする 4. C が返したメモリは PHP にコピーしてから専用関数で解放する VOICEVOX Core for PHP のページも合わせて読むと、FFI の概念と実際のパッケージ設計を往復しながら理解できます。 # Fluentクラス Source: https://kawax.biz/jp/advanced/fluent Illuminate\Support\Fluent クラスを使って、配列データをオブジェクトのように扱う方法を解説します。Laravel初期からある隠れた便利クラスです。 ## Fluentクラスとは `Fluent` クラスは、配列をオブジェクトのように扱える汎用ユーティリティクラスです。`Illuminate\Support\Fluent` に実装されており、Laravel 初期バージョンから存在していながら、公式ドキュメントにはほとんど記載されていません。 内部的には配列をプロパティで管理し、マジックメソッド(`__get` / `__set` / `__call`)を通じてプロパティのような読み書きを実現しています。 Laravel 11 で `fluent()` ヘルパーが追加され、Fluent クラスも機能強化されたため、今こそ活用すべき優れたクラスです。 ## インスタンス作成 ### コンストラクタ ```php theme={null} use Illuminate\Support\Fluent; // 配列で初期化 $user = new Fluent(['name' => 'Laravel', 'type' => 'Framework']); echo $user->name; // 'Laravel' echo $user->type; // 'Framework' ``` ### make() ファクトリメソッド ```php theme={null} use Illuminate\Support\Fluent; $config = Fluent::make([ 'host' => 'localhost', 'port' => 3306, 'database' => 'laravel' ]); echo $config->host; // 'localhost' ``` ### fluent() ヘルパー関数 Laravel 11 では `fluent()` ヘルパーが追加されました。`Fluent::make()` と同等です。 ```php theme={null} $request = fluent([ 'method' => 'POST', 'path' => '/api/users', 'status' => 201 ]); echo $request->method; // 'POST' ``` ## プロパティへのアクセス ### 動的プロパティの読み書き ```php theme={null} $fluent = new Fluent(); // 書き込み $fluent->name = 'Laravel'; $fluent->version = 13; // 読み込み echo $fluent->name; // 'Laravel' echo $fluent->version; // 13 ``` ### メソッドチェーン `__call` マジックメソッドによって、存在しないメソッドを呼び出すとプロパティが設定されます。メソッドは `$this` を返すため、チェーンできます。 ```php theme={null} $config = new Fluent(); $config ->host('localhost') ->port(3306) ->database('laravel') ->username('root') ->password('secret'); echo $config->host; // 'localhost' echo $config->password; // 'secret' ``` この仕組みにより、配列の代わりに流暢なAPI(Fluent Interface)で値を設定できます。 ## 主要なメソッド ### get() — ドット記法でアクセス ```php theme={null} $user = new Fluent([ 'profile' => [ 'email' => 'user@example.com', 'phone' => '090-xxxx-xxxx' ] ]); // ドット記法でネストされた値を取得 $email = $user->get('profile.email'); // 'user@example.com' $phone = $user->get('profile.phone'); // '090-xxxx-xxxx' // デフォルト値を指定可能 $fax = $user->get('profile.fax', 'N/A'); // 'N/A' ``` ### set() — ドット記法でセット ```php theme={null} $fluent = new Fluent(); $fluent->set('user.name', 'Laravel'); $fluent->set('user.email', 'laravel@example.com'); print_r($fluent->toArray()); // Array ( // [user] => Array ( // [name] => Laravel // [email] => laravel@example.com // ) // ) ``` ### fill() — 複数プロパティを一度に設定 ```php theme={null} $fluent = new Fluent(['initial' => 'value']); $fluent->fill([ 'name' => 'Laravel', 'version' => 13, 'license' => 'MIT' ]); echo $fluent->name; // 'Laravel' echo $fluent->version; // 13 ``` ### all() — すべてのプロパティを配列で取得 ```php theme={null} $fluent = fluent([ 'name' => 'Laravel', 'version' => 13, 'license' => 'MIT' ]); // すべてのプロパティ $all = $fluent->all(); // ['name' => 'Laravel', 'version' => 13, 'license' => 'MIT'] // 特定のプロパティのみ $subset = $fluent->all(['name', 'license']); // ['name' => 'Laravel', 'license' => 'MIT'] ``` ### scope() — ネストされた値を新しい Fluent に変換 ```php theme={null} $config = fluent([ 'database' => [ 'host' => 'localhost', 'port' => 3306, 'name' => 'laravel' ] ]); $dbConfig = $config->scope('database'); // $dbConfig は新しい Fluent インスタンス echo $dbConfig->host; // 'localhost' echo $dbConfig->port; // 3306 ``` これにより、ネストされた配列を別の Fluent オブジェクトとして扱えます。 ### value() — デフォルト値をコールバックで動的に指定 ```php theme={null} $user = fluent(['role' => 'admin']); // キーが存在する場合は値を返す $role = $user->value('role'); // 'admin' // キーが存在しない場合はデフォルト値を返す $status = $user->value('status', 'active'); // 'active' // デフォルト値をコールバックで指定 $timestamp = $user->value('updated_at', function () { return now()->toIso8601String(); }); ``` ## 配列操作 ### toArray() — 配列に変換 ```php theme={null} $fluent = fluent(['name' => 'Laravel', 'version' => 13]); $array = $fluent->toArray(); // ['name' => 'Laravel', 'version' => 13] print_r($array); ``` ### getAttributes() — 内部属性を直接アクセス ```php theme={null} $fluent = fluent(['a' => 1, 'b' => 2]); $attributes = $fluent->getAttributes(); // ['a' => 1, 'b' => 2] ``` ### ArrayAccess インターフェース Fluent は `ArrayAccess` を実装しているため、配列のように操作できます。 ```php theme={null} $config = new Fluent(); // 配列のようにセット $config['host'] = 'localhost'; $config['port'] = 3306; // 配列のように読み込み echo $config['host']; // 'localhost' // 存在確認 if (isset($config['port'])) { echo $config['port']; } // 削除 unset($config['port']); ``` ### IteratorAggregate インターフェース Fluent は foreach でループできます。 ```php theme={null} $settings = fluent([ 'debug' => true, 'cache' => 'redis', 'queue' => 'database' ]); foreach ($settings as $key => $value) { echo "$key: $value\n"; // debug: 1 // cache: redis // queue: database } ``` ## JSON 処理 ### toJson() — JSON 文字列に変換 ```php theme={null} $response = fluent([ 'success' => true, 'data' => ['id' => 1, 'name' => 'User'] ]); $json = $response->toJson(); // {"success":true,"data":{"id":1,"name":"User"}} // APIレスポンスに使用可能 return $json; ``` ### toPrettyJson() — 整形した JSON に変換 ```php theme={null} $data = fluent([ 'users' => [ ['id' => 1, 'name' => 'Alice'], ['id' => 2, 'name' => 'Bob'] ] ]); echo $data->toPrettyJson(); // { // "users": [ // { // "id": 1, // "name": "Alice" // }, // { // "id": 2, // "name": "Bob" // } // ] // } ``` ### JsonSerializable インターフェース Fluent は `JsonSerializable` を実装しているため、`json_encode()` で直接変換できます。 ```php theme={null} $fluent = fluent(['status' => 'ok', 'code' => 200]); $json = json_encode($fluent); // {"status":"ok","code":200} $data = json_decode($json, true); // ['status' => 'ok', 'code' => 200] ``` ## 状態確認 ### isEmpty() / isNotEmpty() ```php theme={null} $empty = new Fluent(); $filled = fluent(['value' => 1]); $empty->isEmpty(); // true $empty->isNotEmpty(); // false $filled->isEmpty(); // false $filled->isNotEmpty(); // true ``` ## Conditionable トレイト Fluent は `Conditionable` トレイトを使用しており、条件付き処理をサポートします。 ```php theme={null} $config = fluent(['env' => 'production']); $config ->when($config->env === 'production', function ($fluent) { $fluent->debug = false; $fluent->cache = 'redis'; }) ->when($config->env === 'local', function ($fluent) { $fluent->debug = true; $fluent->cache = 'array'; }); echo $config->debug; echo $config->cache; ``` `when()` / `unless()` メソッドを使うことで、条件に基づいた設定を流暢に記述できます。 ## Macroable トレイト Fluent は `Macroable` トレイトも使用しており、動的にメソッドを追加できます。 ```php theme={null} use Illuminate\Support\Fluent; // プロバイダーの boot() メソッドで定義 Fluent::macro('isProduction', function () { /** @var Fluent $this */ return $this->env === 'production'; }); Fluent::macro('isDevelopment', function () { /** @var Fluent $this */ return $this->env === 'development'; }); // 使用 $config = fluent(['env' => 'production']); if ($config->isProduction()) { // 本番環境の処理 } ``` ## 実践的なユースケース ### API レスポンスビルダー ```php theme={null} namespace App\Support; use Illuminate\Support\Fluent; class ApiResponse { public static function success($data = null, string $message = 'Success'): string { return fluent([ 'success' => true, 'message' => $message, 'data' => $data, 'timestamp' => now()->toIso8601String() ])->toJson(); } public static function error(string $message, int $code = 400): string { return fluent([ 'success' => false, 'message' => $message, 'code' => $code, 'timestamp' => now()->toIso8601String() ])->toJson(); } } // コントローラーで使用 public function store(Request $request) { $user = User::create($request->validated()); return response()->json( json_decode(ApiResponse::success(['id' => $user->id])) ); } ``` ### コンフィグビルダー ```php theme={null} $dbConfig = fluent() ->host(env('DB_HOST', 'localhost')) ->port(env('DB_PORT', 3306)) ->database(env('DB_DATABASE', 'laravel')) ->username(env('DB_USERNAME', 'root')) ->password(env('DB_PASSWORD', '')) ->charset('utf8mb4') ->collation('utf8mb4_unicode_ci') ->when(env('APP_ENV') === 'production', function ($config) { $config->sslmode('require'); $config->sslcert(env('DB_SSL_CERT')); }); // 設定値を検証して使用 config(['database.connections.mysql' => $dbConfig->toArray()]); ``` ### リクエストパラメーターの検証・変換 ```php theme={null} namespace App\Services; use Illuminate\Support\Fluent; class SearchFilter { public function apply(array $params): Fluent { $filter = fluent() ->page($params['page'] ?? 1) ->perPage($params['per_page'] ?? 15) ->sort($params['sort'] ?? 'created_at') ->order($params['order'] ?? 'desc') ->when(isset($params['search']), function ($f) use ($params) { $f->search = $params['search']; }) ->when(isset($params['status']), function ($f) use ($params) { $f->status = $params['status']; }); // ページネーション計算 $filter->offset = ($filter->page - 1) * $filter->perPage; return $filter; } } // 使用 $filter = app(SearchFilter::class)->apply(request()->all()); $users = User::query() ->when($filter->has('search'), fn ($q) => $q->search($filter->search)) ->when($filter->has('status'), fn ($q) => $q->where('status', $filter->status)) ->orderBy($filter->sort, $filter->order) ->offset($filter->offset) ->limit($filter->perPage) ->get(); ``` ### モデルと Fluent の組み合わせ ```php theme={null} namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Fluent; class Post extends Model { protected $casts = [ 'metadata' => 'json' ]; public function getMetadataAttribute($value): Fluent { return new Fluent($value ?? []); } public function setMetadataAttribute($value): void { if ($value instanceof Fluent) { $this->attributes['metadata'] = $value->toJson(); } else { $this->attributes['metadata'] = json_encode($value); } } } // 使用 $post = new Post(); $post->metadata = fluent() ->title('SEO Title') ->description('Meta Description') ->keywords(['laravel', 'fluent', 'tutorial']) ->author('Laravel Community'); $post->save(); // 読み込み $post = Post::first(); echo $post->metadata->title; // 'SEO Title' echo $post->metadata->author; // 'Laravel Community' ``` ### フォームデータの正規化 ```php theme={null} namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Support\Fluent; class CreateUserRequest extends FormRequest { public function rules(): array { return [ 'name' => 'required|string', 'email' => 'required|email|unique:users', 'password' => 'required|min:8|confirmed', 'role' => 'in:user,admin', 'preferences' => 'json', ]; } // バリデーション済みデータを Fluent として返す public function toFluent(): Fluent { $preferences = is_string($this->preferences) ? json_decode($this->preferences, true) : $this->preferences; return fluent([ 'name' => $this->name, 'email' => $this->email, 'password' => bcrypt($this->password), 'role' => $this->role ?? 'user', 'preferences' => new Fluent($preferences ?? []) ]); } } // コントローラーで使用 public function store(CreateUserRequest $request) { $data = $request->toFluent(); $user = User::create($data->all()); return response()->json(['success' => true, 'user_id' => $user->id]); } ``` ## 他のクラスとの比較 ### Fluent vs Array | 機能 | Fluent | Array | | - | - | - | | プロパティアクセス | `$fluent->name` | `$array['name']` | | メソッドチェーン | ✓ サポート | ✗ なし | | JSON 変換 | `toJson()` メソッド | `json_encode()` 関数 | | ドット記法 | ✓ `get('user.name')` | ✗ 手動で処理 | | 状態確認 | `isEmpty()` | `empty()` 関数 | | 動的メソッド追加 | Macroable | ✗ 不可 | ### Fluent vs Model | 機能 | Fluent | Model | | - | - | - | | DB 永続化 | ✗ なし | ✓ 自動 | | メモリ効率 | ✓ 軽量 | ✗ 重い | | リレーション | ✗ なし | ✓ サポート | | キャスト | ✗ なし | ✓ サポート | | バリデーション | ✗ なし | ✓ サポート | | 流暢なAPI | ✓ あり | △ 限定的 | ## 内部実装の詳細 ```php theme={null} class Fluent { protected $attributes = []; // マジックメソッド:存在しないプロパティアクセス public function __get($key) { return $this->value($key); } // マジックメソッド:存在しないプロパティセット public function __set($key, $value) { $this->offsetSet($key, $value); } // マジックメソッド:存在しないメソッド呼び出し // メソッド名がそのままプロパティキーになる public function __call($method, $parameters) { $this->attributes[$method] = count($parameters) > 0 ? $parameters[0] : true; return $this; } } ``` `__call` メソッドは、マクロが登録されていない場合、メソッド名をプロパティキーとして属性に値を設定し、`$this` を返します。これがメソッドチェーンを可能にしています。 Fluent は以下のトレイトを使用しており、それぞれ異なる機能を提供しています: * **Conditionable** — `when()` / `unless()` で条件付き処理 * **InteractsWithData** — `data()` などのデータ操作メソッド * **Macroable** — 動的メソッド追加 ## 次のステップ 複数要素を扱う Collection クラスを深掘りします。 条件付き処理を流暢に記述する Conditionable トレイトを学びます。 既存クラスに動的メソッドを追加する Macroable トレイトを学びます。 # Laravel Fortify とスターターキット Source: https://kawax.biz/jp/advanced/fortify フロントエンド非依存の認証バックエンド Laravel Fortify の内部設計を解説します。現行スターターキットでなぜ Fortify が使われているか、FortifyServiceProvider の設定、2FA・パスキーの有効化まで扱います。 ## Fortify とは [Laravel Fortify](https://github.com/laravel/fortify) は、フロントエンドに依存しない認証バックエンド実装です。ログイン・登録・パスワードリセット・メール確認・2FA・パスキーに必要なルートとコントローラーをすべて提供します。 Fortify は UI を持ちません。スターターキットや独自の UI から Fortify のルートにリクエストを送ることで機能します。 「フロントエンド非依存」という設計思想が重要です。Blade・Inertia(React/Vue/Svelte)・API など、どのフロントエンドスタックでも同じ認証バックエンドを再利用できます。これが Fortify が長く使われ続けている理由です。 ## 歴史:Jetstream から現行スターターキットまで ```mermaid theme={null} timeline title Fortify の変遷 2020 Laravel 8 : Jetstream と同時登場 : Fortify は Jetstream の裏方として自動インストール 2020 Laravel 8 : Breeze 登場(Fortify なし) : シンプルな認証スキャフォールドとして独立 2025 スターターキット刷新 : React/Vue スターターキットに 2FA 対応が必要に : Fortify が再採用 → 認証全体が Fortify ベースに 2026 パスキー対応 : Fortify に Passkeys 機能が追加 ``` ### なぜ現行スターターキットでも Fortify が使われているのか 当初の React/Vue スターターキットは Fortify なしで実装されていました。しかし **二要素認証(2FA)への対応が必要になった際、Fortify がそのまま採用された**ことで、認証全体が Fortify ベースに切り替わりました。 Fortify は「フロントエンド非依存」として設計されているため、Inertia を使ったスターターキットとの相性が良く、現在も使われ続けています。 Fortify は現在、最も長く使われ続けている Laravel 公式認証パッケージです。2020年の登場以来、Jetstream → 現行スターターキットと形を変えながら現役を続けています。 ## 現行スターターキットの構成 React/Vue スターターキットでは以下のファイルで Fortify が設定されています。 * `app/Providers/FortifyServiceProvider.php` — ビュー・アクション・レート制限の登録 * `config/fortify.php` — 有効にする機能と認証設定 ### `config/fortify.php` の主要設定 ```php theme={null} use Laravel\Fortify\Features; return [ 'guard' => 'web', 'username' => 'email', 'home' => '/dashboard', 'limiters' => [ 'login' => 'login', 'two-factor' => 'two-factor', ], 'features' => [ Features::registration(), Features::resetPasswords(), Features::emailVerification(), Features::twoFactorAuthentication([ 'confirm' => true, 'confirmPassword' => true, ]), ], ]; ``` スターターキットではパスキー(`Features::passkeys()`)はデフォルトで有効になっていません。有効化するには `features` 配列に追加します。 ## `FortifyServiceProvider` の設定 スターターキットの `FortifyServiceProvider` は 3 つの役割を担います。 ### 1. アクションの設定 ```php theme={null} private function configureActions(): void { Fortify::resetUserPasswordsUsing(ResetUserPassword::class); Fortify::createUsersUsing(CreateNewUser::class); } ``` `app/Actions/Fortify/` に配置されたクラスでユーザー作成とパスワードリセットのロジックをカスタマイズします。バリデーションやハッシュ処理はここに集約されています。 ### 2. ビューの設定 ```php theme={null} private function configureViews(): void { Fortify::loginView(fn (Request $request) => Inertia::render('auth/login', [ 'canResetPassword' => Features::enabled(Features::resetPasswords()), 'canRegister' => Features::enabled(Features::registration()), 'status' => $request->session()->get('status'), ])); Fortify::resetPasswordView(fn (Request $request) => Inertia::render('auth/reset-password', [ 'email' => $request->email, 'token' => $request->route('token'), ])); Fortify::requestPasswordResetLinkView( fn (Request $request) => Inertia::render('auth/forgot-password', [ 'status' => $request->session()->get('status'), ]) ); Fortify::verifyEmailView( fn (Request $request) => Inertia::render('auth/verify-email', [ 'status' => $request->session()->get('status'), ]) ); Fortify::registerView(fn () => Inertia::render('auth/register')); Fortify::twoFactorChallengeView(fn () => Inertia::render('auth/two-factor-challenge')); Fortify::confirmPasswordView(fn () => Inertia::render('auth/confirm-password')); } ``` 各ビューメソッドは Inertia コンポーネントを返すクロージャを受け取ります。`Features::enabled()` で機能フラグを確認してビューに渡しているのがポイントです。 Blade アプリでは `Inertia::render(...)` の代わりに `view('auth.login')` を返します。フロントエンドを変えても `FortifyServiceProvider` 側だけ修正すれば認証ロジックは変わりません。 ### 3. レート制限の設定 ```php theme={null} private function configureRateLimiting(): void { RateLimiter::for('two-factor', function (Request $request) { return Limit::perMinute(5)->by($request->session()->get('login.id')); }); RateLimiter::for('login', function (Request $request) { $throttleKey = Str::transliterate( Str::lower($request->input(Fortify::username())) . '|' . $request->ip() ); return Limit::perMinute(5)->by($throttleKey); }); } ``` * `login` リミッター: メールアドレスと IP アドレスの組み合わせで制限(ブルートフォース対策) * `two-factor` リミッター: セッション内のログイン試行 ID で制限 `config/fortify.php` の `limiters` キーと一致するキーを `RateLimiter::for()` に登録します。 ## 主な機能と登録されるルート Fortify が登録する主なルートを機能別に整理します。 | 機能 | メソッド | ルート | | - | - | - | | ログイン | `GET` | `/login` | | ログイン | `POST` | `/login` | | ログアウト | `POST` | `/logout` | | 登録 | `GET` | `/register` | | 登録 | `POST` | `/register` | | パスワードリセット要求 | `GET` / `POST` | `/forgot-password` | | パスワードリセット | `GET` / `POST` | `/reset-password` | | メール確認 | `GET` | `/email/verify` | | メール確認リンク再送 | `POST` | `/email/verification-notification` | | パスワード確認 | `GET` / `POST` | `/user/confirm-password` | | 2FA 有効化 | `POST` | `/user/two-factor-authentication` | | 2FA チャレンジ | `GET` / `POST` | `/two-factor-challenge` | | パスキーログイン | `GET` / `POST` | `/passkeys/login` | | パスキー管理 | `GET` / `POST` / `DELETE` | `/user/passkeys` | `php artisan route:list --name=fortify` または単純に `php artisan route:list` で実際に登録されているルートを確認できます。 ## 二要素認証(2FA) スターターキットでは `Features::twoFactorAuthentication()` が有効化されています。`confirm` と `confirmPassword` はどちらも `true` に設定されています。 | オプション | 意味 | | - | - | | `confirm` | 有効化後に認証コードでの確認を要求する | | `confirmPassword` | 2FA の設定変更前にパスワード確認を要求する | 2FA の設定画面から、ユーザーは以下の操作を行えます。 `/user/two-factor-authentication` に POST リクエストを送信します。成功するとセッションに `two-factor-authentication-enabled` ステータスがセットされます。 `/user/two-factor-qr-code` に GET リクエストを送ると、認証アプリに読み込む QR コードの SVG が返されます。 `/user/confirmed-two-factor-authentication` に POST リクエストでコードを送信し、設定を完了します。 `/user/two-factor-recovery-codes` に GET リクエストを送り、リカバリーコードを取得して安全な場所に保存します。 ## パスキー パスキーは Fortify の機能として最近追加されました。WebAuthn を使ったパスワードレス認証で、Face ID・Touch ID・Windows Hello・ハードウェアセキュリティキーに対応しています。 ### 有効化 `config/fortify.php` の `features` 配列に追加します。 ```php theme={null} 'features' => [ Features::registration(), Features::resetPasswords(), Features::emailVerification(), Features::twoFactorAuthentication([ 'confirm' => true, 'confirmPassword' => true, ]), Features::passkeys([ 'confirmPassword' => true, ]), ], ``` 次に、`User` モデルに `PasskeyUser` コントラクトと `PasskeyAuthenticatable` トレイトを追加します。 ```php theme={null} use Illuminate\Foundation\Auth\User as Authenticatable; use Laravel\Fortify\Contracts\PasskeyUser; use Laravel\Fortify\PasskeyAuthenticatable; class User extends Authenticatable implements PasskeyUser { use PasskeyAuthenticatable; } ``` WebAuthn 設定は `config/fortify.php` の `passkeys` キーで管理します。 ```php theme={null} 'passkeys' => [ 'relying_party_id' => parse_url(config('app.url'), PHP_URL_HOST), 'allowed_origins' => [config('app.url')], 'user_handle_secret' => config('app.key'), 'timeout' => 60000, ], ``` Fortify のパスキーは内部で `laravel/passkeys` Composer パッケージをラップしています。`laravel/passkeys` の設定ファイルを公開する必要はなく、`config/fortify.php` の `passkeys` キーが優先されます。 パスキーの詳細な実装(`@laravel/passkeys` JS クライアント、React/Vue/Svelte ヘルパーなど)については、[パスキー初期調査](/jp/blog/passkeys-introduction)も参照してください。 ## 関連ページ Guard・StatefulGuard インターフェースを実装してカスタム認証ガードを作る方法を解説します。 インストール・全機能・カスタマイズの詳細は公式ドキュメントを参照してください。 # ForwardsCallsトレイト Source: https://kawax.biz/jp/advanced/forwards-calls Illuminate\Support\Traits\ForwardsCalls を使って、プロキシ/デコレータークラスのメソッド転送を安全に実装する方法を解説します。 ## ForwardsCallsトレイトとは `Illuminate\Support\Traits\ForwardsCalls` は、あるオブジェクトへのメソッド委譲を共通化するトレイトです。Laravel本体では Eloquent や Mail などの「ラッパーオブジェクト」で使われています。 実装は `src/Illuminate/Support/Traits/ForwardsCalls.php` にあります。存在しないメソッドを転送したときは、呼び出し元クラス名を含む `BadMethodCallException` に整形して再スローします。 ```mermaid theme={null} flowchart LR A["あなたの Proxy クラス"] -->|__call| B["forwardCallTo()"] B --> C["内部の Driver / Builder"] C --> D["結果を返す"] D --> A ``` ## コアAPI ### `forwardCallTo($object, $method, $parameters)` 指定オブジェクトへメソッドをそのまま転送します。典型的には `__call()` から呼びます。 ### `forwardDecoratedCallTo($object, $method, $parameters)` Builder や Decorator でチェーンを維持したいときに使います。転送先メソッドの戻り値が「転送先オブジェクト自身」なら、「呼び出し元オブジェクト (`$this`)」に差し替えて返します。 ### `__call()` / `__callStatic()` との組み合わせ `ForwardsCalls` はマジックメソッドを提供しません。あなたのクラス側で `__call()`(必要なら `__callStatic()`)を実装し、その中で `forwardCallTo` 系メソッドを呼びます。 ## 実際の使用例(Laravel内部) `Illuminate\Support\Facades\Facade` は `__callStatic()` で root instance へ直接委譲します。Facade は static 呼び出しなので、`ForwardsCalls` ではなく直接転送する実装になっています。 `Illuminate\Database\Eloquent\Builder::__call()` は、未解決メソッドを内部 Query Builder へ `forwardCallTo($this->query, ...)` で渡し、最後に `$this` を返して fluent chain を維持します。 `Illuminate\Database\Eloquent\Relations\Relation`、`Illuminate\Mail\Message`、`Illuminate\Events\NullDispatcher` では `forwardDecoratedCallTo` が使われ、内部オブジェクトへ委譲しつつ外側APIのチェーンを保ちます。 ## 基本的なプロキシ実装(`forwardCallTo`) ```php theme={null} use Illuminate\Support\Traits\ForwardsCalls; class CourierProxy { use ForwardsCalls; public function __construct( protected CourierDriver $driver ) {} public function __call(string $method, array $parameters): mixed { return $this->forwardCallTo($this->driver, $method, $parameters); } } ``` この形なら、`CourierProxy` は `CourierDriver` の public API を透過的に公開できます。 ## メソッドチェーン対応プロキシ(`forwardDecoratedCallTo`) ```php theme={null} use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Traits\ForwardsCalls; class QueryProxy { use ForwardsCalls; public function __construct( protected Builder $query ) {} public function __call(string $method, array $parameters): static { $this->forwardDecoratedCallTo($this->query, $method, $parameters); return $this; } public function get(): \Illuminate\Support\Collection { return $this->query->get(); } } ``` ```php theme={null} $users = (new QueryProxy(User::query())) ->where('active', true) ->orderByDesc('created_at') ->limit(10) ->get(); ``` 手動で `return $this` を書くより、`forwardDecoratedCallTo` の「戻り値が転送先自身なら `$this` に置き換える」ルールを使うほうが安全です。 ## BadMethodCallException の自動スロー 存在しないメソッドを呼ぶと、`ForwardsCalls` は呼び出し元クラス名を使って `BadMethodCallException` を投げ直します。 ```php theme={null} try { (new CourierProxy($driver))->missingMethod(); } catch (\BadMethodCallException $e) { // 例: Call to undefined method App\Services\CourierProxy::missingMethod() report($e); } ``` これにより、利用者は「どの外側APIで失敗したか」をすぐ特定できます。 ## Macroableとの比較 | 観点 | `Macroable` | `ForwardsCalls` | | - | - | - | | 目的 | 既存クラスに新メソッドを追加 | 別オブジェクトへメソッド委譲 | | 主な入口 | `macro()`, `mixin()` | `forwardCallTo()`, `forwardDecoratedCallTo()` | | 適したケース | API拡張 | Proxy / Decorator / Adapter | 「メソッドを増やしたい」のに `ForwardsCalls` を使うと、委譲先が持たないメソッドは常に失敗します。API拡張が目的なら `Macroable` を選んでください。 ## パッケージ開発での活用例 ### 1) Driver を切り替える Manager ```php theme={null} use Illuminate\Support\Traits\ForwardsCalls; class SmsManager { use ForwardsCalls; public function __construct( protected SmsDriver $driver ) {} public function via(string $name): static { $this->driver = app(SmsDriverFactory::class)->make($name); return $this; } public function __call(string $method, array $parameters): mixed { return $this->forwardCallTo($this->driver, $method, $parameters); } } ``` ### 2) 複数バックエンドを持つ Adapter HTTP / Queue / WebSocket など異なるバックエンドを、1つの API に統一して委譲できます。 ### 3) テスト用 Spy / Stub 実ドライバーの代わりに Spy を注入し、呼び出しは `forwardCallTo` でそのまま流して呼び出し回数や引数を検証できます。 ## 関連ページ 既存クラスに新しいメソッドを追加する拡張パターンを学びます。 `when()` / `unless()` による条件分岐チェーンの設計を学びます。 # GeneratorCommand — カスタム make: コマンドの実装 Source: https://kawax.biz/jp/advanced/generator-command Illuminate\Console\GeneratorCommand を継承して、パッケージ独自の make: コマンドを作成する方法を解説します。stub ファイルの作成から名前空間の制御まで実践的に紹介します。 ## GeneratorCommand とは `Illuminate\Console\GeneratorCommand` は、Laravel のすべてのコード生成コマンドの基底となる抽象クラスです。`make:model`、`make:controller`、`make:request` などはすべてこのクラスを継承しています。 ```mermaid theme={null} flowchart TD A["GeneratorCommand
    (抽象クラス)"] --> B["make:model
    ModelMakeCommand"] A --> C["make:request
    RequestMakeCommand"] A --> D["make:controller
    ControllerMakeCommand"] A --> E["make:handler
    あなたのコマンド"] ``` パッケージで独自の `make:xxx` コマンドを提供すると、ユーザーはクラスを手動で作成する代わりに `php artisan make:handler OrderHandler` のように実行するだけで正しい名前空間のファイルが生成されます。 `GeneratorCommand` は公式ドキュメントにほとんど記載がなく、フレームワークのソースコードを直接読まないと分かりません。典型的な上級トピックです。 ## 最小限の実装 `GeneratorCommand` を継承したクラスに必要な実装は `getStub()` メソッドのみです。それ以外のプロパティはオプションですが、実際には以下を揃えます。 | メンバー | 種類 | 役割 | | - | - | - | | `$name` | プロパティ | コマンド名(例: `make:handler`) | | `$description` | プロパティ | コマンドの説明文 | | `$type` | プロパティ | 生成物の種類名。成功メッセージに使われる(例: `Handler`) | | `getStub()` | メソッド(必須) | stub ファイルのパスを返す | | `getDefaultNamespace()` | メソッド | 生成先のデフォルト名前空間を制御する | `make:handler` コマンドを実装する例を示します。 ```php theme={null} stub ファイルの例: ```php handler.stub theme={null} app->runningInConsole()) { $this->publishes([ __DIR__.'/../Console/Commands/stubs' => base_path('stubs'), ], 'stubs'); } } ``` 次に `getStub()` でユーザーがカスタマイズした stub を優先的に使います。 ```php theme={null} protected function getStub(): string { return $this->resolveStubPath('/stubs/handler.stub'); } protected function resolveStubPath(string $stub): string { return file_exists($customPath = $this->laravel->basePath(trim($stub, '/'))) ? $customPath : __DIR__.$stub; } ``` `php artisan vendor:publish --tag=stubs` を実行したユーザーはプロジェクトルートの `stubs/handler.stub` を編集してテンプレートを変更できます。 `resolveStubPath()` は Laravel 組み込みの `RequestMakeCommand` でも使われているパターンです。パッケージを公開する場合はこのパターンを採用してください。 ## サービスプロバイダーへの登録 コマンドはサービスプロバイダーの `boot()` メソッドで登録します。`runningInConsole()` で確認することで、Web リクエスト時の無駄なロードを防げます。 ```php theme={null} app->runningInConsole()) { $this->commands([ HandlerMakeCommand::class, ]); $this->publishes([ __DIR__.'/../Console/Commands/stubs' => base_path('stubs'), ], 'stubs'); } } } ``` パッケージの `composer.json` に `extra.laravel` を設定しておくと、ユーザーはサービスプロバイダーを手動登録しなくて済みます。 ```json theme={null} "extra": { "laravel": { "providers": [ "Vendor\\Package\\PackageServiceProvider" ] } } ``` ## 活用例 `GeneratorCommand` が活躍するユースケースをいくつか示します。 認証チェックやカスタムバリデーションロジックを含む独自の基底クラスを継承した Request を生成するコマンド。`getDefaultNamespace()` で `App\Http\Requests` を返します。 Data Transfer Object のボイラープレートを生成するコマンド。readonly プロパティや `from()` ファクトリメソッドを含む stub を用意します。 単一責任の Action クラスを生成するコマンド。`App\Actions` 名前空間に `execute()` メソッドを持つクラスを生成します。 `make:livewire` コマンドは `GeneratorCommand` を継承した `MakeCommand` クラスで実装されています。コンポーネントクラスと Blade ビューの 2 ファイルを同時生成するために `handle()` をオーバーライドしています。 ## テスト Orchestra Testbench を使って、コマンドが正しくファイルを生成することをテストします。 ```php theme={null} artisan('make:handler', ['name' => 'OrderHandler']) ->assertSuccessful(); $this->assertFileExists($path); $this->assertStringContainsString('namespace App\Handlers;', File::get($path)); $this->assertStringContainsString('class OrderHandler', File::get($path)); } public function test_make_handler_does_not_overwrite_existing_file(): void { $path = app_path('Handlers/OrderHandler.php'); File::ensureDirectoryExists(dirname($path)); File::put($path, 'artisan('make:handler', ['name' => 'OrderHandler']) ->assertFailed(); } } ``` Generator コマンドは実際のファイルシステムに書き込みます。`tearDown()` で必ずクリーンアップしてください。テストが途中で失敗した場合でも確実に実行されます。 ## 関連ページ サービスプロバイダーを使ったパッケージ開発の基礎を確認します。 パッケージテストの基盤の作り方を確認します。 Source: [Illuminate\Console\GeneratorCommand](https://github.com/laravel/framework/blob/master/src/Illuminate/Console/GeneratorCommand.php), [Illuminate\Foundation\Console\RequestMakeCommand](https://github.com/laravel/framework/blob/master/src/Illuminate/Foundation/Console/RequestMakeCommand.php) # GitHub Actionsのピン留めとセキュリティ Source: https://kawax.biz/jp/advanced/github-actions-pinning パッケージのサプライチェーン攻撃対策として、GitHub Actionsの依存関係をバージョンからSHAハッシュにピン留めする方法を、Dependabotの自動更新戦略を含めて解説します。 GitHubのアクションに対する攻撃や改ざんのリスクに対応するため、公式Laravelプロジェクトでも採用しているセキュリティ対策がGitHub Actionsのピン留め(pinning)です。このページでは、パッケージ開発者が実装すべきセキュリティ対策を実践的に解説します。 このページは[パッケージ開発の基礎](/jp/advanced/package-development)の姉妹ページです。GitHub Actionsの基本的な使用方法を前提としています。 ## GitHub Actionsのセキュリティリスク ### タグベースの参照の危険性 通常、GitHub Actionsは以下のようにタグで参照されます。 ```yaml theme={null} - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 ``` このアプローチの問題点: * **タグは移動可能** — タグは削除・再作成されても同じ名前を持てます * **改ざんリスク** — リポジトリ所有者のアカウント乗っ取りで悪意あるコードを注入可能 * **サプライチェーン攻撃** — 依存するアクションが攻撃されると、あなたのワークフローが侵害されます ### 公式Laravelプロジェクトでの対応 [laravel/laravel](https://github.com/laravel/laravel) と [laravel/framework](https://github.com/laravel/framework) では、全てのアクションをコミットハッシュ(SHA)にピン留めしています。 ```yaml theme={null} # セキュアな参照方法 - uses: actions/checkout@a5ac7e51b41094c7fcab2042361574b0021804ab # v4 ``` ## ピン留めの実装戦略 ### ステップ1:Dependabot設定ファイルの作成 パッケージのリポジトリに `.github/dependabot.yml` を作成します。Laravelのファイルをそのままコピーできます。 ```yaml theme={null} version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" cooldown: default-days: 5 groups: github-actions: patterns: - "*" ``` このファイルは以下の役割を担います: * **自動スキャン** — GitHub Actionsの新しいバージョンをスキャン * **自動PR作成** — 更新が利用可能な場合、アップデートPRを自動作成 * **更新方法の制御** — ピン留めされていないアクションはバージョンで、ピン留めされたアクションはSHAで更新 ### ステップ2:既存アクションをSHAにピン留める 既存のワークフロー内のすべてのアクション参照をSHAハッシュに変更します。これは [pinact](https://github.com/suzuki-shunsuke/pinact) などのツールで自動化できます。 #### pinactツールを使った自動化 ```shell theme={null} # ワークフロー内のアクションをSHAにピン留め pinact run ``` #### 手動による変更 `pinact` が利用できない場合は、GitHubの [Lookup latest version](https://github.com/actions/checkout) ページから各アクションのコミットSHAを調べて手動で置き換えます。 ```yaml theme={null} # 変更前 - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} # 変更後 - uses: actions/checkout@a5ac7e51b41094c7fcab2042361574b0021804ab # v4 - uses: shivammathur/setup-php@0a92e4568cab5c87b7175192630661408458a086 # v2 with: php-version: ${{ matrix.php }} ``` ### ステップ3:Dependabotの設定を有効化 `.github/dependabot.yml` をリポジトリにコミットしてプッシュすると、Dependabotが自動的にスキャンを開始します。 ## Dependabotによる自動更新メカニズム Dependabotは `dependabot.yml` の設定に基づいて、異なる更新戦略を適用します。 ### ピン留めされていないアクション ```yaml theme={null} # ピン留め前 - uses: actions/checkout@v4 ``` Dependabotの更新:**バージョン範囲を新しいバージョンに更新** ```yaml theme={null} # Dependabotが作成するPR - uses: actions/checkout@v5 ``` このアプローチは利便性重視で、タグの移動に対応できますが、セキュリティリスクは残ります。 ### ピン留めされたアクション ```yaml theme={null} # ピン留め後 - uses: actions/checkout@a5ac7e51b41094c7fcab2042361574b0021804ab # v4 ``` Dependabotの更新:**新しいバージョンのコミットハッシュに更新** ```yaml theme={null} # Dependabotが作成するPR - uses: actions/checkout@f1d3225b54b677ba3e72df8c464cb6cf6dc1aebf # v4 ``` このアプローチは最も安全です。新しいバージョンでも、コミットハッシュの参照なので改ざんに強いです。 ## ワークフローの実装例 複数のワークフローでアクションを使用する場合の完全な例です。 ```yaml theme={null} name: Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: php: [8.2, 8.3, 8.4] laravel: ["^12.0", "^13.0"] steps: - uses: actions/checkout@a5ac7e51b41094c7fcab2042361574b0021804ab # v4 with: fetch-depth: 0 - name: Setup PHP uses: shivammathur/setup-php@0a92e4568cab5c87b7175192630661408458a086 # v2 with: php-version: ${{ matrix.php }} extensions: dom, curl, libxml, mbstring, zip, intl, sqlite3 coverage: none - name: Install dependencies run: | composer require "laravel/framework:${{ matrix.laravel }}" \ --no-interaction --no-update composer update --prefer-dist --no-interaction - name: Run tests run: vendor/bin/pest lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@a5ac7e51b41094c7fcab2042361574b0021804ab # v4 - name: Setup PHP uses: shivammathur/setup-php@0a92e4568cab5c87b7175192630661408458a086 # v2 with: php-version: "8.4" extensions: dom, curl, libxml, mbstring, zip coverage: none - name: Install dependencies run: composer install --prefer-dist --no-interaction - name: Run static analysis run: vendor/bin/phpstan ``` ## Dependabotの更新PR対応 Dependabotが作成するアップデートPRは以下のように対応します。 ### 単一アクションの更新PR ``` Bump shivammathur/setup-php from v1 to v2 ``` このような単純なアップデートは以下のように対応: 1. ワークフロー実行結果を確認 2. 破壊的変更がないか確認 3. マージして完了 ### セキュリティアップデートPR ``` [SECURITY] Bump actions/checkout to a5ac7e51b41094c7fcab2042361574b0021804ab ``` セキュリティ修正に関する更新は優先的にマージします。 ### 複数アクションのグループ更新 `dependabot.yml` で `groups` を設定している場合、複数のアクションが一度のPRで更新されます。 ```yaml theme={null} groups: github-actions: patterns: - "*" ``` 全アクション更新を1つのPRにまとめることで、マージ回数を減らせます。 ## ピン留めのメリットとデメリット ### メリット | メリット | 説明 | | - | - | | **サプライチェーン攻撃対策** | 特定のコミットハッシュを参照するため、アクションの改ざんに対抗できる | | **監査可能性** | 各アクションがいつどのバージョンから更新されたかを追跡できる | | **明示的な更新** | Dependabotが更新を提案する形式なので、人間のレビューを必ず経由する | | **再現性** | 同じコミットハッシュで実行すれば、完全に同じ環境が再現される | ### デメリット | デメリット | 対処方法 | | - | - | | **手作業の初期セットアップ** | `pinact` ツールで自動化 | | **更新管理の手間増加** | Dependabotで自動PR作成するため、実装コスト最小化 | | **読みにくさ** | コメントでバージョンを併記して可読性確保 | ## セキュリティ監査チェックリスト 新しいパッケージプロジェクトを開始する場合のチェックリストです。 * [ ] `.github/dependabot.yml` を作成 * [ ] 既存アクションすべてをSHAにピン留め * [ ] `pinact` または手動で確認完了 * [ ] ワークフローが正常に実行されることを確認 * [ ] Dependabotの更新PRを週1回以上レビュー * [ ] セキュリティ更新は優先的にマージ * [ ] 新しいアクション追加時は必ずSHAで参照 * [ ] 月に1回、全ワークフローの状態を確認 * [ ] すべてのアクション参照がSHAになっているか確認 * [ ] Dependabotが有効になっているか確認 * [ ] 過去6ヶ月のDependabotのPRが全てマージされているか確認 ## 関連ページ サービスプロバイダーを核としたLaravelパッケージの開発方法を解説します。 Laravelのメジャーバージョンアップへのパッケージ対応戦略を解説します。 # コレクションの高階メッセージ Source: https://kawax.biz/jp/advanced/higher-order-messages コレクションの高階メッセージ(Higher-order Messages)の仕組みと実践的な使い方 ## 高階メッセージとは 高階メッセージ(Higher-order Messages)は、コレクションのメソッドをプロパティアクセスの形で呼び出す構文です。コールバックを書くことなく、各要素に対してメソッドを呼び出したり、プロパティを取得したりできます。 ```php theme={null} // 通常の書き方 $names = $users->map(fn ($user) => $user->name); // 高階メッセージを使った書き方 $names = $users->map->name; ``` `$users->map->name` は「各ユーザーの `name` プロパティを取り出して `map` する」という意味になります。 ## 仕組み — HigherOrderCollectionProxy `$collection->map` のようにプロパティとしてアクセスすると、`EnumeratesValues` トレイトの `__get()` が呼ばれます。 ```php theme={null} // src/Illuminate/Collections/Traits/EnumeratesValues.php public function __get($key) { if (! in_array($key, static::$proxies)) { throw new Exception("Property [{$key}] does not exist on this collection instance."); } return new HigherOrderCollectionProxy($this, $key); } ``` `$proxies` に含まれるメソッド名の場合、`HigherOrderCollectionProxy` インスタンスが返ります。このプロキシはコレクションとメソッド名の2つを保持します。 ### \_\_get によるプロパティアクセスのプロキシ 続けてプロパティにアクセス(`->name`)すると、プロキシの `__get()` が呼ばれます。 ```php theme={null} // src/Illuminate/Collections/HigherOrderCollectionProxy.php public function __get($key) { return $this->collection->{$this->method}(function ($value) use ($key) { return is_array($value) ? $value[$key] : $value->{$key}; }); } ``` つまり `$users->map->name` は次と同等です。 ```php theme={null} $users->map(fn ($user) => $user->name); ``` ### \_\_call によるメソッド呼び出しのプロキシ 続けてメソッドを呼び出す(`->activate()`)と、プロキシの `__call()` が呼ばれます。 ```php theme={null} public function __call($method, $parameters) { return $this->collection->{$this->method}(function ($value) use ($method, $parameters) { return is_string($value) ? $value::{$method}(...$parameters) : $value->{$method}(...$parameters); }); } ``` `$users->each->activate()` は次と同等です。 ```php theme={null} $users->each(fn ($user) => $user->activate()); ``` ## 対応しているメソッド一覧 高階メッセージとして使えるメソッドは `$proxies` 配列で定義されています。 ```php theme={null} protected static $proxies = [ 'average', 'avg', 'contains', 'doesntContain', 'some', 'each', 'every', 'filter', 'reject', 'first', 'last', 'flatMap', 'map', 'groupBy', 'keyBy', 'hasMany', 'hasSole', 'max', 'min', 'sum', 'percentage', 'partition', 'skipUntil', 'skipWhile', 'sortBy', 'sortByDesc', 'takeUntil', 'takeWhile', 'unique', 'unless', 'until', 'when', ]; ``` カスタムメソッドを追加するには `Collection::proxy()` を使います。 ```php theme={null} Collection::proxy('myCustomMethod'); ``` ## 実践的なユースケース ### Eloquentモデルのコレクション 高階メッセージはEloquentのコレクションで特に便利です。 ```php theme={null} $users = User::with('posts')->get(); // メソッド呼び出し: 各ユーザーのステータスを更新 $users->each->markAsVerified(); // プロパティアクセス: 名前だけ取り出す $names = $users->map->name; // メソッドに引数を渡す $users->each->sendPasswordReset('ja'); // メール送信 $users->each->notify(new WelcomeNotification); ``` ### フィルタリング ```php theme={null} $activeUsers = $users->filter->isActive(); // isActive() が true を返すユーザーだけ残す // $users->filter(fn ($user) => $user->isActive()) と同等 $inactiveUsers = $users->reject->isActive(); ``` ### 集計 ```php theme={null} // 各ユーザーのpost_countプロパティで合計 $totalPosts = $users->sum->post_count; // 各ユーザーのscoreで最大値 $highestScore = $users->max->score; // 各ユーザーのageで平均 $averageAge = $users->avg->age; ``` ### グループ化・ソート ```php theme={null} // role プロパティでグループ化 $grouped = $users->groupBy->role; // name プロパティで昇順ソート $sorted = $users->sortBy->name; // created_at プロパティで降順ソート $sorted = $users->sortByDesc->created_at; ``` ### flatMap でネストを展開 ```php theme={null} // 各ユーザーの posts リレーションを展開して1つのコレクションにする $posts = $users->flatMap->posts; ``` ### 条件確認 ```php theme={null} // 全員が管理者か確認 $allAdmins = $users->every->isAdmin(); // 1人でもアクティブがいるか確認 $hasActive = $users->contains->isActive(); // 全員が特定のプランか確認 $allPro = $users->every->hasPlan('pro'); ``` ### 文字列コレクション 値が文字列のコレクションでも使えます。プロキシの `__call()` は文字列の場合に静的メソッドとして呼び出します。 ```php theme={null} $names = collect(['alice', 'bob', 'charlie']); // 文字列コレクションに対してはクロージャの方が明示的 $upper = $names->map(fn ($name) => strtoupper($name)); // ['ALICE', 'BOB', 'CHARLIE'] ``` 文字列コレクションへの高階メッセージは、メソッドが文字列型に静的に定義されている必要があります。通常はEloquentモデルや独自クラスのコレクションで使うのが自然です。 ## 通常のクロージャとの比較 高階メッセージは簡潔ですが、すべてのケースで使えるわけではありません。 ```php theme={null} // 高階メッセージ: シンプルなプロパティ取得・メソッド呼び出しに適する $emails = $users->map->email; $users->each->sendWelcomeMail(); $admins = $users->filter->isAdmin(); // クロージャ: 複雑なロジックや引数が必要なケースに適する $formatted = $users->map(function ($user) { return "{$user->name} <{$user->email}>"; }); $filtered = $users->filter(function ($user) use ($minAge, $role) { return $user->age >= $minAge && $user->role === $role; }); ``` 「各要素の同じプロパティを取り出す」「各要素の同じメソッドを呼ぶ」場合は高階メッセージが読みやすくなります。条件が複雑だったり引数が動的だったりする場合はクロージャを使います。 ## カスタムクラスで高階メッセージを使う `EnumeratesValues` トレイトを通じて `HigherOrderCollectionProxy` の仕組みを利用するには、`Collection::proxy()` を呼んでメソッドをプロキシ対象に追加します。 ```php theme={null} namespace App\Providers; use Illuminate\Support\Collection; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function boot(): void { // カスタムマクロをプロキシ対象に追加 Collection::macro('active', function () { return $this->filter(fn ($item) => $item->isActive()); }); Collection::proxy('active'); } } ``` ```php theme={null} // 登録後は高階メッセージとして使える $activeUsers = $users->active; ``` ## 次のステップ `when()` / `unless()` の内部実装と、QueryBuilderや自作クラスへの適用方法を学びます。 # InteractsWithDataトレイト Source: https://kawax.biz/jp/advanced/interacts-with-data Illuminate\Support\Traits\InteractsWithData の設計と、パッケージ開発で自前クラスにデータアクセスAPIを実装する方法を解説します。 ## InteractsWithDataトレイトとは `Illuminate\Support\Traits\InteractsWithData` は、「配列ライクな入力データに対する共通API」をまとめたトレイトです。 このトレイト自体は `all()` と `data()` の2つだけを抽象メソッドとして要求し、実データの取得ロジックは各クラス側に委譲します。代わりに、以下のような高頻度メソッドをまとめて提供します。 * 存在判定: `has()`, `hasAny()`, `exists()`, `missing()` * 空判定: `filled()`, `isNotFilled()`, `anyFilled()` * 条件実行: `whenHas()`, `whenFilled()`, `whenMissing()` * 抽出: `only()`, `except()` * 型変換: `string()`, `boolean()`, `integer()`, `float()`, `date()`, `enum()`, `collect()` ## Request系メソッドとの関係 `request()` ヘルパーで取得する `Illuminate\Http\Request` は、`Concerns\InteractsWithInput` を通して `InteractsWithData` を取り込みます。 そのため、次のような日常的な入力アクセスはトレイト経由で提供されています。 ```php theme={null} $search = request()->input('search'); if (request()->has('search')) { $filters = request()->only(['search', 'status']); } $payload = request()->except(['_token']); ``` `Request::get()` は `Request` クラス本体にある Symfony 互換メソッドです。Laravel 13 のソースコード上でも `@deprecated use ->input() instead` と明記され、推奨は `input()` です。 ```php theme={null} $legacy = request()->get('search'); // 互換目的のメソッド(推奨は input()) ``` ## Laravelコアでの主な実装例 ### 直接 `InteractsWithData` を use するクラス | クラス | 用途 | | - | - | | `Illuminate\Http\Concerns\InteractsWithInput` | `Request` の入力アクセスAPI | | `Illuminate\Support\ValidatedInput` | `validated()` / `safe()` の戻り値ラッパー | | `Illuminate\Support\Fluent` | 設定値や任意属性のフルーエント操作 | | `Illuminate\Support\UriQueryString` | `Uri` のクエリ文字列操作 | | `Illuminate\View\ComponentAttributeBag` | Bladeコンポーネント属性の操作 | ### 近い責務を持つ関連実装 * `Illuminate\Session\Store` は `has()`, `get()`, `only()`, `except()` など似たAPIを独自実装しています * `Illuminate\Validation\Concerns\ValidatesAttributes` はバリデーション判定ロジックを提供し、入力アクセスAPIそのものは `InteractsWithData` とは別責務で設計されています ## トレイトと主要クラスの関係 ```mermaid theme={null} flowchart TD IWD["InteractsWithData
    共通データアクセスAPI"] --> IWI["InteractsWithInput
    Request向け Concern トレイト"] IWI --> REQ["Request
    input()/has()/only()/except()"] IWD --> VIN["ValidatedInput
    safe()/validated()"] IWD --> FLU["Fluent"] IWD --> UQS["UriQueryString"] IWD --> CAB["ComponentAttributeBag"] REQ -.類似APIパターン.-> SES["Session Store
    has()/get()/only()/except() を独自実装"] REQ -.関連領域(直接依存ではない).-> VAL["ValidatesAttributes
    バリデーション判定ロジック"] ``` ## パッケージ開発で自前クラスに組み込む `InteractsWithData` は、パッケージ内で「入力配列を保持し、Laravel風の取得APIを提供したい」クラスに向いています。 ```php theme={null} namespace Vendor\Package\Support; use Illuminate\Support\Arr; use Illuminate\Support\Traits\InteractsWithData; class OptionBag { use InteractsWithData; /** * @param array $items */ public function __construct( protected array $items = [], ) {} public function all($keys = null): array { if (! $keys) { return $this->items; } $result = []; $keyList = is_array($keys) ? $keys : [$keys]; foreach ($keyList as $key) { Arr::set($result, $key, Arr::get($this->items, $key)); } return $result; } protected function data($key = null, $default = null): mixed { return data_get($this->items, $key, $default); } } ``` ```php theme={null} $options = new OptionBag([ 'feature.enabled' => 'true', 'retry.max' => '5', 'channels' => ['mail', 'slack'], ]); $enabled = $options->boolean('feature.enabled'); // true $retryMax = $options->integer('retry.max'); // 5 $channels = $options->collect('channels'); // Collection $public = $options->except(['secret']); // secret を除外 ``` ## 実践ユースケース * 外部APIクライアントのオプションバッグ * Webhookペイロードの正規化レイヤー * パッケージの設定オーバーライド解決クラス `all()` と `data()` だけ実装すれば、入力アクセスAPIを毎回自作せずに済むため、メンテナンスコストを下げられます。 ## 関連ページ * [Macroableトレイト](/jp/advanced/macroable) * [Conditionableトレイト](/jp/advanced/conditionable) * [tap() ヘルパーと Tappable トレイト](/jp/advanced/tap) # InteractsWithTime トレイト Source: https://kawax.biz/jp/advanced/interacts-with-time キュー・キャッシュ・スケジューリングで使われる時間計算ユーティリティトレイトの解説と活用法 ## InteractsWithTime とは `Illuminate\Support\InteractsWithTime` は、時間・遅延・経過時間の計算をまとめたトレイトです。Laravel フレームワーク内では 40 以上のクラスが `use InteractsWithTime;` しており、キャッシュ・キュー・コンソール・データベース接続など幅広いコンポーネントで使われています。 ソースコードは `src/Illuminate/Support/InteractsWithTime.php` にあります。`protected` メソッドのみで構成されており、クラス内部から呼び出す用途を前提にしています。 ## フレームワーク内の使用例 | クラス | 利用目的 | | - | - | | `Cache\ArrayStore` / `DatabaseStore` 等 | TTL(有効期限)の計算 | | `Cache\RateLimiter` | レート制限のリセット時刻 | | `Queue\Console\RestartCommand` | ワーカー再起動タイムスタンプ | | `Database\Connection` | クエリ実行時間の計測 | | `Console\View\Components\Task` | コマンドタスクの実行時間表示 | | `Redis\Limiters\DurationLimiterBuilder` | Redis レート制限の期間計算 | ## メソッド解説 ### `secondsUntil()` — 残り秒数を取得 指定した日時・遅延値までの残り秒数を返します。引数には `DateTimeInterface`・`DateInterval`・秒数の整数値のいずれかを渡せます。 ```php theme={null} use Illuminate\Support\InteractsWithTime; class MyService { use InteractsWithTime; public function scheduleAt(\DateTime $target): int { return $this->secondsUntil($target); // 現在から $target までの秒数 } } ``` 過去の日時を渡すと `0` を返します(負にはなりません)。 ```php theme={null} $seconds = $this->secondsUntil(now()->addMinutes(5)); // 300 $seconds = $this->secondsUntil(now()->subMinutes(5)); // 0(過去は 0) $seconds = $this->secondsUntil(60); // 60(整数はそのまま秒数) ``` ### `availableAt()` — 利用可能になる UNIX タイムスタンプ 現在時刻に遅延を加算した UNIX タイムスタンプを返します。キューのジョブ遅延やキャッシュの有効期限計算に使われます。 ```php theme={null} $timestamp = $this->availableAt(60); // 60 秒後の UNIX タイムスタンプ $timestamp = $this->availableAt(new \DateTime('2026-12-31')); // 指定日時の UNIX タイムスタンプ $timestamp = $this->availableAt(new \DateInterval('PT5M')); // 5 分後 ``` 引数なし(デフォルト `0`)で呼ぶと現在時刻のタイムスタンプを返します。 ### `parseDateInterval()` — DateInterval を DateTime に変換 `DateInterval` インスタンスを受け取ると、現在時刻に加算した `Carbon` インスタンスに変換して返します。`DateTimeInterface` や整数値はそのまま返します。 ```php theme={null} $delay = $this->parseDateInterval(new \DateInterval('P1D')); // 明日の Carbon $delay = $this->parseDateInterval(now()->addHour()); // DateTimeInterface はそのまま $delay = $this->parseDateInterval(3600); // 3600(整数はそのまま) ``` `secondsUntil()` と `availableAt()` は内部でこのメソッドを呼んでいます。 ### `currentTime()` — 現在時刻の UNIX タイムスタンプ `Carbon::now()->getTimestamp()` を返すシンプルなラッパーです。`secondsUntil()` の基準時刻として使われます。 ```php theme={null} $now = $this->currentTime(); // int(UNIX タイムスタンプ) ``` ### `runTimeForHumans()` — 実行時間を人間が読みやすい形式に `microtime(true)` で記録した開始時刻と終了時刻の差を、読みやすい文字列に変換します。 ```php theme={null} $start = microtime(true); // ... 処理 ... echo $this->runTimeForHumans($start); // 例: "42.15ms" または "1s 234ms" ``` 1000ms 未満は `42.15ms` のように小数点付きミリ秒、1000ms 以上は CarbonInterval の `forHumans()` 短縮形式(`1s 234ms` など)で返します。 第2引数に終了時刻を渡すことで、計測済みの区間を変換することもできます。 ```php theme={null} $elapsed = $this->runTimeForHumans($start, $end); ``` ## パッケージ開発での活用 ### キャッシュドライバーの TTL 統一 `put(key, value, ttl)` の `ttl` として `int`・`DateInterval`・`DateTime` のいずれも受け付けるメソッドを作るときに便利です。 ```php theme={null} use Illuminate\Support\InteractsWithTime; class MyStore { use InteractsWithTime; public function put(string $key, mixed $value, mixed $ttl): void { $seconds = $this->secondsUntil($ttl); // $seconds(int)で実際のストレージに保存 $this->storage[$key] = [ 'value' => $value, 'expires_at' => $this->availableAt($ttl), ]; } } ``` ### コンソールコマンドの処理時間表示 `Artisan` コマンドの `handle()` で処理時間をユーザーに表示する際に活用できます。 ```php theme={null} use Illuminate\Console\Command; use Illuminate\Support\InteractsWithTime; class ProcessDataCommand extends Command { use InteractsWithTime; public function handle(): void { $start = microtime(true); // ... 処理 ... $this->info("完了: {$this->runTimeForHumans($start)}"); } } ``` ## テスト用の InteractsWithTime `Illuminate\Foundation\Testing\Concerns\InteractsWithTime` は別物で、テストケースで時間を操作する `travel*()` メソッドを提供します(`TestCase` に自動的に組み込まれます)。 ```php theme={null} // テスト内で時間を進める(Testing\Concerns\InteractsWithTime) $this->travel(5)->days(); $this->travelTo(now()->addMonth()); $this->travelBack(); ``` `Illuminate\Support\InteractsWithTime`(本ページの対象)とは名前空間が異なるため、混同しないよう注意してください。 ## まとめ | メソッド | 戻り値 | 主な用途 | | - | - | - | | `secondsUntil($delay)` | `int` | 残り秒数の取得 | | `availableAt($delay)` | `int` | 有効開始タイムスタンプの計算 | | `parseDateInterval($delay)` | `DateTimeInterface\|int` | DateInterval → Carbon への変換 | | `currentTime()` | `int` | 現在の UNIX タイムスタンプ | | `runTimeForHumans($start, $end)` | `string` | 実行時間の人間向け表示 | 引数に `int`・`DateInterval`・`DateTimeInterface` のいずれも受け付けるメソッドを作りたい場合、このトレイトを `use` するだけで変換ロジックを再実装せずに済みます。 ## 関連ページ * [パッケージ開発](/jp/advanced/package-development) * [InteractsWithData トレイト](/jp/advanced/interacts-with-data) * [Conditionable トレイト](/jp/advanced/conditionable) # Lotteryクラス Source: https://kawax.biz/jp/advanced/lottery Illuminate\Support\Lottery を使って確率的な操作をエレガントに実装する方法を解説します。パッケージ開発でのユースケースやテスト時の固定化も紹介します。 ## Lotteryクラスとは `Illuminate\Support\Lottery` は、確率ベースの操作を流れるようなAPIで表現できるユーティリティクラスです。「100リクエストに1回だけ処理を実行する」「一部のリクエストのみ詳細ログを記録する」といったパターンをシンプルに記述できます。 実装は `src/Illuminate/Support/Lottery.php` にあります。LaravelはこのクラスをSession GCやキャッシュロックのプルーンなど、フレームワーク内部でも活用しています。 ```mermaid theme={null} flowchart LR A["Lottery::odds(1, 100)"] --> B{"ランダム判定"} B -->|"当選 (1%)"| C["winner コールバック実行"] B -->|"落選 (99%)"| D["loser コールバック実行"] ``` ## 基本的な使い方 ### 整数比率で確率を指定する `Lottery::odds($chances, $outOf)` で「$outOf 回中 $chances 回当選」という確率を指定します。 ```php theme={null} use Illuminate\Support\Lottery; Lottery::odds(1, 100) // 100回に1回 ->winner(fn () => $this->runMaintenance()) ->loser(fn () => null) ->choose(); ``` ### 小数で確率を指定する `$outOf` を省略して `0.0`〜`1.0` の小数を渡すと、そのまま確率として使われます。 ```php theme={null} Lottery::odds(0.01) // 1% の確率 ->winner(fn () => $this->sample()) ->choose(); ``` 小数指定のとき値が `1.0` を超えると `RuntimeException` が投げられます。 ### コールバックなしで真偽値を返す winner / loser を設定しない場合、`choose()` は当選なら `true`、落選なら `false` を返します。 ```php theme={null} $shouldSample = Lottery::odds(1, 50)->choose(); // bool ``` ### 複数回実行する `choose($times)` に回数を渡すと結果の配列が返されます。 ```php theme={null} $results = Lottery::odds(1, 2) ->winner(fn () => 'win') ->loser(fn () => 'lose') ->choose(10); // 例: ['win', 'lose', 'win', 'win', 'lose', ...] ``` ### Callable として渡す `Lottery` インスタンスは `__invoke` を実装しているため、callable を受け取るAPIに直接渡せます。 ```php theme={null} // DB::whenQueryingForLongerThan の第二引数として渡す例 DB::whenQueryingForLongerThan( Interval::seconds(5), Lottery::odds(1, 5)->winner(function ($connection) { // スロークエリを検知したとき 1/5 の確率でアラート送信 Alert::send("Slow query on {$connection->getName()}"); }) ); ``` ## 実践的なユースケース ### 1. キャッシュのプルーン(100回に1回だけ実行) 期限切れレコードの削除など、毎回実行する必要がないメンテナンス処理に最適です。 ```php theme={null} Lottery::odds(1, 100) ->winner(fn () => Cache::store('database')->flush()) ->choose(); ``` ### 2. テレメトリ・サンプリング(一部リクエストのみ詳細ログ) 全リクエストをログに残すとコストが高い場合、サンプリングに使えます。 ```php theme={null} Lottery::odds(1, 20) ->winner(function () use ($request) { Log::channel('telemetry')->info('Request sampled', [ 'url' => $request->url(), 'duration' => microtime(true) - LARAVEL_START, 'memory' => memory_get_peak_usage(true), ]); }) ->choose(); ``` ### 3. A/Bテスト的な振る舞い ユーザーを確率的に2つのコードパスに振り分けます。 ```php theme={null} $result = Lottery::odds(1, 2) ->winner(fn ($user) => $this->newCheckoutFlow($user)) ->loser(fn ($user) => $this->legacyCheckoutFlow($user)) ->choose(); ``` ### 4. Schedulerの補助として定期タスクをランダム実行 複数サーバーで重複実行を避けつつ、あるタスクをランダムに実行したいときに使えます。 ```php theme={null} // app/Console/Kernel.php $schedule->call(function () { Lottery::odds(1, 3) ->winner(fn () => Artisan::call('cache:prune-stale-tags')) ->choose(); })->everyMinute(); ``` ## Laravelフレームワーク内での確率的パターン Laravelはフレームワーク内部でも確率的なメンテナンス処理を広く使っています。一部の実装は `Lottery` クラスが追加される前に書かれたため `random_int()` を直接使っていますが、同じ思想に基づいています。 `Illuminate\Session\Middleware\StartSession::configHitsLottery()` は `config/session.php` の `lottery` 設定を使い、`random_int` で確率判定してGCを実行します。 ```php theme={null} // config/session.php 'lottery' => [2, 100], // 100リクエストに2回 // StartSession 内部 (random_int を直接使用) protected function configHitsLottery(array $config): bool { return random_int(1, $config['lottery'][1]) <= $config['lottery'][0]; } ``` `Illuminate\Cache\DatabaseLock::acquire()` はロック取得のたびに同じ比率パターンで期限切れロックを削除します。 ```php theme={null} // config/cache.php (database ドライバー) 'lock_lottery' => [2, 100], // 100回に2回プルーン実行 // DatabaseLock::acquire() 内部 (random_int を直接使用) if (random_int(1, $this->lottery[1]) <= $this->lottery[0]) { $this->pruneExpiredLocks(); } ``` `Lottery` インスタンスは callable として渡せるため、スロークエリ検知コールバックに直接使えます。 ```php theme={null} DB::whenQueryingForLongerThan( Interval::seconds(5), Lottery::odds(1, 5)->winner(function ($connection) { Log::warning("Slow query on {$connection->getName()}"); }) ); ``` Session や DatabaseLock が `random_int()` を直接使っているのに対し、`Lottery` クラスを使うと `alwaysWin()` / `alwaysLose()` / `fix()` でテスト時に結果を制御できるメリットがあります。パッケージ開発では `Lottery` クラスを選ぶとテスタビリティが向上します。 ## テスト時の利用 ランダム性があるコードのテストには、`Lottery` が提供するテスト用APIを使います。 ### `Lottery::alwaysWin()` — 常に当選させる ```php theme={null} public function test_maintenance_runs_on_win(): void { $ranMaintenance = false; Lottery::alwaysWin(function () use (&$ranMaintenance) { Lottery::odds(1, 100) ->winner(function () use (&$ranMaintenance) { $ranMaintenance = true; }) ->choose(); }); $this->assertTrue($ranMaintenance); } ``` ### `Lottery::alwaysLose()` — 常に落選させる ```php theme={null} public function test_maintenance_skipped_on_lose(): void { $ranMaintenance = false; Lottery::alwaysLose(function () use (&$ranMaintenance) { Lottery::odds(1, 100) ->winner(function () use (&$ranMaintenance) { $ranMaintenance = true; }) ->choose(); }); $this->assertFalse($ranMaintenance); } ``` ### `Lottery::fix()` — 結果をシーケンスで固定する 複数回の呼び出し結果を `true`/`false` の配列で制御できます。 ```php theme={null} public function test_alternating_results(): void { Lottery::fix([true, false, true, false]); $results = Lottery::odds(1, 100) ->winner(fn () => 'winner') ->loser(fn () => 'loser') ->choose(4); $this->assertSame(['winner', 'loser', 'winner', 'loser'], $results); Lottery::determineResultNormally(); // テスト後は必ず元に戻す } ``` `alwaysWin()` / `alwaysLose()` / `fix()` はグローバルな静的プロパティを変更します。テストの `tearDown()` で必ず `Lottery::determineResultNormally()` を呼んでください。 ```php theme={null} protected function tearDown(): void { Lottery::determineResultNormally(); parent::tearDown(); } ``` ### `Lottery::setResultFactory()` — カスタムファクトリを注入する より細かい制御が必要な場合は、カスタムファクトリを使います。 ```php theme={null} Lottery::setResultFactory(function ($chances, $outOf) { // 常に当選させるカスタムロジック return true; }); // テスト後はリセット Lottery::determineResultNormally(); ``` ## パッケージ開発での活用 ### サービスプロバイダーでの登録 パッケージのサービスプロバイダーにメンテナンス処理を組み込む場合、`Lottery` を使って負荷を分散させます。 ```php theme={null} use Illuminate\Support\Lottery; use Illuminate\Support\ServiceProvider; class AcmeServiceProvider extends ServiceProvider { public function boot(): void { $this->app->booted(function () { Lottery::odds(1, 100) ->winner(fn () => $this->pruneExpiredRecords()) ->choose(); }); } protected function pruneExpiredRecords(): void { $this->app['db']->table('acme_logs') ->where('expires_at', '<', now()) ->delete(); } } ``` ### 設定値からオッズを読み込む 確率を設定ファイルから変更可能にすると、ユーザーが調整しやすくなります。 ```php theme={null} $lottery = config('acme.prune_lottery', [1, 100]); Lottery::odds(...$lottery) ->winner(fn () => $this->pruneExpiredRecords()) ->choose(); ``` ```php theme={null} // config/acme.php return [ // [当選数, 試行数] = 100リクエストに1回プルーン実行 'prune_lottery' => [1, 100], ]; ``` ### Middleware でのサンプリング ```php theme={null} use Illuminate\Support\Lottery; class SampleTelemetryMiddleware { public function handle(Request $request, Closure $next): Response { $response = $next($request); Lottery::odds(1, 50) ->winner(fn () => $this->recordTelemetry($request, $response)) ->choose(); return $response; } } ``` ## API リファレンス | メソッド | 説明 | | - | - | | `Lottery::odds($chances, $outOf)` | Lotteryインスタンスを生成する静的ファクトリ | | `->winner(callable $callback)` | 当選時のコールバックを設定 | | `->loser(callable $callback)` | 落選時のコールバックを設定 | | `->choose($times = null)` | Lotteryを実行。`$times` を指定すると配列を返す | | `Lottery::alwaysWin($callback)` | テスト用: 常に当選 | | `Lottery::alwaysLose($callback)` | テスト用: 常に落選 | | `Lottery::fix($sequence)` | テスト用: 結果をシーケンスで固定 | | `Lottery::determineResultNormally()` | テスト用固定をリセット | | `Lottery::setResultFactory(callable)` | カスタム判定ロジックを注入 | ## 関連ページ 既存クラスに新しいメソッドを追加する拡張パターンを学びます。 `when()` / `unless()` による条件分岐チェーンの設計を学びます。 # Macroableトレイト Source: https://kawax.biz/jp/advanced/macroable Illuminate\Support\Traits\Macroable トレイトを使って、既存のLaravelクラスに独自メソッドを追加する方法を解説します。 ## Macroableトレイトとは `Macroable` トレイトは、クラスを変更せずに後からメソッドを動的に追加できる仕組みです。Laravelのコアクラスの多くがこのトレイトを使っているため、コアコードに手を加えることなく機能を拡張できます。 トレイトの実体は `Illuminate\Support\Traits\Macroable` にあります。内部的には、登録されたマクロを静的プロパティ `$macros` に格納し、`__call` / `__callStatic` マジックメソッドを通じて呼び出します。 ## Macroableを使っているクラス Laravelには多くのMacroable対応クラスがあります。 | クラス | 用途 | | - | - | | `Illuminate\Support\Collection` | コレクション操作 | | `Illuminate\Support\Str` | 文字列操作 | | `Illuminate\Support\Arr` | 配列操作 | | `Illuminate\Http\Request` | HTTPリクエスト | | `Illuminate\Http\Response` | HTTPレスポンス | | `Illuminate\Routing\Router` | ルーター | | `Illuminate\Routing\ResponseFactory` | レスポンスファクトリ | | `Illuminate\Database\Schema\Blueprint` | スキーマビルダー | | `Illuminate\Pipeline\Pipeline` | パイプライン | | `Illuminate\Testing\TestResponse` | テストレスポンス | ## macro() — メソッドを追加する `macro()` の第一引数にメソッド名、第二引数にクロージャを渡します。 ```php theme={null} use Illuminate\Support\Collection; Collection::macro('toSentence', function (string $separator = '、') { /** @var Collection $this */ return $this->implode($separator); }); $result = collect(['りんご', 'みかん', 'ぶどう'])->toSentence(); // 'りんご、みかん、ぶどう' ``` クロージャ内の `$this` はマクロを呼び出したインスタンスにバインドされます。これにより、クラスのプロパティやメソッドに直接アクセスできます。 ## mixin() — 複数メソッドをまとめて追加する 多くのマクロをまとめて登録したい場合は `mixin()` を使います。ミックスインクラスの `public` / `protected` メソッドがすべてマクロとして登録されます。 ```php theme={null} namespace App\Mixins; class CollectionMixin { public function toCsv(): Closure { return function (string $separator = ',') { /** @var \Illuminate\Support\Collection $this */ return $this->map(function ($item) use ($separator) { return is_array($item) ? implode($separator, $item) : $item; })->implode("\n"); }; } public function filterEmpty(): Closure { return function () { /** @var \Illuminate\Support\Collection $this */ return $this->filter(fn ($item) => ! empty($item))->values(); }; } public function groupByFirst(): Closure { return function (string $key) { /** @var \Illuminate\Support\Collection $this */ return $this->groupBy(fn ($item) => $item[$key][0] ?? ''); }; } } ``` `mixin()` のメソッドは、マクロとして登録されるクロージャを返す必要があります。メソッド自体の戻り値がマクロの実装になります。 ## サービスプロバイダーへの登録 マクロはアプリケーション起動時に登録する必要があります。`AppServiceProvider` の `boot()` メソッドが適切な場所です。 ```php theme={null} namespace App\Providers; use App\Mixins\CollectionMixin; use Illuminate\Support\Collection; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Str; use Illuminate\Http\Request; class AppServiceProvider extends ServiceProvider { public function boot(): void { // 単一マクロを登録 Collection::macro('toSentence', function (string $separator = '、') { return $this->implode($separator); }); // ミックスインをまとめて登録 Collection::mixin(new CollectionMixin); // Strへのマクロ Str::macro('initials', function (string $name) { return collect(explode(' ', $name)) ->map(fn ($word) => strtoupper($word[0])) ->implode(''); }); } } ``` ## 実践的なユースケース ### コレクションの拡張 コレクションへのカスタムメソッド追加は最も一般的な使用例です。 ```php theme={null} // 数値コレクションの統計メソッド Collection::macro('median', function () { $sorted = $this->sort()->values(); $count = $sorted->count(); if ($count === 0) { return null; } $middle = (int) floor($count / 2); if ($count % 2 === 0) { return ($sorted->get($middle - 1) + $sorted->get($middle)) / 2; } return $sorted->get($middle); }); $median = collect([3, 1, 4, 1, 5, 9, 2, 6])->median(); // 3.5 // ページネーション情報を付与したコレクションを返すマクロ Collection::macro('paginateArray', function (int $perPage = 15, int $page = 1) { return $this->slice(($page - 1) * $perPage, $perPage)->values(); }); ``` ### Strクラスの拡張 ```php theme={null} // 日本語の文字数カウント(マルチバイト対応) Str::macro('mbLength', function (string $value) { return mb_strlen($value, 'UTF-8'); }); // スネークケースをドット記法に変換 Str::macro('toDotNotation', function (string $value) { return str_replace('_', '.', $value); }); $length = Str::mbLength('こんにちは'); // 5 $dot = Str::toDotNotation('user_profile_name'); // 'user.profile.name' ``` ### Requestクラスの拡張 ```php theme={null} use Illuminate\Http\Request; Request::macro('isFromMobile', function () { /** @var Request $this */ $userAgent = $this->userAgent() ?? ''; return preg_match('/Mobile|Android|iPhone|iPad/i', $userAgent) === 1; }); Request::macro('preferredLocale', function (array $available = ['ja', 'en']) { /** @var Request $this */ foreach ($this->getLanguages() as $lang) { $short = substr($lang, 0, 2); if (in_array($short, $available)) { return $short; } } return $available[0] ?? 'en'; }); ``` ```php theme={null} // コントローラーで使用 public function index(Request $request) { if ($request->isFromMobile()) { return response()->json($this->getMobileData()); } $locale = $request->preferredLocale(['ja', 'en', 'zh']); // ... } ``` ### Blueprintの拡張(マイグレーション) スキーマのカラム定義をまとめてマクロ化すると、一貫したDB設計を保てます。 ```php theme={null} use Illuminate\Database\Schema\Blueprint; Blueprint::macro('addTimestampsWithTimezone', function () { /** @var Blueprint $this */ $this->timestampsTz(); $this->softDeletesTz(); }); Blueprint::macro('addUserTracking', function () { /** @var Blueprint $this */ $this->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete(); $this->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete(); }); ``` ```php theme={null} // マイグレーションで使用 Schema::create('posts', function (Blueprint $table) { $table->id(); $table->string('title'); $table->text('body'); $table->addTimestampsWithTimezone(); $table->addUserTracking(); }); ``` ### テストレスポンスの拡張 テスト専用のアサーションメソッドを追加できます。 ```php theme={null} use Illuminate\Testing\TestResponse; TestResponse::macro('assertPaginated', function () { /** @var TestResponse $this */ return $this->assertJsonStructure([ 'data', 'meta' => ['current_page', 'last_page', 'per_page', 'total'], 'links' => ['first', 'last', 'prev', 'next'], ]); }); TestResponse::macro('assertApiSuccess', function () { /** @var TestResponse $this */ return $this->assertOk()->assertJsonPath('success', true); }); ``` ```php theme={null} // テストで使用 $this->getJson('/api/posts')->assertPaginated(); $this->postJson('/api/orders', $data)->assertApiSuccess(); ``` ## hasMacro() — マクロの存在確認 ```php theme={null} use Illuminate\Support\Collection; if (Collection::hasMacro('toSentence')) { $result = collect(['a', 'b'])->toSentence(); } ``` ## flushMacros() — マクロのリセット テストでマクロをリセットしたい場合に使います。 ```php theme={null} use Illuminate\Support\Collection; // テスト内でのリセット Collection::flushMacros(); ``` `flushMacros()` はそのクラスのすべてのマクロを削除します。テスト間の独立性を保つために `tearDown()` で呼ぶことがありますが、他のテストで登録したマクロも消えるため注意が必要です。 ## 静的マクロ マクロはインスタンスメソッドとしてだけでなく、静的メソッドとしても動作します。`__callStatic` によって処理されます。 ```php theme={null} Str::macro('randomHex', function (int $length = 8) { return substr(bin2hex(random_bytes($length)), 0, $length); }); // 静的呼び出し $hex = Str::randomHex(16); ``` ## Macroableトレイトを自作クラスで使う 自分で作ったクラスにも `Macroable` を組み込めます。 ```php theme={null} namespace App\Services; use Illuminate\Support\Traits\Macroable; class ReportBuilder { use Macroable; protected array $sections = []; public function addSection(string $name, callable $content): static { $this->sections[$name] = $content; return $this; } public function build(): array { return array_map(fn ($fn) => $fn(), $this->sections); } } ``` ```php theme={null} // サービスプロバイダーで拡張 ReportBuilder::macro('withSummary', function (string $title) { /** @var ReportBuilder $this */ return $this->addSection('summary', fn () => [ 'title' => $title, 'generated_at' => now()->toIso8601String(), ]); }); // 使用例 $report = app(ReportBuilder::class) ->withSummary('月次レポート') ->addSection('data', fn () => ['rows' => 42]) ->build(); ``` ## 内部実装の詳細 ```php theme={null} // __call の実装(インスタンスメソッド呼び出し) public function __call($method, $parameters) { if (! static::hasMacro($method)) { throw new BadMethodCallException(sprintf( 'Method %s::%s does not exist.', static::class, $method )); } $macro = static::$macros[$method]; if ($macro instanceof Closure) { // bindTo で $this をインスタンスにバインド $macro = $macro->bindTo($this, static::class); } return $macro(...$parameters); } ``` クロージャは `Closure::bindTo()` によってインスタンスにバインドされます。これにより `$this` がマクロ呼び出し元のオブジェクトを指すようになります。クロージャ以外(invokableオブジェクトなど)の場合はバインドされません。 IDEのサポートを得るには、マクロのアノテーションを `@mixin` を使ったDocブロックで定義するか、Laravel IdeHelperパッケージを使ってヘルパーファイルを自動生成する方法があります。 ## 次のステップ パイプラインパターンを使って複数の処理ステップを直列に構成する方法を学びます。 # Illuminate\Support\Manager — ドライバーシステムの解剖 Source: https://kawax.biz/jp/advanced/manager Laravel 4.0から続くドライバーシステムの核心クラス。カスタムドライバーの登録方法からMultipleInstanceManagerまで解説。 ## Manager とは `Illuminate\Support\Manager` は Laravel 4.0 の頃から存在する抽象クラスです。キャッシュ・セッション・メールなど、複数の「ドライバー」を切り替えて使えるシステムを簡単に構築するための基盤を提供します。 「ドライバー」とは、同じインターフェースを持ちながら内部実装が異なるバックエンドのことです。たとえばセッションなら `file`・`cookie`・`database`・`redis` といったドライバーがあり、設定ファイルの `driver` キーで切り替えます。 ```mermaid theme={null} classDiagram class Manager { <> #container #config #customCreators[] #drivers[] +driver(name) mixed +extend(driver, Closure) self +getDrivers() array +forgetDrivers() self +getDefaultDriver()* string #createDriver(driver) mixed #callCustomCreator(driver) mixed } class SessionManager { +getDefaultDriver() string #createFileDriver() Store #createDatabaseDriver() Store #createRedisDriver() Store #createArrayDriver() Store } class YourCustomManager { +getDefaultDriver() string #createFooDriver() mixed #createBarDriver() mixed } Manager <|-- SessionManager Manager <|-- YourCustomManager ``` ## フレームワーク内での利用状況 `Manager` という名前が付いていても `Illuminate\Support\Manager` を継承して**いない**クラスが存在します。 | クラス | `Manager` を継承しているか | | - | - | | `Illuminate\Session\SessionManager` | ✅ 継承している | | `Illuminate\Cache\CacheManager` | ❌ 継承していない(独自実装) | | `Illuminate\Queue\QueueManager` | ❌ 継承していない(独自実装) | | `Laravel\Socialite\SocialiteManager` | ✅ 継承している | 継承していないクラスも `extend()` で拡張する基本的な考え方は同じです。そのため、`Manager` パターンを理解することがフレームワーク全体の動作理解につながります。 ## 基本的な仕組み ### driver() — ドライバーを取得する `driver()` を呼ぶと、次の手順でドライバーが解決されます。 ```mermaid theme={null} flowchart TD A["driver(name) を呼ぶ"] --> B{drivers[] に
    キャッシュがあるか?} B -- はい --> C["キャッシュされた
    インスタンスを返す"] B -- いいえ --> D{customCreators[]
    に登録があるか?} D -- はい --> E["callCustomCreator()
    でインスタンスを生成"] D -- いいえ --> F{"createXxxDriver()
    メソッドが存在するか?"} F -- はい --> G["createXxxDriver()
    を呼ぶ"] F -- いいえ --> H["InvalidArgumentException
    をスロー"] E --> I["drivers[] にキャッシュして返す"] G --> I ``` 実際のソースコードを見ると、ドライバー名から `Str::studly()` でメソッド名を組み立てています。 ```php theme={null} // Illuminate\Support\Manager::createDriver() より protected function createDriver($driver) { if (isset($this->customCreators[$driver])) { return $this->callCustomCreator($driver); } $method = 'create'.Str::studly($driver).'Driver'; if (method_exists($this, $method)) { return $this->$method(); } throw new InvalidArgumentException("Driver [$driver] not supported."); } ``` つまり `file` ドライバーなら `createFileDriver()` が、`my-custom` ドライバーなら `createMyCustomDriver()` が呼ばれます。 ### extend() — カスタムドライバーを登録する `extend()` にドライバー名とクロージャを渡すことで、独自のドライバーを登録できます。クロージャの引数はコンテナインスタンスです。 ```php theme={null} use Illuminate\Support\Facades\Session; Session::extend('redis-cluster', function ($app) { return new RedisClusterSessionHandler( $app->make('redis'), $app['config']['session'], ); }); ``` `extend()` はクロージャを `$this` にバインドするため、クロージャ内で Manager のプロパティやメソッドにアクセスできます。 ```php theme={null} // Illuminate\Support\Manager::extend() より public function extend($driver, Closure $callback) { try { $callback = $callback->bindTo($this, static::class) ?? throw new RuntimeException; } catch (Throwable) { $callback = $callback->bindTo(null, static::class); } $this->customCreators[$driver] = $callback; return $this; } ``` ### \_\_call — デフォルトドライバーへの委譲 Manager クラスには `__call` が実装されており、Manager 自身に存在しないメソッド呼び出しはデフォルトドライバーに自動的に委譲されます。 ```php theme={null} public function __call($method, $parameters) { return $this->driver()->$method(...$parameters); } ``` これにより `SessionManager::get('key')` のように Manager を直接使いつつ、実際の処理はドライバーに任せるという流れが実現しています。 ## SessionManager の実装例 `Illuminate\Session\SessionManager` を見ると、`Manager` の使い方が具体的にわかります。 ```php theme={null} namespace Illuminate\Session; use Illuminate\Support\Manager; class SessionManager extends Manager { // 必須: デフォルトドライバーを返す public function getDefaultDriver() { return $this->config->get('session.driver'); } // file ドライバーの生成 protected function createFileDriver() { return $this->createNativeDriver(); } // database ドライバーの生成 protected function createDatabaseDriver() { $table = $this->config->get('session.table'); $lifetime = $this->config->get('session.lifetime'); return $this->buildSession(new DatabaseSessionHandler( $this->getDatabaseConnection(), $table, $lifetime, $this->container )); } // redis ドライバーの生成 protected function createRedisDriver() { $handler = $this->createCacheHandler('redis'); // ... return $this->buildSession($handler); } } ``` ## 自作パッケージで Manager を使う ### 基本的な実装 通知サービスを例に、`Manager` を継承したカスタムクラスを作ります。 ```php theme={null} namespace App\Notifications; use Illuminate\Support\Manager; class NotificationManager extends Manager { public function getDefaultDriver(): string { return $this->config->get('notifications.driver', 'slack'); } protected function createSlackDriver(): SlackNotifier { return new SlackNotifier( $this->config->get('notifications.slack'), ); } protected function createEmailDriver(): EmailNotifier { return new EmailNotifier( $this->config->get('notifications.email'), ); } protected function createLogDriver(): LogNotifier { return new LogNotifier( $this->container->make('log'), ); } } ``` ```php theme={null} namespace App\Providers; use App\Notifications\NotificationManager; use Illuminate\Support\ServiceProvider; class NotificationServiceProvider extends ServiceProvider { public function register(): void { $this->app->singleton(NotificationManager::class, function ($app) { return new NotificationManager($app); }); } } ``` ```php theme={null} // AppServiceProvider::boot() などで $manager = app(NotificationManager::class); $manager->extend('teams', function ($app) { return new TeamsNotifier( $app['config']['notifications.teams'], ); }); ``` ```php theme={null} $manager = app(NotificationManager::class); // デフォルトドライバーを使う $manager->send('メッセージ'); // 特定のドライバーを指定する $manager->driver('email')->send('メッセージ'); // 一度解決したドライバーはキャッシュされる $manager->driver('slack'); // 同じインスタンスが返る ``` ## MultipleInstanceManager `Illuminate\Support\MultipleInstanceManager` は Laravel 10 で追加されたクラスです。`Manager` がドライバーの「種類」を管理するのに対し、`MultipleInstanceManager` は名前付きの「インスタンス」を複数管理できます。 ### Manager との違い | | `Manager` | `MultipleInstanceManager` | | - | - | - | | 管理単位 | ドライバーの種類 (`file`, `redis` など) | 名前付きインスタンス (`mailer1`, `mailer2` など) | | 設定の持ち方 | 1つのデフォルトドライバー | インスタンスごとに設定を持つ | | 主な用途 | セッション、キャッシュなど | メール、ログなど複数接続が必要なもの | | 取得メソッド | `driver()` | `instance()` | ### 必須メソッド `MultipleInstanceManager` を継承する場合は、3つのメソッドを実装する必要があります。 ```php theme={null} // ソースコードより abstract public function getDefaultInstance(); abstract public function setDefaultInstance($name); abstract public function getInstanceConfig($name); ``` `getInstanceConfig()` は名前に対応する設定配列を返します。設定には必ず `driver`(または `$driverKey` で指定したキー)が含まれている必要があります。 ### 解決フロー ```mermaid theme={null} flowchart TD A["instance(name) を呼ぶ"] --> B["name が null なら
    getDefaultInstance() を使用"] B --> C{instances[] に
    キャッシュがあるか?} C -- はい --> D["キャッシュされた
    インスタンスを返す"] C -- いいえ --> E["getInstanceConfig(name) で設定を取得"] E --> F{設定に driver
    キーがあるか?} F -- いいえ --> G["RuntimeException をスロー"] F -- はい --> H{customCreators[]
    に登録があるか?} H -- はい --> I["callCustomCreator(config) で生成"] H -- いいえ --> J["createXxxDriver(config) を呼ぶ"] I --> K["instances[] にキャッシュして返す"] J --> K ``` ### 自作パッケージでの実装例 複数の SMS ゲートウェイに同時対応するパッケージを例にします。 ```php theme={null} namespace App\Sms; use Illuminate\Support\MultipleInstanceManager; class SmsManager extends MultipleInstanceManager { // デフォルトインスタンス名 public function getDefaultInstance(): string { return $this->config->get('sms.default', 'primary'); } public function setDefaultInstance($name): void { $this->config->set('sms.default', $name); } // インスタンス名に対応する設定を返す public function getInstanceConfig($name): array { return $this->config->get("sms.gateways.{$name}"); } // twilio ドライバーの生成 (設定配列が渡される) protected function createTwilioDriver(array $config): TwilioSmsGateway { return new TwilioSmsGateway( $config['account_sid'], $config['auth_token'], ); } // vonage ドライバーの生成 protected function createVonageDriver(array $config): VonageSmsGateway { return new VonageSmsGateway( $config['api_key'], $config['api_secret'], ); } } ``` 対応する設定ファイル: ```php theme={null} // config/sms.php return [ 'default' => 'primary', 'gateways' => [ 'primary' => [ 'driver' => 'twilio', 'account_sid' => env('TWILIO_ACCOUNT_SID'), 'auth_token' => env('TWILIO_AUTH_TOKEN'), ], 'backup' => [ 'driver' => 'vonage', 'api_key' => env('VONAGE_API_KEY'), 'api_secret' => env('VONAGE_API_SECRET'), ], 'marketing' => [ 'driver' => 'twilio', 'account_sid' => env('TWILIO_MARKETING_SID'), 'auth_token' => env('TWILIO_MARKETING_TOKEN'), ], ], ]; ``` 使用時は `instance()` で名前を指定します。 ```php theme={null} $sms = app(SmsManager::class); // デフォルトインスタンス (primary = twilio) を使う $sms->send('+81901234xxxx', '確認コード: 123456'); // 名前でインスタンスを指定する $sms->instance('backup')->send('+81901234xxxx', '確認コード: 123456'); // マーケティング用のインスタンスを使う $sms->instance('marketing')->send('+81901234xxxx', 'キャンペーンのお知らせ'); ``` `MultipleInstanceManager` は `Manager` と同じように `extend()` でカスタムドライバーを追加できます。また `__call` も実装されているため、インスタンス自体に存在しないメソッドはデフォルトインスタンスに委譲されます。 ## ドライバーキャッシュの管理 ### Manager のキャッシュ操作 ```php theme={null} // 全ドライバーを取得 $drivers = $manager->getDrivers(); // 全ドライバーのキャッシュをクリア $manager->forgetDrivers(); ``` ### MultipleInstanceManager のキャッシュ操作 ```php theme={null} // 特定のインスタンスを削除する $manager->forgetInstance('backup'); // デフォルトインスタンスを削除する $manager->forgetInstance(); // 複数のインスタンスを一括削除する $manager->forgetInstance(['primary', 'backup']); // インスタンスを削除してキャッシュも消す $manager->purge('backup'); ``` ## まとめ * `Manager` はドライバーの「種類」を管理する。`createXxxDriver()` メソッドを実装し、`extend()` で拡張する * `MultipleInstanceManager` はドライバーの「インスタンス」を名前ごとに管理する。複数の接続設定が必要なパッケージに適している * `Manager` という名前のクラスがすべて `Illuminate\Support\Manager` を継承しているわけではない。`CacheManager` や `QueueManager` は独自実装 既存クラスに独自メソッドを追加するMacroableトレイトの使い方を学びます。 # LaravelでMCPサーバーを作る Source: https://kawax.biz/jp/advanced/mcp-server laravel/mcp パッケージを使って本番運用できるMCPサーバーを構築する。ツール・リソース・プロンプトの実装から認証・テスト・デプロイまで、実践的な実装知識を解説します。 ## MCPサーバーとは(上級向け解説) **Model Context Protocol(MCP)** は、AIクライアント(Claude、Cursor、GitHub Copilotなど)とアプリケーションが標準化されたプロトコルで通信するための仕様です。MCP には3つの主要なプリミティブがあります。 | プリミティブ | 役割 | 主な用途 | | - | - | - | | **ツール(Tools)** | AIが呼び出せる関数 | データ操作、外部API連携、計算処理 | | **リソース(Resources)** | AIが読み込めるコンテキスト | ドキュメント、設定情報、動的データ | | **プロンプト(Prompts)** | 再利用可能なテンプレート | 定型クエリ、ワークフロー誘導 | LaravelでMCPサーバーを構築する利点は、Eloquent・キャッシュ・認証・バリデーションといったLaravelのエコシステムをそのまま活用できることです。 この上級ガイドでは実践的な実装に踏み込みます。基本的なMCPの概念については [中級: Laravel MCP](/jp/mcp) を参照してください。 ## インストールと初期設定 Composerでパッケージをインストールします。 ```shell theme={null} composer require laravel/mcp ``` `vendor:publish` でMCPサーバーの登録場所となる `routes/ai.php` を生成します。 ```shell theme={null} php artisan vendor:publish --tag=ai-routes ``` Artisanコマンドでサーバークラスを作成します。 ```shell theme={null} php artisan make:mcp-server DatabaseServer ``` 生成された `app/Mcp/Servers/DatabaseServer.php` にツール、リソース、プロンプトを登録します。 `routes/ai.php` でサーバーをルートに登録します。 ```php Webサーバー theme={null} use App\Mcp\Servers\DatabaseServer; use Laravel\Mcp\Facades\Mcp; Mcp::web('/mcp/database', DatabaseServer::class); ``` ```php ローカルサーバー theme={null} use App\Mcp\Servers\DatabaseServer; use Laravel\Mcp\Facades\Mcp; Mcp::local('database', DatabaseServer::class); ``` WebサーバーはHTTP POST経由でアクセスされます。ローカルサーバーはArtisanコマンドとして動作し、CLIベースのAIクライアントとの連携に使います。 ## ツールの実装 ツールはAIクライアントが呼び出せる関数です。Laravelのサービスコンテナ、バリデーション、Eloquentをそのまま使えます。 ### ツールの作成 ```shell theme={null} php artisan make:mcp-tool SearchUsersTool ``` 生成されたクラスに `handle` メソッドと `schema` メソッドを実装します。 ### パラメータ定義(スキーマ) `schema` メソッドで `Illuminate\Contracts\JsonSchema\JsonSchema` ビルダーを使い、受け付けるパラメータを定義します。 ```php theme={null} validate([ 'query' => 'required|string|max:100', 'limit' => 'integer|min:1|max:50', 'role' => 'nullable|string|in:admin,editor,viewer', ], [ 'query.required' => '検索キーワードを指定してください。例: "田中" や "tanaka@example.com"', 'limit.max' => '取得件数は最大50件まで指定できます。', 'role.in' => 'ロールは admin、editor、viewer のいずれかを指定してください。', ]); $users = \App\Models\User::query() ->where(function ($q) use ($validated) { $q->where('name', 'like', "%{$validated['query']}%") ->orWhere('email', 'like', "%{$validated['query']}%"); }) ->when(isset($validated['role']), fn ($q) => $q->where('role', $validated['role'])) ->limit($validated['limit'] ?? 10) ->get(['id', 'name', 'email', 'role']); if ($users->isEmpty()) { return Response::text('条件に一致するユーザーが見つかりませんでした。'); } $result = $users->map(fn ($u) => "ID:{$u->id} {$u->name} <{$u->email}> [{$u->role}]") ->implode("\n"); return Response::text($result); } public function schema(JsonSchema $schema): array { return [ 'query' => $schema->string() ->description('検索キーワード。名前またはメールアドレスで検索します。') ->required(), 'limit' => $schema->integer() ->description('取得件数の上限(デフォルト: 10、最大: 50)。') ->minimum(1) ->maximum(50) ->default(10), 'role' => $schema->string() ->description('絞り込むロール。admin、editor、viewer のいずれか。') ->enum(['admin', 'editor', 'viewer']), ]; } } ``` ### ツールアノテーション MCPプロトコルのアノテーションを使うと、AIクライアントがツールの安全性を判断できます。 ```php theme={null} use Laravel\Mcp\Server\Tools\Annotations\IsIdempotent; use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly; #[IsReadOnly] // 環境を変更しない読み取り専用ツール #[IsIdempotent] // 同じ引数で複数回呼んでも結果が変わらない class SearchUsersTool extends Tool { // ... } ``` | アノテーション | 説明 | | - | - | | `#[IsReadOnly]` | データを変更しない | | `#[IsDestructive]` | 削除など破壊的な操作を行う | | `#[IsIdempotent]` | 同じ引数での再実行が安全 | | `#[IsOpenWorld]` | 外部エンティティと通信する | ### 構造化レスポンス AIクライアントがパースしやすいJSON形式のレスポンスを返す場合は `Response::structured` を使います。 ```php theme={null} return Response::structured([ 'total' => $users->count(), 'users' => $users->map(fn ($u) => [ 'id' => $u->id, 'name' => $u->name, 'email' => $u->email, ])->toArray(), ]); ``` ### ストリーミングレスポンス 長時間かかる処理では Generator を返すことで途中経過をストリームできます。 ```php theme={null} use Generator; use Laravel\Mcp\Request; use Laravel\Mcp\Response; public function handle(Request $request): Generator { $items = \App\Models\Product::all(); $total = $items->count(); foreach ($items as $index => $item) { yield Response::notification('processing/progress', [ 'current' => $index + 1, 'total' => $total, 'name' => $item->name, ]); // 重い処理... yield Response::text("処理完了: {$item->name}"); } } ``` Webサーバーではストリーミングレスポンスが自動的にSSE(Server-Sent Events)ストリームとして送信されます。 ### 条件付き登録 特定の条件を満たすユーザーにのみツールを公開できます。 ```php theme={null} public function shouldRegister(Request $request): bool { return $request?->user()?->hasRole('admin') ?? false; } ``` ## リソースの実装 リソースはAIクライアントがコンテキストとして読み込むデータです。ドキュメント、設定情報、動的データを提供します。 ### 静的リソース ```shell theme={null} php artisan make:mcp-resource AppDocumentationResource ``` ```php theme={null} get('userId'); $user = \App\Models\User::find($userId); if (! $user) { return Response::error("ユーザーID {$userId} が見つかりません。"); } return Response::text(json_encode([ 'id' => $user->id, 'name' => $user->name, 'email' => $user->email, 'created_at' => $user->created_at->toIso8601String(), ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); } } ``` AIクライアントは `app://users/42/profile` のようなURIでリソースを要求し、`{userId}` の値が `$request->get('userId')` で取得できます。 ### リソースアノテーション リソースの優先度やオーディエンスを明示できます。 ```php theme={null} use Laravel\Mcp\Enums\Role; use Laravel\Mcp\Server\Annotations\Audience; use Laravel\Mcp\Server\Annotations\Priority; #[Audience(Role::Assistant)] // AIアシスタント向け #[Priority(0.8)] // 重要度(0.0〜1.0) class AppDocumentationResource extends Resource { // ... } ``` ## プロンプトの実装 プロンプトはAIクライアントが使える再利用可能なテンプレートです。定型的なクエリや複雑なワークフローを標準化します。 ### プロンプトの作成 ```shell theme={null} php artisan make:mcp-prompt DataAnalysisPrompt ``` ```php theme={null} validate([ 'target' => 'required|string|max:200', 'format' => 'nullable|in:summary,detail', ]); $target = $validated['target']; $format = $validated['format'] ?? 'summary'; $instruction = $format === 'detail' ? '数値・トレンド・異常値・推奨アクションを含む詳細なレポートを作成してください。' : '主要な指標と3つの重要な洞察を含む簡潔な要約を作成してください。'; return [ Response::text( "あなたはデータアナリストです。{$instruction}" )->asAssistant(), Response::text( "次のデータを分析してください: {$target}" ), ]; } } ``` `asAssistant()` を使うとメッセージがAIアシスタントの発言として扱われます。システムプロンプトとユーザーメッセージを組み合わせて、AIの振る舞いを細かく制御できます。 ## 認証と認可 ### Sanctumによるトークン認証 最もシンプルな認証方法です。MCPクライアントは `Authorization: Bearer ` ヘッダーを付与します。 ```php theme={null} // routes/ai.php Mcp::web('/mcp/database', DatabaseServer::class) ->middleware('auth:sanctum'); ``` ### OAuth 2.1 による認証 より堅牢な認証にはLaravel Passportを使います。 ```php theme={null} // routes/ai.php use Laravel\Mcp\Facades\Mcp; Mcp::oauthRoutes(); Mcp::web('/mcp/database', DatabaseServer::class) ->middleware('auth:api'); ``` OAuth認証を使う場合は、MCP提供の認可ビューを公開して `AppServiceProvider` に設定します。 ```shell theme={null} php artisan vendor:publish --tag=mcp-views ``` ```php theme={null} // app/Providers/AppServiceProvider.php use Laravel\Passport\Passport; public function boot(): void { Passport::authorizationView(function ($parameters) { return view('mcp.authorize', $parameters); }); } ``` ### カスタムミドルウェアによる認証 独自のAPIトークンを使っている場合は、カスタムミドルウェアで `Authorization` ヘッダーを検証します。 ```php theme={null} // app/Http/Middleware/McpTokenMiddleware.php public function handle($request, Closure $next) { $token = $request->bearerToken(); if (! $token || ! \App\Models\ApiToken::where('token', hash('sha256', $token))->exists()) { return response()->json(['error' => 'Unauthorized'], 401); } return $next($request); } ``` ### ツール内での認可 ツールやリソースの `handle` メソッド内で `$request->user()` を使い、きめ細かい認可チェックができます。 ```php theme={null} public function handle(Request $request): Response { $user = $request->user(); if (! $user?->can('manage-users')) { return Response::error('このツールを使用する権限がありません。管理者にお問い合わせください。'); } // 認可済みの処理を続ける... } ``` `shouldRegister` はツールをリストから非表示にするだけです。ツールが呼び出された際の認可チェックは `handle` メソッド内で必ず行ってください。 ## 実践例: データベース操作ツール Eloquentを使ってデータを検索・作成するツールの完全な実装例です。 ### サーバークラス ```php theme={null} validate([ 'keyword' => 'nullable|string|max:100', 'status' => 'nullable|in:draft,published,archived', 'limit' => 'integer|min:1|max:20', ]); $posts = Post::query() ->when($validated['keyword'] ?? null, fn ($q, $kw) => $q->where('title', 'like', "%{$kw}%") ->orWhere('body', 'like', "%{$kw}%") ) ->when($validated['status'] ?? null, fn ($q, $s) => $q->where('status', $s)) ->latest() ->limit($validated['limit'] ?? 5) ->get(['id', 'title', 'status', 'published_at']); if ($posts->isEmpty()) { return Response::text('条件に一致する記事が見つかりませんでした。'); } return Response::structured([ 'total' => $posts->count(), 'posts' => $posts->map(fn ($p) => [ 'id' => $p->id, 'title' => $p->title, 'status' => $p->status, 'published_at' => $p->published_at?->toIso8601String(), ])->toArray(), ]); } public function schema(JsonSchema $schema): array { return [ 'keyword' => $schema->string() ->description('タイトルまたは本文に含まれるキーワード。'), 'status' => $schema->string() ->description('記事のステータス。') ->enum(['draft', 'published', 'archived']), 'limit' => $schema->integer() ->description('取得件数の上限(デフォルト: 5、最大: 20)。') ->default(5), ]; } } ``` ### 作成ツール(書き込み) ```php theme={null} user(); if (! $user?->can('create-posts')) { return Response::error('記事を作成する権限がありません。'); } $validated = $request->validate([ 'title' => 'required|string|max:255', 'body' => 'required|string', 'tags' => 'nullable|array', 'tags.*' => 'string|max:50', ], [ 'title.required' => '記事のタイトルを指定してください。', 'body.required' => '記事の本文を指定してください。', ]); $post = Post::create([ 'title' => $validated['title'], 'body' => $validated['body'], 'status' => 'draft', 'user_id' => $user->id, ]); if (! empty($validated['tags'])) { $post->syncTags($validated['tags']); } return Response::structured([ 'id' => $post->id, 'title' => $post->title, 'status' => $post->status, 'created_at' => $post->created_at->toIso8601String(), 'message' => '記事を下書きとして作成しました。', ]); } public function schema(JsonSchema $schema): array { return [ 'title' => $schema->string() ->description('記事のタイトル(最大255文字)。') ->required(), 'body' => $schema->string() ->description('記事の本文。Markdownが使用できます。') ->required(), 'tags' => $schema->array() ->description('記事に付けるタグの配列。例: ["Laravel", "PHP"]'), ]; } } ``` ## 実践例: ファイルシステム操作ツール Storageファサードを使ってファイルを操作するツールの実装例です。 ```php theme={null} validate([ 'path' => 'required|string|max:500', 'disk' => 'nullable|string|in:local,public,s3', ]); $disk = $validated['disk'] ?? 'local'; $path = $validated['path']; // ディレクトリトラバーサル攻撃を防ぐ if (str_contains($path, '..')) { return Response::error('無効なパスが指定されました。'); } if (! Storage::disk($disk)->exists($path)) { return Response::error("ファイルが見つかりません: {$path}"); } $mimeType = Storage::disk($disk)->mimeType($path); // テキストファイルのみ内容を返す if (str_starts_with($mimeType, 'text/') || $mimeType === 'application/json') { $content = Storage::disk($disk)->get($path); return Response::text($content); } // 画像ファイルはバイナリで返す if (str_starts_with($mimeType, 'image/')) { return Response::fromStorage($path, disk: $disk); } return Response::error("このファイル形式には対応していません: {$mimeType}"); } public function schema(JsonSchema $schema): array { return [ 'path' => $schema->string() ->description('読み込むファイルのパス。例: "reports/2025-01.csv"') ->required(), 'disk' => $schema->string() ->description('使用するストレージディスク。') ->enum(['local', 'public', 's3']) ->default('local'), ]; } } ``` ファイル操作ツールでは必ずパスのサニタイズを行い、許可されたディレクトリ以外へのアクセスを防いでください。`..` を含むパスは拒否することが重要です。 ## テスト MCPサーバー、ツール、リソース、プロンプトはLaravelの標準テスト機能でユニットテストを書けます。 ### ツールのテスト `Server::tool()` メソッドでツールを直接呼び出してテストします。 ```php Pest theme={null} use App\Mcp\Servers\BlogServer; use App\Mcp\Tools\SearchPostsTool; use App\Models\Post; use App\Models\User; test('記事を検索できる', function () { Post::factory()->create(['title' => 'Laravelの基礎', 'status' => 'published']); Post::factory()->create(['title' => 'PHPの応用', 'status' => 'draft']); $response = BlogServer::tool(SearchPostsTool::class, [ 'keyword' => 'Laravel', 'status' => 'published', ]); $response ->assertOk() ->assertSee('Laravelの基礎'); }); test('権限のないユーザーは記事を作成できない', function () { $user = User::factory()->create(); $response = BlogServer::actingAs($user) ->tool(\App\Mcp\Tools\CreatePostTool::class, [ 'title' => 'テスト記事', 'body' => '本文', ]); $response->assertSee('記事を作成する権限がありません'); }); ``` ```php PHPUnit theme={null} use App\Mcp\Servers\BlogServer; use App\Mcp\Tools\SearchPostsTool; use App\Models\Post; use App\Models\User; public function test_記事を検索できる(): void { Post::factory()->create(['title' => 'Laravelの基礎', 'status' => 'published']); Post::factory()->create(['title' => 'PHPの応用', 'status' => 'draft']); $response = BlogServer::tool(SearchPostsTool::class, [ 'keyword' => 'Laravel', 'status' => 'published', ]); $response ->assertOk() ->assertSee('Laravelの基礎'); } public function test_権限のないユーザーは記事を作成できない(): void { $user = User::factory()->create(); $response = BlogServer::actingAs($user) ->tool(\App\Mcp\Tools\CreatePostTool::class, [ 'title' => 'テスト記事', 'body' => '本文', ]); $response->assertSee('記事を作成する権限がありません'); } ``` ### リソースとプロンプトのテスト ```php theme={null} // リソースのテスト $response = BlogServer::resource(\App\Mcp\Resources\AppDocumentationResource::class); $response->assertOk()->assertSee('API'); // プロンプトのテスト $response = BlogServer::prompt(\App\Mcp\Prompts\DataAnalysisPrompt::class, [ 'target' => '先月の売上', 'format' => 'summary', ]); $response->assertOk()->assertSee('データアナリスト'); ``` ### 主なアサーションメソッド | メソッド | 説明 | | - | - | | `assertOk()` | レスポンスにエラーがないことを確認 | | `assertSee($text)` | レスポンスに指定テキストが含まれることを確認 | | `assertHasErrors()` | レスポンスにエラーがあることを確認 | | `assertHasNoErrors()` | レスポンスにエラーがないことを確認 | | `assertName($name)` | ツール名を確認 | | `assertSentNotification($method, $data)` | 通知が送信されたことを確認 | | `assertNotificationCount($count)` | 通知の送信回数を確認 | ### MCP Inspector を使ったデバッグ インタラクティブなデバッグには MCP Inspector を使います。 ```shell theme={null} # Webサーバーの検査 php artisan mcp:inspector mcp/database # ローカルサーバーの検査 php artisan mcp:inspector database ``` ## デプロイの考慮事項 ### HTTPストリーミングとSSE Webサーバーでストリーミングレスポンス(Generator)を使う場合、サーバーの設定を確認してください。 ```nginx Nginx theme={null} location /mcp { proxy_pass http://127.0.0.1:8000; proxy_buffering off; # SSEにはバッファリングを無効化 proxy_cache off; proxy_read_timeout 3600s; # 長時間接続に対応 proxy_set_header Connection ''; chunked_transfer_encoding on; } ``` ```apache Apache theme={null} # mod_proxy_http を使用する場合 ProxyPass /mcp http://127.0.0.1:8000/mcp ProxyPassReverse /mcp http://127.0.0.1:8000/mcp SetEnv proxy-sendchunks 1 ``` ### Laravel Octane との組み合わせ 高トラフィックのMCPサーバーには Laravel Octane(FrankenPHP または Swoole)の使用を検討してください。リクエストごとのオーバーヘッドが大幅に削減されます。 Octane 使用時はリクエスト間で状態が共有されます。ツール内で静的プロパティやグローバルな状態を使わないよう注意してください。 ### レートリミット `throttle` ミドルウェアでMCPサーバーへのリクエストを制限します。 ```php theme={null} // config/cache.php で専用のレートリミッターを定義 // routes/ai.php Mcp::web('/mcp/database', DatabaseServer::class) ->middleware(['auth:sanctum', 'throttle:60,1']); ``` ### キャッシュ 頻繁に呼び出される読み取り専用ツールにはキャッシュを活用します。 ```php theme={null} public function handle(Request $request): Response { $data = \Illuminate\Support\Facades\Cache::remember( "mcp:search:{$request->get('keyword')}", now()->addMinutes(5), fn () => $this->fetchFromDatabase($request) ); return Response::structured($data); } ``` ### ログとモニタリング MCPツールの呼び出しをログに記録することで、AIクライアントの利用状況を把握できます。 ```php theme={null} public function handle(Request $request): Response { \Illuminate\Support\Facades\Log::info('MCP tool called', [ 'tool' => static::class, 'user' => $request->user()?->id, 'params' => $request->all(), ]); // ... } ``` 本番環境では Laravel Telescope や Sentry を使ってMCPサーバーのパフォーマンスと例外を監視することをお勧めします。 # Mercureブロードキャストドライバーの内部構造 Source: https://kawax.biz/jp/advanced/mercure-broadcasting Laravel framework に追加された Mercure ブロードキャストドライバーの仕組みを、ソースコードを元に解説します。Reverb や Pusher とは異なる HTTP/SSEベースのアーキテクチャ、認可Cookie、E2E暗号化チャンネルまで扱います。 このページで解説する `MercureBroadcaster` は [laravel/framework の PR #61474](https://github.com/laravel/framework/pull/61474)(2026年9月10日マージ)で追加された機能です。執筆時点ではタグ付きリリースにまだ含まれておらず、[公式ドキュメント](https://laravel.com/docs/broadcasting)にも記載がありません。ソースコードとdocblockを元にした先行解説なので、実際に使う場合は`laravel/framework`の変更履歴を確認してください。 ## 概要 Laravelのブロードキャストは長らく [Reverb](/jp/broadcasting)・Pusher・Ably という「WebSocketネイティブ」なドライバーを提供してきましたが、`config/broadcasting.php` の`driver`に新しく`mercure`が加わりました。 [Mercure](https://mercure.rocks) は Server-Sent Events (SSE) をベースにしたリアルタイム通信プロトコルです。WebSocketのような双方向コネクションではなく、HTTP/1.1・HTTP/2上のロングポーリングに近い仕組みで動作するため、以下のような特徴があります。 * 通常のHTTPインフラ(リバースプロキシ、CDN、ロードバランサー)を素通りできる * ブラウザの`EventSource` APIだけでクライアントを実装でき、専用クライアントライブラリなしで動く * [FrankenPHP](/jp/blog/laravel-cloud) には組み込みのMercureハブが搭載されているため、追加のインフラなしで動かせる ```mermaid theme={null} flowchart LR A["サーバー
    broadcast(event)"] --> B["MercureBroadcaster"] B --> C["Mercureハブ
    (Hub / FrankenPhpHub)"] C -->|"SSE (EventSource)"| D["ブラウザ
    Laravel Echo"] D --> E["UIの
    リアルタイム更新"] ``` ## `driver`の追加とサポートされる値 `config/broadcasting.php` の冒頭コメントに追記されたサポートドライバー一覧に`mercure`が加わりました。 ```php theme={null} // Supported: "reverb", "pusher", "ably", "mercure", "redis", "log", "null" ``` 接続設定のサンプルも用意されています。 ```php theme={null} 'mercure' => [ 'driver' => 'mercure', 'url' => env('MERCURE_URL'), 'public_url' => env('MERCURE_PUBLIC_URL'), 'secret' => env('MERCURE_JWT_SECRET'), 'encryption_key' => env('MERCURE_ENCRYPTION_KEY'), 'claims' => [ 'iss' => env('MERCURE_JWT_ISSUER'), 'client_id' => env('APP_NAME'), ], 'cookie_name' => env('MERCURE_COOKIE_NAME'), 'subscribe_expiration' => (int) env('MERCURE_SUBSCRIBE_EXPIRATION', 5), ], ``` `url`を省略した場合、FrankenPHPの組み込みMercureハブ(`mercure_publish()`関数)へ自動フォールバックします。この判定は`CreatesMercureDrivers::mercure()`で行われています。 ```php theme={null} public function mercure(array $config) { if (empty($config['url'])) { return $this->frankenPhpMercure($config); } // ... } ``` 外部にMercureハブを立てている場合は`url`に管理API(publish用)のURL、`public_url`にブラウザから見えるURLを指定します。両者が異なるのは、Docker Composeなど内部ネットワーク経由でpublishし、公開URLだけをブラウザに晒す構成を想定しているためです。 ## トークン発行の仕組み:publish用とsubscribe用を分離 MercureはJWT (JSON Web Token) でアクセス制御を行うプロトコルです。`CreatesMercureDrivers`トレイトは、publish(サーバー→ハブ)とsubscribe(ブラウザ→ハブ)で **別々のトークンファクトリー** を組み立てます。 * `secret` / `publish_secret` / `subscribe_secret` を個別に指定でき、省略時は`secret`にフォールバック * `algorithm` / `publish_algorithm` / `subscribe_algorithm` も同様に個別指定可能(デフォルト`HS256`) * HS256は32バイト、HS384は48バイト、HS512は64バイト以上のシークレット長が必須で、満たさない場合は`InvalidArgumentException`が投げられます ```php theme={null} protected function mercureSecret(array $config, string $side) { $secret = ($config[$side.'_secret'] ?? null) ?: ($config['secret'] ?? null); // ... $minimumLength = ['HS256' => 32, 'HS384' => 48, 'HS512' => 64][$algorithm] ?? 0; if (strlen($secret) < $minimumLength) { throw new InvalidArgumentException(/* ... */); } return $secret; } ``` publishトークンは`Grant::ACTION_PUBLISH`で全トピック(`*`)への発行権限を持ち、`CachingTokenProvider`でメモリキャッシュされます。毎回のブロードキャストごとにJWTを再生成しないための最適化です。 ## チャンネル認可とCookieベースの購読モデル WebSocketドライバーとの最大の違いは、購読側の認可が **単一のCookie** で行われる点です。`MercureBroadcaster::auth()`は`channel_names`配列(1リクエストあたり最大100件)を受け取り、チャンネルごとに認可を判定してから、まとめて1つの認可Cookieを発行します。 ```php theme={null} public function auth($request) { $channelNames = (array) $request->input('channel_names', []); if ($channelNames === [] || count($channelNames) > 100 || $channelNames !== array_filter($channelNames, 'is_string')) { throw new AccessDeniedHttpException; } // チャンネルごとに認可を判定し Grant を積み上げる... return (new JsonResponse([/* ... */])) ->cookie($this->makeAuthorizationCookie($request, $grants, $user)); } ``` ポイントは「1チャンネルの認可が失敗しても、レスポンス全体を失敗させない」設計です。拒否されたチャンネルは`denied: true`としてレスポンスに含まれるだけで、他の許可されたチャンネルの購読は継続します。これは、Mercureが1つのEventSourceで複数トピックを多重化するため、途中でチャンネルを追加・削除しても既存の接続を切断せずに済むようにするためです。 Presenceチャンネルは通常の`private`/`private-encrypted`チャンネルと異なる`Grant`を発行し、購読URLパターン(Mercureの[Subscription API](https://mercure.rocks/docs/hub/concepts/active-subscriptions))に対する`subscribe`権限として組み込まれます。 ## エンドツーエンド暗号化チャンネル `private-encrypted-`で始まるチャンネル名は、Mercureハブ自体にもペイロードを見せない「エンドツーエンド暗号化(E2EE)」チャンネルとして扱われます。これは既存のPusher/Reverbドライバーにはない機能です。 `encryption_key`をbase64エンコードされた32バイトの鍵として設定すると`ChannelEncrypter`が有効になり、`broadcast()`実行時にイベント名・ペイロード・ソケットIDをまとめてJWE(JSON Web Encryption)として暗号化します。ハブが中継するのは暗号化済みのバイト列だけです。 ```php theme={null} 'encryption_key' => env('MERCURE_ENCRYPTION_KEY'), ``` ```shell theme={null} php -r "echo base64_encode(random_bytes(32));" ``` 購読側は`auth()`レスポンスに含まれる`jwk`(JSON Web Key)フィールドを使ってブラウザ側で復号します。鍵交換はMercure仕様が推奨する「認可レスポンスに載せてアウトオブバンドで渡す」方式で、ハブ自体は鍵を一切知りません。 Presenceチャンネルは暗号化の対象外です。メンバー一覧がMercureのSubscription APIを経由してハブに見える仕様のため、E2EEとは両立しません。 ## Whisper(クライアント間の直接メッセージ) `client_events`が有効な場合(デフォルト`true`)、各ガード付きチャンネルには専用の「whisperトピック」が割り当てられ、購読者はそのトピックに対してのみ`publish`権限を持ちます。 ```php theme={null} if ($this->clientEvents && $whisperTopics !== []) { $grants[] = new Grant([Grant::ACTION_SUBSCRIBE, Grant::ACTION_PUBLISH], $whisperTopics); } ``` チャンネル本体のトピックはサーバー専用のまま保たれるため、クライアントが偽装したサーバーイベントを発行することはできません。Pusherの「クライアントイベント」機能に近い設計ですが、トピックを分離することでより明確に権限を分けています。 ## トピックの命名規則 Mercureはハブを複数のアプリケーションで共有できる設計になっており、トピックの衝突を避けるために`topic_prefix`という名前空間を導入しています。デフォルト値は次のとおりです。 ```php theme={null} protected string $topicPrefix = 'https://laravel.alt/echo/', ``` `.alt`はDNS上で予約された未解決ドメイン([RFC 9476](https://www.rfc-editor.org/rfc/rfc9476.html))なので、実在のドメインと衝突する心配がありません。チャンネル名は[RFC 3986](https://www.rfc-editor.org/rfc/rfc3986.html)に従ってURLエンコードされ、単一のパスセグメントに収められます。 ```php theme={null} protected function channelTopic($channelName) { return $this->topicPrefix.'channel/'.rawurlencode($channelName); } ``` ## Cookieドメインとエラーメッセージ Mercureハブがアプリケーションと異なるサブドメインで動く構成もよくあるため(例:`mercure.example.com` と `app.example.com`)、認可Cookieのドメイン解決に失敗すると、原因を特定しやすい例外メッセージが投げられます。 ```php theme={null} throw new BroadcastException(sprintf( 'Mercure error: %s. Adjust the Mercure "public_url" configuration value so the hub [%s] shares a registrable domain with the application host [%s].', rtrim($e->getMessage(), '.'), $this->hub->getPublicUrl(), $request->getHost() ), 0, $e); ``` `__Secure-`や`__Host-`プレフィックス付きのCookie名を使う場合、`public_url`がHTTPSであることも起動時にチェックされます。 ```php theme={null} if (str_starts_with($hub->getCookieName(), '__') && parse_url($hub->getPublicUrl(), PHP_URL_SCHEME) === 'http') { throw new InvalidArgumentException(/* ... */); } ``` ## Reverb・Pusher・Ablyとの使い分け | ドライバー | トランスポート | インフラ | E2E暗号化 | | - | - | - | - | | Reverb | WebSocket | 自前サーバー必須 | なし | | Pusher / Ably | WebSocket | SaaS | なし | | Mercure | SSE (HTTP) | 自前ハブ、または FrankenPHP 組み込み | あり(`private-encrypted-`) | MercureはWebSocketの常時双方向接続が不要なユースケース(通知、進捗表示、チャットのようにサーバー→クライアントの一方向配信が中心の用途)に向いています。FrankenPHPを使っている場合は追加インフラなしで動作するのも利点です。 ## 関連ページ * [ブロードキャストの基礎](/jp/broadcasting) * [Laravel Cloud](/jp/blog/laravel-cloud)(FrankenPHPベースの実行環境) # once() ヘルパーの内部実装 — Once・Onceable・PreventsCircularRecursion Source: https://kawax.biz/jp/advanced/once-helper グローバル関数once()の内部実装をIlluminate\Support\OnceクラスとOnceableクラスから解説し、Eloquentの循環参照防止トレイトPreventsCircularRecursionへの応用も紹介します。 ## once() とは `once()` はコールバックを実行し、その結果をリクエストの間メモリにキャッシュするグローバルヘルパーです。同じ呼び出し箇所から同じコールバックで再度呼ばれると、キャッシュされた結果を返します。 ```php theme={null} function random(): int { return once(function () { return random_int(1, 1000); }); } random(); // 123 random(); // 123 (キャッシュされた結果) random(); // 123 (キャッシュされた結果) ``` オブジェクトインスタンスのメソッド内から呼び出すと、キャッシュはそのインスタンス単位で独立します。 ```php theme={null} class NumberService { public function all(): array { return once(fn () => [1, 2, 3]); } } $service = new NumberService; $service->all(); $service->all(); // (キャッシュされた結果) $secondService = new NumberService; $secondService->all(); $secondService->all(); // (キャッシュされた結果、$serviceとは別のキャッシュ) ``` この「呼び出し箇所ごと・インスタンスごと」にキャッシュが独立する仕組みは、`memoize` のような単純なメモ化ヘルパーとは異なります。仕組みを理解するには `Illuminate\Support\Once` と `Illuminate\Support\Onceable` の2クラスを見る必要があります。 ## helpers.php での呼び出し `Illuminate\Support\helpers.php` の `once()` 関数本体はごくシンプルです。 ```php theme={null} use Illuminate\Support\Once; use Illuminate\Support\Onceable; if (! function_exists('once')) { function once(callable $callback) { $onceable = Onceable::tryFromTrace( debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2), $callback, ); return $onceable ? Once::instance()->value($onceable) : call_user_func($callback); } } ``` ポイントは、`once()` 自体はキャッシュを持たず、`debug_backtrace()` で取得した呼び出し元の情報から `Onceable` インスタンスを組み立てて `Once` クラスに処理を委譲していることです。 ```mermaid theme={null} flowchart TD A["once(callback) を呼ぶ"] --> B["debug_backtrace()で呼び出し元を取得"] B --> C["Onceable::tryFromTrace()"] C --> D{"ハッシュを計算できた?"} D -- いいえ --> E["callback を素通しで実行"] D -- はい --> F["Once::instance()->value(onceable)"] F --> G{"WeakMapにキャッシュ済み?"} G -- はい --> H["キャッシュ値を返す"] G -- いいえ --> I["callback を実行してキャッシュに保存"] ``` ## Onceable — 呼び出し箇所を特定するハッシュ計算 `Onceable` クラスは「どの呼び出し箇所か」を一意に特定するハッシュを、バックトレースから計算する役割を持ちます。 ```php theme={null} class Onceable { public function __construct( public string $hash, public ?object $object, public $callable, ) { // } public static function tryFromTrace(array $trace, callable $callable) { if (! is_null($hash = static::hashFromTrace($trace, $callable))) { $object = static::objectFromTrace($trace); return new static($hash, $object, $callable); } } protected static function objectFromTrace(array $trace) { return $trace[1]['object'] ?? null; } protected static function hashFromTrace(array $trace, callable $callable) { if (str_contains($trace[0]['file'] ?? '', 'eval()\'d code')) { return null; } $uses = array_map( static function (mixed $argument) { if ($argument instanceof HasOnceHash) { return $argument->onceHash(); } if (is_object($argument)) { return spl_object_id($argument); } return $argument; }, $callable instanceof Closure ? (new ReflectionClosure($callable))->getClosureUsedVariables() : [], ); $class = $callable instanceof Closure ? (new ReflectionClosure($callable))->getClosureCalledClass()?->getName() : null; $class ??= $trace[1]['class'] ?? null; return hash('xxh128', sprintf( '%s@%s%s:%s (%s)', $trace[0]['file'], $class ? $class.'@' : '', $trace[1]['function'], $trace[0]['line'], serialize($uses), )); } } ``` ハッシュの元になる情報は、`ファイルパス + クラス名 + 関数名 + 行番号 + クロージャがuseしている変数の値` です。つまり、**同じ行から呼ばれた `once()` でも、`use` している変数の値が変われば別のキャッシュ扱いになります**。 ```php theme={null} function greet(string $name) { return once(fn () => "Hello, {$name}!"); } greet('Alice'); // "Hello, Alice!" を計算してキャッシュ greet('Bob'); // $name の値が違うのでハッシュも変わり、再計算される ``` `eval()` されたコード内から `once()` を呼ぶと `hashFromTrace()` が `null` を返し、キャッシュは一切行われません。Bladeのコンパイル済みビューなどeval経由の実行を想定した安全策です。 `object` は「呼び出し元がインスタンスメソッドかどうか」を示します。`$trace[1]['object']` は `debug_backtrace()` の1階層上(`once()` を呼び出した側)のオブジェクトなので、インスタンスメソッド内なら `$this` が入り、静的メソッドやグローバル関数なら `null` になります。 ## Once — WeakMapによるキャッシュ本体 実際のキャッシュ保持は `Illuminate\Support\Once` が担います。 ```php theme={null} class Once { protected static ?self $instance = null; protected static bool $enabled = true; protected function __construct(protected WeakMap $values) { // } public static function instance() { return static::$instance ??= new static(new WeakMap); } public function value(Onceable $onceable) { if (! static::$enabled) { return call_user_func($onceable->callable); } $object = $onceable->object ?: $this; $hash = $onceable->hash; if (! isset($this->values[$object])) { $this->values[$object] = []; } if (array_key_exists($hash, $this->values[$object])) { return $this->values[$object][$hash]; } return $this->values[$object][$hash] = call_user_func($onceable->callable); } public static function enable() { static::$enabled = true; } public static function disable() { static::$enabled = false; } public static function flush() { static::$instance = null; } } ``` キーとなる技術は `WeakMap` です。`$onceable->object`(呼び出し元インスタンス)をキーにして、値としてハッシュ→結果の連想配列を保持します。`object` が無い場合(グローバル関数や静的メソッド)は `Once` 自身の `$this` がキーになり、事実上プロセス全体で共有される単一のキャッシュ領域になります。 `WeakMap` を使っているため、キーとなるオブジェクトへの参照が他になくなれば、そのオブジェクトに紐づくキャッシュもガベージコレクションの対象になります。`once()` を大量のオブジェクトから呼んでもメモリリークになりにくい設計です。 ### テストでの活用 — enable / disable / flush `Once::disable()` を呼ぶとキャッシュが完全に無効化され、`once()` は毎回コールバックを実行するようになります。テストで「毎回新しい値が欲しい」場合に便利です。 ```php theme={null} use Illuminate\Support\Once; beforeEach(function () { Once::disable(); }); afterEach(function () { Once::enable(); Once::flush(); }); ``` `Once::flush()` はキャッシュ全体を破棄し、次回アクセス時に新しい `WeakMap` を作り直します。Artisanコマンドのテストや、Octaneのようにプロセスが使い回される環境でリクエストをまたいでキャッシュを持ち越したくない場合に使います。 ## PreventsCircularRecursion — Onceableの応用例 `Onceable::tryFromTrace()` は `once()` 専用ではなく、Eloquentの `Illuminate\Database\Eloquent\Concerns\PreventsCircularRecursion` トレイトでも再利用されています。こちらは「同じオブジェクトの同じ呼び出し箇所を、コールスタック内で再入させない」ためのトレイトです。 ```php theme={null} trait PreventsCircularRecursion { protected static $recursionCache; protected function withoutRecursion($callback, $default = null) { $trace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 2); $onceable = Onceable::tryFromTrace($trace, $callback); if (is_null($onceable)) { return call_user_func($callback); } $stack = static::getRecursiveCallStack($this); if (array_key_exists($onceable->hash, $stack)) { return is_callable($stack[$onceable->hash]) ? static::setRecursiveCallValue($this, $onceable->hash, call_user_func($stack[$onceable->hash])) : $stack[$onceable->hash]; } try { static::setRecursiveCallValue($this, $onceable->hash, $default); return call_user_func($onceable->callable); } finally { static::clearRecursiveCallValue($this, $onceable->hash); } } // getRecursiveCallStack() / getRecursionCache() / setRecursiveCallValue() // / clearRecursiveCallValue() はいずれも WeakMap ベースの実装 } ``` `Once` との決定的な違いは、`finally` ブロックで **呼び出し完了後にキャッシュをクリアしている**点です。`Once` は「リクエスト中は永続的にキャッシュする」のに対し、`PreventsCircularRecursion` は「今実行中のコールスタックの間だけキャッシュ(実質的にはロック)する」ために同じ `Onceable` の仕組みを流用しています。 Eloquentモデルでの典型的な使用例は、モデルの `toArray()` やアクセサが自分自身を再帰的に参照してしまうケースの防止です。 ```php theme={null} use Illuminate\Database\Eloquent\Concerns\PreventsCircularRecursion; use Illuminate\Database\Eloquent\Model; class Category extends Model { use PreventsCircularRecursion; public function getPathAttribute(): string { return $this->withoutRecursion(function () { // parentのpathを参照する処理で // 循環参照が発生してもここで無限ループを防ぐ return $this->parent ? $this->parent->path.' > '.$this->name : $this->name; }, default: $this->name); } } ``` ## パッケージ開発での応用 自作パッケージで「同じインスタンスの同じメソッド呼び出しを1リクエスト中は1回だけ実行したい」場合、`once()` をそのまま使うのが最も簡単です。`Onceable`/`Once` を直接使う機会は少ないですが、以下のような場面では内部実装の理解が役立ちます。 * Artisanコマンドやテストで `Once::disable()` / `Once::flush()` を使ってキャッシュ挙動を制御したいとき * 自作トレイトで「呼び出し箇所単位」のキャッシュや再入防止を実装したいとき(`PreventsCircularRecursion` と同じパターンが使えます) * `once()` の結果が想定と異なりデバッグが必要なとき、ハッシュの元になる要素(ファイル・クラス・関数・行・use変数)を理解しておくと原因を特定しやすくなります `once()` はクロージャが `use` している変数までハッシュに含めるため、ループの中で毎回異なる値をキャプチャするクロージャを渡すと、意図せず毎回別キャッシュになり実質的にキャッシュされないことがあります。ループ内で使う場合は、キャッシュキーとして意図した粒度になっているか確認してください。 ## 関連ページ 副作用を挟みつつ値を返すtap()ヘルパーの実装パターン。 Eloquentモデルのイベントとオブザーバーによる一元管理。 # パッケージの診断情報をaboutに追加する Source: https://kawax.biz/jp/advanced/package-about Laravel 13のAboutCommandを使い、パッケージの設定状態をCLIとJSONで表示する方法、評価タイミングと長期保守時の注意点を解説します。 利用者から問い合わせを受けたときに、パッケージの有効・無効や選択中のドライバーを同じ形式で確認できるようにします。`AboutCommand::add()` を使えば、専用コマンドを実装せずに `php artisan about` の出力へパッケージ用のセクションを追加できます。 公式ドキュメントには登録の基本例があります。このページではLaravel 13の実装まで確認し、情報を収集するタイミング、JSONの型、セクション名の衝突、テストでの登録状態を掘り下げます。 ## プロバイダーで表示内容を登録する 次の例は、`courier.enabled` と `courier.driver` がパッケージの設定として登録済みであることを前提にしています。設定の登録方法は[パッケージ設定のマージと更新](/jp/advanced/package-config-merging)を参照してください。 ```php theme={null} app->runningInConsole()) { return; } AboutCommand::add('Acme Courier', fn () => [ 'Enabled' => AboutCommand::format( (bool) config('courier.enabled', false), console: fn (bool $enabled) => $enabled ? 'ENABLED' : 'OFF', ), 'Driver' => (string) config('courier.driver', 'log'), ]); } } ``` `runningInConsole()` はHTTPリクエストで不要な登録を避ける条件です。`about` の実行時だけを判定する条件ではなく、他のArtisanコマンドでも登録されます。ただし、上のクロージャー内の設定取得は登録時には実行されません。 表示する項目は明示的に選びます。APIキー、アクセストークン、認証情報を含む接続URL、設定配列全体は出力しないでください。JSON出力にも自動的な秘密情報のマスクはありません。問い合わせ時はパッケージのセクションだけを共有してもらい、共有前に内容を確認します。 ## 登録と評価のタイミングを分けて考える Laravel `v13.35.0` の `add()` は、その場でデータを収集せず、静的な `$customDataResolvers` に登録用のクロージャーを追加します。`about` の実行時に表示用データを組み立て、登録されたデータ取得用クロージャーを評価します。 ```mermaid theme={null} flowchart TD A["プロバイダーのboot()"] --> B["add()で登録を保存"] B --> C["aboutを実行"] C --> D["標準情報と追加セクションを収集"] D --> E["データ取得用クロージャーを評価"] E --> F["--onlyでセクションを絞る"] F --> G["CLIまたはJSONで表示"] ``` 設定値を `add()` の外側で先に読み込んで配列へ固定するより、クロージャー内で取得するほうがコマンド実行時の状態を反映できます。一方、`--only` のフィルターはデータ取得用クロージャーの評価より**後**です。 ```bash theme={null} php artisan about --only=environment ``` このように標準セクションだけを指定しても、上の `Acme Courier` のクロージャーは評価されます。表示されないことと、処理されないことは別です。 そのため、追加情報は設定値や軽量なローカル状態から取得します。外部APIへの疎通確認、DBへの問い合わせ、ファイルの書き換えなどを入れると、無関係なセクションを確認するコマンドまで遅くなったり失敗したりします。疎通確認や修復は専用のArtisanコマンドへ分けてください。 `runningInConsole()` の条件だけでは、遅延サービスプロバイダーの `boot()` が実行されることは保証されません。診断情報を常に登録したい場合は、その登録を即時読み込みのプロバイダーへ置きます。サービスのバインディングだけを遅延化する構成は[遅延サービスプロバイダー](/jp/advanced/deferred-provider)を参照してください。 ## CLI表示とJSONの型を両立する パッケージの情報だけを確認するには、セクション名を小文字のsnake caseにした値を指定します。`Acme Courier` の場合は `acme_courier` です。 ```bash theme={null} php artisan about --only=acme_courier php artisan about --only=environment,acme_courier php artisan about --only=acme_courier --json ``` 上の登録例で `courier.enabled` が `true`、`courier.driver` が `log` のとき、JSONは次の形になります。 ```json theme={null} { "acme_courier": { "enabled": true, "driver": "log" } } ``` CLIでは `Enabled` が `ENABLED` と表示されます。`AboutCommand::format()` は、CLI用の `console` とJSON用の `json` を指定できるヘルパーです。上の例は `console` だけを指定しているため、JSONでは元のboolean値が返ります。CLIの表示文字列をそのままJSONへ流用する必要はありません。 | 対象 | 実装上の変換・処理 | | - | - | | `--only` の値とセクション名 | 小文字にした後にsnake caseへ変換して比較 | | JSONのセクションキー | セクション名をsnake caseへ変換 | | JSONの項目キー | 項目名を小文字にした後にsnake caseへ変換 | | `format()` の値 | CLIでは `console`、JSONでは `json` を使用。未指定なら元の値 | 例のように通常の英語の単語を空白で区切った名前を使うと、フィルターやJSONキーを扱いやすくなります。自動処理で参照するキーは、表示名の変更でも変わり得るため、リリース時に互換性を確認します。 ## セクションはパッケージ専用の名前にする `add()` は同じセクションへの項目を追記します。同じ名前のセクションを指定したからといって、先に登録した内容全体を置き換えるわけではありません。 他のパッケージと区別できる `Acme Courier` のような名前を選び、Laravel標準の `Environment`、`Cache`、`Drivers`、`Storage` への追加は必要な場合に限定します。 同一セクションに同名の項目を重複登録すると、CLIでは複数行として残り得ますが、JSONでは同じキーに集約され後の値が残ります。また、異なる表記でもsnake caseへの変換後に同じキーになる名前は避けてください。登録場所を1か所にまとめ、CLIとJSONのどちらでも項目が一意になる設計にします。 ## 静的な登録状態をテストで扱う `about` の実行開始時に表示用の `$data` は初期化されますが、追加情報の登録リスト `$customDataResolvers` は保持されます。毎回同じ登録から情報を再収集できる一方、同一PHPプロセスでプロバイダーの `boot()` を繰り返すと登録が累積する可能性があります。 `AboutCommand::flushState()` は**すべてのパッケージの登録**と表示用データを消すメソッドです。本番のプロバイダーで自分のセクションを重複回避するために呼ばないでください。他のパッケージの診断情報まで失われます。 独自のテスト基盤でアプリケーションを再構築する場合は、テスト間で状態を初期化する責任を決めます。初期化するなら対象プロバイダーを起動する前に行い、その後に必要な登録をすべて行います。既存のテスト基盤が状態を初期化しているかも確認してください。 ### リリース前の確認項目 [パッケージのテスト](/jp/advanced/package-testing)と実際の利用アプリケーションで、次の組み合わせを確認します。 | 確認する操作 | 期待する結果 | | - | - | | `about --only=acme_courier` | パッケージのセクションのみを表示し、項目が重複しない | | `about --only=acme_courier --json` | `enabled` はboolean、`driver` はstringとして取得できる | | `about --only=environment` | パッケージの情報収集に外部通信や副作用がなく、正常に終了する | | 有効・無効やドライバーを変更 | 表示値とJSONの値が、コマンド実行時の設定に対応する | | 同じアプリケーションで2回実行 | 項目が累積せず、各実行で情報を再収集する | | 同一PHPプロセスでアプリケーションを再構築 | 登録状態がテスト間に漏れず、他パッケージの情報も欠落しない | | 設定キャッシュがある状態 | 公開設定ファイルの内容ではなく、アプリケーションが使っている設定値を表示する | ## 関連ページ プロバイダーとリソース登録の基本を確認します。 既定値、利用者の上書き、設定キャッシュの関係を確認します。 ## 参照した一次情報 * [Laravel公式ドキュメント: パッケージからaboutへの情報追加](https://github.com/laravel/docs/blob/13.x/packages.md#about-artisan-command) * [Laravel公式ドキュメント: aboutと--only](https://github.com/laravel/docs/blob/13.x/configuration.md#the-about-command) * [Laravel Framework v13.35.0: AboutCommand](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Foundation/Console/AboutCommand.php) — 登録、評価順序、キー変換、`format()`、`flushState()`。 * [Laravel Framework v13.35.0: formatのテスト](https://github.com/laravel/framework/blob/v13.35.0/tests/Foundation/Console/AboutCommandTest.php) * [Laravel Framework v13.35.0: JSON出力の統合テスト](https://github.com/laravel/framework/blob/v13.35.0/tests/Integration/Foundation/Console/AboutCommandTest.php) # パッケージの公開アセットと更新 Source: https://kawax.biz/jp/advanced/package-assets Laravel 13の公開処理を読み解き、JavaScript・CSSの配布、タグの選択範囲、上書きオプション、Composer更新時の再公開を長期保守の観点で解説します。 パッケージのJavaScriptやCSSを更新しても、アプリケーションの `public` にコピー済みのファイルは自動では変わりません。PHPコードだけが新版になり、ブラウザーが旧版のアセットを使い続ける状態を防ぐには、公開先の所有者と更新手順を決める必要があります。 このページでは[パッケージ開発の基礎](/jp/advanced/package-development)を前提に、Laravel 13の公開処理から配布と保守の設計を整理します。実装の確認には `laravel/framework` の `v13.35.0` を使用しています。 ## 公開はビルドでも同期でもない `ServiceProvider::publishes()` はコピー元とコピー先を登録します。実際にファイルをコピーするのは `vendor:publish` です。JavaScriptのトランスパイルやCSSのビルド、アプリケーションのViteエントリへの追加は行いません。 パッケージ側でビルド済みのファイルを配布する場合、たとえば次の構成にします。 ```text theme={null} courier/ ├── public/ │ ├── courier.css │ └── courier.js └── src/ └── CourierServiceProvider.php ``` ```php theme={null} app->runningInConsole()) { $this->publishes([ __DIR__.'/../public' => public_path('vendor/courier'), ], 'courier-assets'); } } } ``` 初回の公開では、プロバイダーとタグを明示します。 ```bash theme={null} php artisan vendor:publish --provider="Acme\Courier\CourierServiceProvider" --tag=courier-assets ``` この例では `public/vendor/courier/courier.css` と `courier.js` が作られます。通常のCSSとJavaScriptとして配布する設計なら、Bladeから次のように参照できます。 ```blade theme={null} ``` ES modulesとして配布する場合などは、配布形式に合わせて読み込み方を変更します。`asset()` はURLを生成するヘルパーであり、ビルドや公開、内容に応じたファイル名の生成をするものではありません。 コピー元には、公開してよいビルド成果物だけを置いてください。この例の公開先はWebからアクセスできる `public` です。設定ファイルや内部データを同じ公開グループに含めないでください。 ## タグはプロバイダー専用の名前空間ではない `ServiceProvider` は公開パスをプロバイダークラス別の配列と、タグ別の配列に登録します。タグ別の配列は複数のプロバイダーで共有されるため、`public` のような汎用タグを使うと他のパッケージも対象になり得ます。 | 指定 | 選ばれる公開パス | | - | - | | `--tag=courier-assets` | そのタグを登録したすべてのプロバイダーのパス | | `--provider="Acme\Courier\CourierServiceProvider"` | そのプロバイダーが登録したすべてのパス | | プロバイダーとタグの両方 | そのプロバイダーのパスと、そのタグのパスの共通部分 | | `--all` | すべてのプロバイダーの公開パス | 両方を指定したとき、`pathsForProviderAndGroup()` は**コピー元パスをキーとして** `array_intersect_key()` を使います。タグによって別のコピー先へ切り替える仕組みではありません。同じコピー元を何度も登録して用途別のコピー先を持たせる設計は避けてください。 `--tag` は繰り返し指定できます。その場合は各タグを順に公開します。`--all` は選択処理の冒頭で戻るため、`--provider` や `--tag` を同時に付けても絞り込みには使われません。 利用者の設定やビューまで上書きしないよう、更新手順ではパッケージ固有のアセットタグを使います。プロバイダーだけを指定して `--force` を付けると、同じプロバイダーの設定やビューも対象になり得ます。 ## 再公開オプションを使い分ける `VendorPublishCommand` のファイル公開とディレクトリ公開は、コピー先ファイルの存在とオプションを使ってコピーを判断します。次の表は、コピー元に存在する通常のアセットファイルについての動作です。 | オプション | コピー先に存在しないファイル | コピー先に存在するファイル | | - | - | - | | なし | 追加する | 保持する | | `--force` | 追加する | 上書きする | | `--existing` | 追加しない | 上書きする | | `--existing --force` | 追加しない | 上書きする | `--existing` は編集を保護するオプションではありません。既存ファイルを上書きする一方、新版で追加されたファイルは公開しません。JavaScriptが新しい追加ファイルを必要とする更新では、`--existing` だけでは成果物がそろわない可能性があります。 パッケージが公開先を管理し、利用者が直接編集しない契約なら、更新後に次を実行します。 ```bash theme={null} php artisan vendor:publish --provider="Acme\Courier\CourierServiceProvider" --tag=courier-assets --force ``` `--force` は差分をマージせず、利用者の編集も上書きします。利用者がカスタマイズするCSSは別ファイルとして読み込むなど、パッケージ管理の成果物と分離してください。設定やビューのカスタマイズとは更新方針を分けます。 ### 削除したファイルは公開先に残る ディレクトリ公開の `moveManagedFiles()` は、コピー元にあるファイルを走査して書き込みます。公開先にしかないファイルを探して削除する処理はありません。`--force` もディレクトリの完全同期にはなりません。 たとえば新版で `legacy.js` を削除しても、旧版を公開済みなら `public/vendor/courier/legacy.js` は残ります。改名した場合も旧名のファイルは残るため、リリースノートには削除・改名したファイルと参照先の変更を記録します。 旧ファイルを除去する手順を提供する場合は、パッケージが所有するファイルを具体的に示してください。利用者の独自ファイルが置かれている可能性のあるディレクトリを丸ごと削除する手順にはしません。 ## laravel-assetsへの参加は上書き契約を意味する Laravel 13の公式アプリケーション雛形には、`composer.json` の `post-update-cmd` に次のスクリプトがあります。 ```json theme={null} { "scripts": { "post-update-cmd": [ "@php artisan vendor:publish --tag=laravel-assets --ansi --force" ] } } ``` これはアプリケーション側のスクリプトです。パッケージの自動検出そのものが公開ファイルを更新しているわけではありません。既存アプリケーションではスクリプトが変更・削除されている場合もあるため、利用側の設定を確認します。 この更新経路へ参加する場合は、先ほどの `publishes()` の第2引数を配列に変更して、同じアセットを2つのタグへ登録します。 ```php theme={null} $this->publishes([ __DIR__.'/../public' => public_path('vendor/courier'), ], ['courier-assets', 'laravel-assets']); ``` `laravel-assets` は特別なコピー処理を持つタグではありません。雛形のスクリプトがそのタグを `--force` 付きで公開するため、参加したファイルはComposer更新時の上書き対象になります。利用者が編集する設定やビューは登録しないでください。 自動更新の前提は、アプリケーション側にスクリプトがあり、そのイベントが実行され、プロバイダーが公開パスを登録していることです。この前提を満たさないデプロイでも更新できるよう、パッケージ固有のタグを使った再公開コマンドを案内します。 ## デプロイでPHPとアセットの版をそろえる ```mermaid theme={null} flowchart TD A["パッケージを更新"] --> B["公開パスを登録"] B --> C["固有タグでvendor:publish --force
    またはlaravel-assetsの更新スクリプト"] C --> D["新しいファイルを追加
    既存ファイルを上書き"] D --> E["指定された旧ファイルの整理
    ブラウザー・CDNのキャッシュ方針を適用"] E --> F["PHPとアセットが同じ版で動くことを確認"] ``` `config:cache` や `view:cache` は公開済みのJavaScript・CSSを書き換えません。公開後も同じURLで配信する設計なら、ブラウザーやCDNのキャッシュによって古い内容が使われることがあります。配布物のバージョンを反映したURLやキャッシュ無効化など、アプリケーションの配信方針も更新手順に含めます。 リリースごとに次の組み合わせを確認します。 * 未公開のアプリケーションで、必要なビルド済みファイルがすべて公開される。 * 旧版を公開済みの状態で、`--force` によって既存ファイルが更新され、新しいファイルが追加される。 * アセット更新が利用者の設定・ビュー・独自CSSを上書きしない。 * 削除・改名したファイルの扱いが明示され、旧版の参照が残っていない。 * 実際の配信URLで新版の内容が届き、PHPとブラウザー側の処理が連携する。 ## 関連ページ Composerの更新、プロバイダーの検出、ファイル公開の違いを確認します。 利用者がカスタマイズするテンプレートの保守方針を確認します。 ファイル公開とは別に管理するパッケージキャッシュを解説します。 公開先や配布形式の変更を互換性の契約として扱います。 ## 参照した一次情報 * [Laravel公式ドキュメント: Public assets](https://github.com/laravel/docs/blob/13.x/packages.md#public-assets) * [Laravel公式ドキュメント: Publishing file groups](https://github.com/laravel/docs/blob/13.x/packages.md#publishing-file-groups) * [Laravel Framework v13.35.0: ServiceProvider](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Support/ServiceProvider.php) — `publishes()`、`addPublishGroup()`、`pathsToPublish()`、`pathsForProviderAndGroup()`。 * [Laravel Framework v13.35.0: VendorPublishCommand](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Foundation/Console/VendorPublishCommand.php) — 選択範囲、上書き条件、ディレクトリ内のコピー処理。 * [Laravel 13公式アプリケーション雛形: composer.json](https://github.com/laravel/laravel/blob/13.x/composer.json) — `post-update-cmd` による `laravel-assets` の再公開。 # パッケージのCHANGELOGとリリース管理 Source: https://kawax.biz/jp/advanced/package-changelog Laravelパッケージの変更履歴管理からGitHubリリースの自動化まで、長期メンテナンスに必要なリリース管理の実践を解説します。 あなたがLaravelパッケージを長く保守するなら、変更履歴とリリース手順を先に整備してください。リリース管理を仕組み化すると、破壊的変更の周知漏れとリリース作業の属人化を防げます。 このページは[Laravelパッケージ開発](/jp/advanced/package-development)の姉妹ページです。Laravel/PHP互換性の戦略は[パッケージのバージョン互換性管理](/jp/advanced/package-versioning)を参照してください。 ## CHANGELOG.md の書き方 CHANGELOGは「何が、いつ、どのバージョンで変わったか」を利用者が確認する一次情報です。フォーマットは[Keep a Changelog](https://keepachangelog.com/ja/1.1.0/)を採用すると、カテゴリ構成をチームで統一できます。 ### 基本ルール * `## [x.y.z] - YYYY-MM-DD` 形式でバージョンを見出しにする * `Added / Changed / Deprecated / Removed / Fixed / Security` を使う * Compareリンクを末尾に置いて差分を追えるようにする * まだ未リリースの変更は `## [Unreleased]` に積む ```markdown theme={null} # CHANGELOG All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ### Added - Add `Package::warmCache()` for preloading metadata. ## [2.1.0] - 2026-05-10 ### Added - Add Laravel 13 support. ### Changed - Improve default cache key generation for tagged cache stores. ### Deprecated - Deprecate `Package::legacyHandle()` and schedule removal in v3.0. ### Fixed - Fix null handling in `Package::resolveTenant()`. ## [2.0.0] - 2026-03-01 ### Removed - Drop Laravel 11 support. ### Security - Harden signed URL validation against malformed host headers. [Unreleased]: https://github.com/vendor/package/compare/v2.1.0...HEAD [2.1.0]: https://github.com/vendor/package/compare/v2.0.0...v2.1.0 [2.0.0]: https://github.com/vendor/package/releases/tag/v2.0.0 ``` リリースノートはCHANGELOGの該当バージョンをそのまま使ってください。履歴の情報源を1つに集約すると、README・GitHub Releases・SNS告知で内容がずれにくくなります。 ## セマンティックバージョニング(SemVer) [Semantic Versioning](https://semver.org/spec/v2.0.0.html)では `MAJOR.MINOR.PATCH` を次の基準で使います。 * **MAJOR**: 後方互換性を壊す変更(Breaking change) * **MINOR**: 後方互換性を保った機能追加 * **PATCH**: 後方互換性を保ったバグ修正 ### Laravelパッケージでの判断例 Laravel 13対応は、変更内容で扱いが変わります。 | 変更 | 推奨バージョン | | - | - | | 既存Laravel 12対応を維持しつつ `^13.0` を追加 | MINOR | | Laravel 11/12サポートを打ち切って `^13.0` のみにする | MAJOR | | Laravel 13でのみ発生する不具合修正 | PATCH | Laravel本体のアップグレード内容は[Upgrade Guide](https://laravel.com/docs/13.x/upgrade)で必ず確認してください。最新版のリリース状況は[laravel/framework releases](https://github.com/laravel/framework/releases)で確認できます。あなたのパッケージが利用するAPIにBreaking changeがある場合、互換性ポリシーの再設計が必要です。 PHP最低要件の引き上げや公開APIの削除は、利用者から見るとBreaking changeです。Laravelメジャー対応の作業と同時でも、MAJORリリースとして扱ってください。 ## GitタグとGitHub Releases まずGitタグを打ち、タグをoriginにpushします。 ```bash theme={null} git tag v2.1.0 git push origin v2.1.0 ``` 次にGitHubのRelease作成画面で、タグ `v2.1.0` を選択して公開します。本文にはCHANGELOGの `## [2.1.0]` セクションを貼り付けてください。 テストがすべて通っている状態で `main` にマージしてください。 `vX.Y.Z` 形式のタグを作成し、`origin` へpushします。 タイトルはタグ名、本文はCHANGELOGの該当項目を使います。 ## GitHub Actions による自動リリース `push: tags:` をトリガーにすると、タグ作成を起点にGitHub Releasesを自動公開できます。`softprops/action-gh-release` を使うと、`CHANGELOG.md` の内容を直接本文に流用できます。 ```mermaid theme={null} flowchart LR A["Merge to main"] --> B["Create tag
    vX.Y.Z"] B --> C["GitHub Actions
    on push tags"] C --> D["Run test job"] D --> E["Publish GitHub Release"] ``` ```yaml theme={null} name: release on: push: tags: - "v*.*.*" permissions: contents: write jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: "8.3" - run: composer install --no-interaction --prefer-dist - run: vendor/bin/pest release: needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Publish GitHub Release uses: softprops/action-gh-release@v2 with: generate_release_notes: true ``` `release` ジョブに `needs: test` を設定すると、テスト失敗時に公開を止められます。手動リリースでも自動リリースでも、このゲートは必ず維持してください。 ## Breaking changes の扱い方 Breaking changeは段階的に移行できる設計にしてください。まず非推奨化し、次のMAJORで削除すると、利用者の移行コストを下げられます。 ### 1. 非推奨化をコードで示す ```php theme={null} handle($payload); } public function handle(array $payload): array { return $payload; } } ``` ### 2. 移行ガイドをドキュメント化する `UPGRADE.md` または専用ページで、あなたが利用者に実施してほしい変更を手順化してください。 ```markdown theme={null} ## Upgrading from v2 to v3 - Replace `legacyHandle()` with `handle()`. - Update PHP to 8.3+. - Update Laravel constraint to `^13.0`. ``` ### 3. メジャーバージョン間の移行ノートを残す MAJORを上げるときは、CHANGELOGの `Removed` と移行ガイドを相互リンクしてください。利用者は「何が削除されたか」と「どう直すか」を1回で追えます。 ## 関連ページ サービスプロバイダーを中心にした実装の基礎を確認します。 Laravel/PHP互換性とSemVer運用の判断基準を整理します。 リリース前に必要なテスト戦略と実装方法を確認します。 # パッケージのArtisanコマンド登録と依存解決 Source: https://kawax.biz/jp/advanced/package-commands Laravel 13のcommands()からArtisan起動までを読み解き、コマンドの生成と実行を分離する設計、依存注入、遅延プロバイダーと長期保守の注意点を解説します。 パッケージに診断・同期・インストール用のArtisanコマンドを追加したいときは、コマンドの登録、インスタンスの生成、処理の実行を分けて考えます。実行時だけ必要なサービスをコンストラクターで解決すると、無関係なArtisanコマンドの起動まで巻き込むことがあります。 このページではLaravel Framework `v13.35.0` の実装を確認し、[パッケージ開発の基礎](/jp/advanced/package-development)にある `commands()` の登録例を掘り下げます。ファイル生成の実装は[GeneratorCommand](/jp/advanced/generator-command)、最適化タスクへの参加は[optimizeへの統合](/jp/advanced/package-optimization)を参照してください。 ## プロバイダーから明示的に登録する パッケージ内のコマンドは、サービスプロバイダーの `boot()` でクラス名を登録します。アプリケーションの `app/Console/Commands` に対する自動検出と、パッケージが提供するクラスの登録は別の入口です。 ```php theme={null} app->runningInConsole()) { $this->commands([ StatusCommand::class, ]); } } } ``` この例は、プロバイダーが[パッケージの自動検出](/jp/advanced/package-discovery)またはアプリケーション側の明示登録で読み込まれることを前提にしています。コマンドクラスを置くだけでは、パッケージのプロバイダーまで自動的に登録されるわけではありません。 `runningInConsole()` はコンソール環境の判定であり、特定のコマンドを実行しているかの判定ではありません。HTTPリクエスト内から `Artisan::call()` する用途も提供するなら、この条件により登録されない可能性を考慮し、無条件登録を含めて利用契約を決めます。プログラムからの実行方法は[Artisan](/jp/artisan)を参照してください。 ## commands()はその場でコマンドを実行しない `ServiceProvider::commands()` は、受け取ったクラス名を使うクロージャーを `Illuminate\Console\Application::starting()` に登録します。この時点では、コマンドの `handle()` を呼びません。 通常のConsole Kernelでは、後でArtisan Applicationが作られると、登録済みの起動コールバックが実行されます。そこで `resolveCommands()` が各クラスを `resolve()` に渡し、コマンドの登録を進めます。 ```mermaid theme={null} flowchart TD A["プロバイダーのboot()"] --> B["commands()で起動コールバックを登録"] B --> C["Console KernelがArtisan Applicationを生成"] C --> D["resolveCommands()でクラスを登録"] D --> E["通常のクラスはコンテナでインスタンス化"] E --> F["選択されたコマンドを実行"] F --> G["handle()の依存を解決して呼び出す"] ``` ここで重要なのは、**コマンドのインスタンス生成は、そのコマンドの実行と同じタイミングとは限らない**ことです。後述の専用の遅延登録経路を使わない通常のクラスでは、`resolve()` がコンテナの `make()` を呼ぶため、登録時にコンストラクターとその依存関係が解決されます。 したがって、`php artisan list` や別のコマンドを実行するだけでも、パッケージコマンドのコンストラクターが動くことがあります。`commands()` を `boot()` に置くこと自体は、実行時までインスタンス生成を遅らせる保証にはなりません。 ## 実行時の依存はhandle()に注入する 例として、パッケージ設定の有効・無効だけを表示するコマンドを作ります。Laravel 13の公式Artisanドキュメントと同じく、名前と説明を `Signature`・`Description` アトリビュートで定義します。 ```php theme={null} get('courier.enabled', false); $this->line($enabled ? 'Courier: enabled' : 'Courier: disabled'); return self::SUCCESS; } } ``` この例ではコンストラクターを追加せず、設定Repositoryを `handle()` の引数として受け取ります。コマンドの実行経路で `Command::execute()` がコンテナの `call()` を使うため、型指定した依存関係が注入されます。 外部APIクライアントや同期サービスを使う場合も、実行時だけ必要なら同じ位置に注入できます。ただし、サービス自身の生成や別プロバイダーの `boot()` に副作用があれば、それまで自動的に防げるわけではありません。 | 置き場所 | 通常のコマンドで評価されるタイミング | 向いている処理 | | - | - | - | | プロバイダーの `boot()` | プロバイダーの起動時 | コマンドクラスの登録 | | コマンドのコンストラクター | インスタンス生成時。実行前とは限らない | 軽量な初期化 | | `handle()` の型指定引数 | コマンドの実行時 | 実行に必要なサービスの解決 | | `handle()` の本体 | コマンドの実行時 | 入力の検証、業務処理、結果表示 | コンストラクターでは、外部APIへの接続、データベースの更新、公開ファイルの上書きなどを行わないでください。パッケージの利用者は、インストール直後や接続設定が未完成の状態でも `list`・`help` を使える必要があります。 コンストラクターを独自に定義する場合は、`parent::__construct()` も呼びます。Laravelの親コンストラクターがアトリビュート、signature、descriptionなどからコマンドを構成しているためです。 ## Signatureと遅延登録は別の仕組み 確認した `Application::resolve()` には、Symfonyの `AsCommand` アトリビュートの `name` を読み、名前とクラスの対応をcommand mapへ保存する分岐があります。この分岐では、その場でコンテナの `make()` を呼びません。Console Kernelは後で `setContainerCommandLoader()` を呼び、このmapを使うローダーを設定します。 一方、上の例に使ったLaravelの `Signature` アトリビュートは、この `AsCommand` 判定とは別です。**`Signature` を付けただけで、そのクラスがこの遅延登録経路に入るわけではありません。** この違いは内部実装を読む際の注意点として扱い、まずは公式のコマンド定義と `handle()` への依存注入で、生成コストと副作用を小さくします。遅延ローダーがある場合も、一覧・ヘルプなどによってコマンド情報が必要になる可能性があるため、「選択したコマンドの実行時以外は絶対に生成されない」とは設計しません。 ## コマンドの登録まで遅延させない サービスのバインディングを遅延読み込みすることと、Artisanへコマンドを知らせることは別です。プロバイダーが `DeferrableProvider` を実装していると、対象サービスが必要になるまで `boot()` 自体が実行されない場合があります。 コマンド登録だけをその `boot()` に置き、Artisan起動までにプロバイダーが読み込まれなければ、コマンドの存在を知らせる起動コールバックも登録されません。`handle()` への依存注入に移したサービスが遅延プロバイダーを読み込むとしても、Artisanがコマンドを見つける前の問題は解決しません。 コマンドの公開を確実にする構成として、即時読み込みのプロバイダーへ登録を置き、重いサービスのバインディングだけを別の遅延プロバイダーへ分離できます。具体的な要件は[遅延サービスプロバイダー](/jp/advanced/deferred-provider)を参照してください。 ## 更新時に確認すること コマンド名、引数、オプション、出力、終了コードは、利用者のシェルスクリプトやCIから使われる公開インターフェースです。クラス内部の変更と同じ感覚で変更せず、互換性と移行手順を確認します。 | ケース | 確認すること | | - | - | | クリーンなアプリケーションへ導入 | プロバイダーが読み込まれ、コマンド名が見つかる | | `php artisan list` | パッケージの実行処理や外部接続が始まらない | | `php artisan help courier:status` | 説明と入力仕様を確認できる | | 実際のコマンド実行 | `handle()` の依存が解決され、意図した終了コードになる | | 設定が未公開・未設定 | 一覧・ヘルプが壊れず、実行時の不足設定を適切に扱える | | パッケージやLaravelの更新 | 既存のコマンド名・引数・オプションが引き続き使える | 上の `StatusCommand` なら、プロバイダーを読み込んだテスト環境で次のように出力と終了コードを検証できます。テスト基盤は[パッケージのテスト](/jp/advanced/package-testing)、assertionの詳細は[Console Tests](/jp/console-tests)を参照してください。 ```php theme={null} public function test_status_command_displays_enabled_state(): void { config()->set('courier.enabled', true); $this->artisan('courier:status') ->expectsOutput('Courier: enabled') ->assertExitCode(0); } ``` これに加え、重いサービスを使うコマンドでは、一覧やヘルプの実行時にサービスの業務メソッドが呼ばれないことも検証します。登録できることだけを確認しても、無関係なコマンドへの影響までは検出できません。 ## 参照した一次情報 * [Laravel公式ドキュメント: パッケージのコマンド登録](https://github.com/laravel/docs/blob/13.x/packages.md#commands) * [Laravel公式ドキュメント: コマンド構造と依存注入](https://github.com/laravel/docs/blob/13.x/artisan.md#command-structure) * [ServiceProvider: commands()](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Support/ServiceProvider.php) * [Console Application: starting()・bootstrap()・resolve()](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Console/Application.php) * [Console Kernel: getArtisan()とローダー登録](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Foundation/Console/Kernel.php) * [Command: コンストラクターとexecute()](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Console/Command.php) * [公式テスト: コマンド解決とAsCommandによる別名登録](https://github.com/laravel/framework/blob/v13.35.0/tests/Console/ConsoleApplicationTest.php) # パッケージ設定のマージとキャッシュ Source: https://kawax.biz/jp/advanced/package-config-merging Laravel 13のServiceProvider実装から、浅いマージと再帰的な置換の違い、数値キー配列の落とし穴、設定キャッシュを考慮したパッケージの保守方法を解説します。 パッケージの設定を追加しても、利用者が以前に公開した設定ファイルには新しい項目が自動で書き込まれるわけではありません。公開済みの設定を壊さずにデフォルト値を補うには、配列のマージ方針と設定キャッシュの両方を設計する必要があります。 このページではLaravel 13の `ServiceProvider` を読み、設定をパッケージの公開APIとして保守する方法を整理します。[パッケージ開発の基礎](/jp/advanced/package-development)を前提とし、実装の確認には `laravel/framework` の `v13.34.0` を使用しています。 ## 公開とマージは別の処理 `publishes()` はコピー元とコピー先を登録します。`vendor:publish` がファイルをコピーするまで、利用者の `config` ディレクトリは変わりません。一方、`mergeConfigFrom()` は起動時の設定リポジトリを更新し、ファイル自体を書き換えません。 ```php theme={null} mergeConfigFrom( __DIR__.'/../config/courier.php', 'courier' ); } public function boot(): void { $this->publishes([ __DIR__.'/../config/courier.php' => config_path('courier.php'), ], 'courier-config'); } } ``` 利用者は必要なときだけ設定ファイルを公開します。公開しなくても、キャッシュがない通常の起動では `register()` のマージでデフォルト値が使われます。 ```bash theme={null} php artisan vendor:publish --tag=courier-config ``` 更新手順として設定ファイルを `--force` で再公開すると、利用者の編集内容を上書きします。新しい設定項目を追加するだけなら、デフォルト値の補完と変更点の案内を優先してください。 ## mergeConfigFrom()は最上位だけをマージする `ServiceProvider::mergeConfigFrom()` は、パッケージの設定を先に、アプリケーションの既存設定を後に渡して `array_merge()` を実行します。同じ文字列キーでは、アプリケーション側の値が優先されます。 以下はフレームワークのマージ処理を配列だけで再現した例です。 ```php theme={null} true, 'transport' => [ 'timeout' => 10, 'retries' => 3, ], ]; $overrides = [ 'transport' => [ 'timeout' => 30, ], ]; $config = array_merge($defaults, $overrides); var_export($config); ``` ```text theme={null} array ( 'enabled' => true, 'transport' => array ( 'timeout' => 30, ), ) ``` 最上位の `enabled` は補われますが、`transport` は配列全体が置き換わります。`transport.retries` は残りません。利用者が古い `transport` 配列を公開済みなら、新しいキーを同じ配列に追加しても補完されない点が重要です。 ## replaceConfigRecursivelyFrom()でネストした設定を補う Laravel 13の `ServiceProvider` には、protectedメソッドの `replaceConfigRecursivelyFrom()` もあります。こちらは同じ順序で `array_replace_recursive()` を実行します。 ネストした文字列キーを個別に上書きする設定APIにしたい場合は、プロバイダーの `register()` を次のように変更します。同じ設定キーに対して、先ほどの `mergeConfigFrom()` と併用する必要はありません。 ```php theme={null} public function register(): void { $this->replaceConfigRecursivelyFrom( __DIR__.'/../config/courier.php', 'courier' ); } ``` 先ほどの `$defaults` と `$overrides` を使うと、結果は次のようになります。 ```php theme={null} $config = array_replace_recursive($defaults, $overrides); var_export($config); ``` ```text theme={null} array ( 'enabled' => true, 'transport' => array ( 'timeout' => 30, 'retries' => 3, ), ) ``` | 設定の契約 | 選ぶ処理 | 注意点 | | - | - | - | | ネストした配列は利用者が全体を指定する | `mergeConfigFrom()` | 配列内の未指定キーは補われない | | ネストした項目は利用者が一部だけ指定する | `replaceConfigRecursivelyFrom()` | 数値キーのリストも再帰的な置換の対象になる | `replaceConfigRecursivelyFrom()` は、このページで確認したLaravel 13のソースコードに存在するメソッドです。公式のパッケージ開発ドキュメントが紹介する `mergeConfigFrom()` と区別し、対応するフレームワークの実装を確認して利用してください。 ### 数値キーのリストは全体置換にならない 再帰的な置換は「配列をすべて利用者の値に差し替える」処理ではありません。数値キーでも同じキーの値を置き換え、利用者が指定しなかったキーは残します。 ```php theme={null} ['mail', 'database']]; $overrides = ['channels' => ['slack']]; $config = array_replace_recursive($defaults, $overrides); var_export($config['channels']); ``` ```text theme={null} array ( 0 => 'slack', 1 => 'database', ) ``` 利用者が `['slack']` だけを指定しても、`database` が残ります。また、`['channels' => []]` を渡してもデフォルトのリストは空になりません。通知先やミドルウェアなど、リスト全体の指定に意味がある設定では特に注意してください。 このような設定を持つ場合は、リストと部分上書き可能な連想配列を別の最上位キーに分けて浅いマージを使うなど、設定の構造から検討します。すでに公開したパッケージでマージ方式を変更すると、同じ設定ファイルでも挙動が変わるため、単なる実装の置き換えとして扱わないでください。 ## 設定キャッシュにはマージ後の値が保存される 両メソッドは、アプリケーションが `CachesConfiguration` を実装し、`configurationIsCached()` が `true` の場合にはマージ処理をスキップします。通常のLaravelアプリケーションでは、設定キャッシュが存在する起動がこれに該当します。 `ConfigCacheCommand` は古い設定キャッシュを削除し、新しいアプリケーションを起動して設定リポジトリ全体を取得します。その起動でプロバイダーの設定がマージされ、結果がキャッシュファイルに保存されます。以降の起動では `LoadConfiguration` がその値を読み込みます。 ```mermaid theme={null} flowchart TD A["php artisan config:cache"] --> B["古い設定キャッシュを削除"] B --> C["新しいアプリケーションを起動"] C --> D["設定ファイルを読み込み
    プロバイダーでマージ"] D --> E["設定リポジトリ全体を保存"] E --> F["以降の起動では保存済み設定を使用
    両マージメソッドはスキップ"] ``` したがって、パッケージを更新してデフォルト値やマージ方式が変わっても、古いキャッシュを使い続けるアプリケーションには反映されません。設定キャッシュを利用するデプロイでは、更新後のコードで再構築します。 ```bash theme={null} php artisan config:cache ``` 開発中にファイルから読み直す状態へ戻したい場合は、`php artisan config:clear` を使います。キャッシュの生成場所を独自に決めず、Laravelのコマンドに管理を任せてください。 設定ファイルにClosureを定義しないでください。`config:cache` で正しくシリアライズできません。コールバックを渡したい場合は、設定にはクラス名などを置き、実際のサービスの登録はプロバイダーで行います。 ## 設定変更をリリースする前の確認 パッケージのテストでは、未公開の設定だけでなく、旧バージョンから残っている設定を入力として扱います。 * 設定未公開でも、必要なデフォルト値が取得できる。 * 古い公開済み設定で、利用者の値が優先され、新しい項目が設計どおり補われる。 * ネストした配列、数値キーのリスト、空配列について、上書きの契約が変わらない。 * 利用アプリケーションで `config:cache` が成功し、キャッシュを使う別の起動でも同じ設定になる。 新しいキーのデフォルト値と、必要なキャッシュ再構築をリリースノートに記載します。キーの削除・改名やマージ方式の変更は、利用者の公開済み設定との互換性も含めて判断します。 ## 関連ページ サービスプロバイダーを登録して、設定やサービスの動作を検証します。 公開APIの変更をリリース方針と継続的なメンテナンスに結び付けます。 ## 参照した一次情報 * [Laravel公式ドキュメント: パッケージ設定](https://github.com/laravel/docs/blob/13.x/packages.md#configuration) * [ServiceProvider: マージと公開の実装](https://github.com/laravel/framework/blob/v13.34.0/src/Illuminate/Support/ServiceProvider.php) * [ConfigCacheCommand: 設定キャッシュの生成](https://github.com/laravel/framework/blob/v13.34.0/src/Illuminate/Foundation/Console/ConfigCacheCommand.php) * [LoadConfiguration: 設定の読み込み](https://github.com/laravel/framework/blob/v13.34.0/src/Illuminate/Foundation/Bootstrap/LoadConfiguration.php) * [ConfigClearCommand: 設定キャッシュの削除](https://github.com/laravel/framework/blob/v13.34.0/src/Illuminate/Foundation/Console/ConfigClearCommand.php) # Laravelパッケージ開発 Source: https://kawax.biz/jp/advanced/package-development サービスプロバイダーを核としたLaravelパッケージの開発方法を、設定・ビュー・マイグレーション・ファサードの公開から自動検出・遅延読み込みまで網羅します。 ## パッケージとは Laravelにおけるパッケージとは、アプリケーションに機能を追加するComposerパッケージです。パッケージには大きく2種類あります。 * **スタンドアロンパッケージ** — Laravelに依存しない汎用PHPライブラリ(例:Carbon、Pest) * **Laravelパッケージ** — ルート、コントローラー、ビュー、設定など、Laravelと統合された機能を持つパッケージ このガイドでは後者、Laravel専用パッケージの開発を扱います。パッケージ開発にはサービスプロバイダー、ファサード、設定ファイルの公開など、Laravelの内部構造を深く理解する必要があります。 パッケージのテストを書く場合は、[Orchestra Testbench](https://github.com/orchestral/testbench) を使います。通常のLaravelアプリケーションと同じようにパッケージのテストを記述できます。 ## パッケージの自動検出 Laravelはパッケージをインストールした際に、`composer.json` の `extra.laravel` セクションを読み取ってサービスプロバイダーとファサードを自動登録します。 ```json theme={null} "extra": { "laravel": { "providers": [ "Acme\\Courier\\CourierServiceProvider" ], "aliases": { "Courier": "Acme\\Courier\\Facades\\Courier" } } } ``` この設定を加えると、ユーザーは `bootstrap/providers.php` を手動で編集しなくてもパッケージが自動的に読み込まれます。 この自動検出がどのように実装され、いつキャッシュが再構築されるのかは[パッケージ自動検出の内部構造](/jp/advanced/package-discovery)で詳しく解説しています。 ### 自動検出を無効にする ユーザー側で特定のパッケージの自動検出を無効にしたい場合は、アプリケーションの `composer.json` に設定します。 ```json theme={null} "extra": { "laravel": { "dont-discover": [ "acme/courier" ] } } ``` ## サービスプロバイダーの役割 サービスプロバイダーはパッケージのエントリポイントです。ビュー、設定、マイグレーション、ルートなどのリソースをLaravelに登録する処理をここに集約します。 サービスプロバイダーは `Illuminate\Support\ServiceProvider` を継承し、`register` と `boot` の2つのメソッドを持ちます。 ```php theme={null} mergeConfigFrom( __DIR__.'/../config/courier.php', 'courier' ); $this->app->singleton(CourierManager::class, function ($app) { return new CourierManager($app['config']['courier']); }); } /** * パッケージのサービスをブートストラップする */ public function boot(): void { // リソースの登録はここで行う $this->loadRoutesFrom(__DIR__.'/../routes/web.php'); $this->loadViewsFrom(__DIR__.'/../resources/views', 'courier'); $this->loadTranslationsFrom(__DIR__.'/../lang', 'courier'); $this->publishesMigrations([ __DIR__.'/../database/migrations' => database_path('migrations'), ]); $this->publishes([ __DIR__.'/../config/courier.php' => config_path('courier.php'), ], 'courier-config'); $this->publishes([ __DIR__.'/../resources/views' => resource_path('views/vendor/courier'), ], 'courier-views'); } } ``` `register` メソッド内でイベントリスナー、ルート、ビューなどを登録しないでください。まだ読み込まれていない別のサービスプロバイダーのサービスを誤って使ってしまう可能性があります。バインディング以外の処理は必ず `boot` メソッドで行います。 サービスを使われるまで生成せずに拡張し、解決済みの場合にも設定を反映する方法は、[パッケージのサービス解決後フック](/jp/advanced/package-service-resolution)を参照してください。`callAfterResolving()` の実行タイミングと、重複適用に耐える設計を解説しています。 Octaneやキューワーカーでも状態を持ち越さない登録方法は、[パッケージサービスのライフサイクル設計](/jp/advanced/package-service-lifetimes)を参照してください。`scoped()` の内部実装と、singletonが保持する依存関係の注意点を解説しています。 パッケージのHTTP機能へミドルウェアを追加する方法は、[パッケージのミドルウェア登録と適用範囲](/jp/advanced/package-middleware)を参照してください。エイリアスの登録とルートへの適用を分離し、利用アプリケーションの既存グループを上書きしない設計を解説しています。 ## 設定ファイルのPublish ### publishes() — ファイルを公開する `boot` メソッドで `publishes()` を呼び出すと、ユーザーが `vendor:publish` コマンドで設定ファイルを自分のアプリケーションにコピーできるようになります。 ```php theme={null} public function boot(): void { $this->publishes([ __DIR__.'/../config/courier.php' => config_path('courier.php'), ]); } ``` 公開後の設定値は通常のconfigアクセスと同じ方法で取得できます。 ```php theme={null} $value = config('courier.option'); ``` ### mergeConfigFrom() — デフォルト値とマージする `register` メソッドで `mergeConfigFrom()` を使うと、ユーザーが設定ファイルを公開していない場合にもパッケージのデフォルト値が使われます。 ```php theme={null} public function register(): void { $this->mergeConfigFrom( __DIR__.'/../config/courier.php', 'courier' ); } ``` `mergeConfigFrom()` はネストした配列の深いレベルまではマージしません。多次元配列を持つ設定では、ユーザーが一部だけ定義した場合に残りのオプションがマージされないことがあります。 Laravel 13の再帰的な置換メソッドとの違い、数値キーのリストの扱い、設定キャッシュを利用する場合の更新手順は[パッケージ設定のマージとキャッシュ](/jp/advanced/package-config-merging)で解説しています。 ### タグで公開グループを分ける `publishes()` の第2引数にタグを指定すると、ユーザーが必要なリソースだけを選んで公開できます。 ```php theme={null} public function boot(): void { $this->publishes([ __DIR__.'/../config/courier.php' => config_path('courier.php'), ], 'courier-config'); $this->publishesMigrations([ __DIR__.'/../database/migrations/' => database_path('migrations'), ], 'courier-migrations'); } ``` ```shell theme={null} # 設定ファイルだけを公開する php artisan vendor:publish --tag=courier-config # プロバイダーが提供するすべてのファイルを公開する php artisan vendor:publish --provider="Acme\Courier\CourierServiceProvider" ``` ## ルートの登録 `loadRoutesFrom()` を使ってルートファイルを読み込みます。アプリケーションのルートキャッシュが有効な場合は自動的にスキップされます。 ```php theme={null} public function boot(): void { $this->loadRoutesFrom(__DIR__.'/../routes/web.php'); } ``` ルートファイルではパッケージのコントローラーを指定します。 ```php theme={null} // routes/web.php use Acme\Courier\Http\Controllers\TrackingController; use Illuminate\Support\Facades\Route; Route::prefix('courier')->group(function () { Route::get('/track/{id}', [TrackingController::class, 'show']) ->name('courier.track'); }); ``` ミドルウェアの明示、URIとルート名の衝突回避、設定変更をキャッシュへ反映する手順は[パッケージのルート登録とキャッシュ](/jp/advanced/package-routes)で解説しています。 ## マイグレーションのPublish `publishesMigrations()` を使うと、マイグレーションファイルを公開対象に登録できます。Laravel 13の標準構成では、`database.migrations.update_date_on_publish` が有効なため、公開時にファイル名のタイムスタンプが更新されます。設定が無効または存在しない場合は、この更新は行われません。 ```php theme={null} public function boot(): void { $this->publishesMigrations([ __DIR__.'/../database/migrations' => database_path('migrations'), ]); } ``` 公開と直接読み込みの使い分け、タイムスタンプ変更の条件、再公開のリスク、既存利用者へのスキーマ変更は[パッケージのマイグレーション公開と更新](/jp/advanced/package-migrations)で解説しています。 ## ビューのPublish ### loadViewsFrom() — ビューを登録する `loadViewsFrom()` でビューディレクトリを登録します。第2引数の名前空間を使って `package::view` の形式でビューを参照します。 ```php theme={null} public function boot(): void { $this->loadViewsFrom(__DIR__.'/../resources/views', 'courier'); } ``` 登録後、ビューはパッケージ名前空間で参照します。 ```php theme={null} Route::get('/dashboard', function () { return view('courier::dashboard'); }); ``` Laravelはビューを2か所から探します。まずアプリケーションの `resources/views/vendor/courier` ディレクトリを確認し、なければパッケージのビューディレクトリを使います。これによりユーザーがビューをカスタマイズできます。 実装上の検索順序とファイル単位のフォールバック、公開済みテンプレートへの変更の届け方、Bladeキャッシュとの違いは[パッケージビューの上書きと更新](/jp/advanced/package-views)で解説しています。 ### ビューを公開する ```php theme={null} public function boot(): void { $this->loadViewsFrom(__DIR__.'/../resources/views', 'courier'); $this->publishes([ __DIR__.'/../resources/views' => resource_path('views/vendor/courier'), ], 'courier-views'); } ``` ### Bladeコンポーネントを登録する コンポーネントをパッケージに含める場合は、`boot` メソッドで登録します。 ```php theme={null} use Illuminate\Support\Facades\Blade; use Acme\Courier\View\Components\AlertComponent; public function boot(): void { Blade::component('courier-alert', AlertComponent::class); } ``` コンポーネント名前空間を使って一括登録することもできます。 ```php theme={null} use Illuminate\Support\Facades\Blade; public function boot(): void { Blade::componentNamespace('Acme\\Courier\\View\\Components', 'courier'); } ``` ```blade theme={null} {{-- 個別登録の場合 --}} {{-- 名前空間登録の場合 --}} ``` ## 公開アセットの配布 パッケージのJavaScript・CSS・画像を配布するには、`publishes()` で `public_path('vendor/courier')` などへのコピーを登録します。パッケージを更新しても公開済みファイルは自動では変わりません。 タグとプロバイダーによる対象の絞り込み、`--force` と `--existing` の違い、削除済みファイルの扱い、`laravel-assets` による更新は[パッケージの公開アセットと更新](/jp/advanced/package-assets)で解説しています。 ## 翻訳ファイルのPublish `loadTranslationsFrom()` で翻訳ファイルを登録します。翻訳は `package::file.key` の形式で参照します。 ```php theme={null} public function boot(): void { $this->loadTranslationsFrom(__DIR__.'/../lang', 'courier'); $this->publishes([ __DIR__.'/../lang' => $this->app->langPath('vendor/courier'), ]); } ``` ```php theme={null} // 翻訳の使用 echo trans('courier::messages.welcome'); ``` JSONの翻訳ファイルを使う場合は `loadJsonTranslationsFrom()` を使います。 ```php theme={null} public function boot(): void { $this->loadJsonTranslationsFrom(__DIR__.'/../lang'); } ``` PHP翻訳の部分上書き、JSON翻訳の読み込み順序と上書き先、翻訳キーの互換性と公開済みファイルの更新は[パッケージ翻訳の上書きと更新](/jp/advanced/package-translations)で解説しています。 ## コマンドの登録 パッケージのArtisanコマンドは `commands()` メソッドで登録します。コンソール環境でのみ登録するのが一般的です。 ```php theme={null} use Acme\Courier\Console\Commands\InstallCommand; use Acme\Courier\Console\Commands\SyncCommand; public function boot(): void { if ($this->app->runningInConsole()) { $this->commands([ InstallCommand::class, SyncCommand::class, ]); } } ``` ### optimize コマンドへの統合 パッケージが独自のキャッシュを持つ場合、`optimizes()` メソッドで `php artisan optimize` と `php artisan optimize:clear` に統合できます。 ```php theme={null} public function boot(): void { if ($this->app->runningInConsole()) { $this->optimizes( optimize: 'courier:cache', clear: 'courier:clear-cache', ); } } ``` コマンドクラスの登録と最適化タスクの登録は別の処理です。登録キーの衝突、`--except` による除外、標準タスクとの実行順序、失敗時の終了コードは[パッケージのキャッシュとoptimizeへの統合](/jp/advanced/package-optimization)で解説しています。 ### `about` コマンドへの情報追加 `php artisan about` の出力にパッケージ情報を追加するには `AboutCommand::add()` を使います。 ```php theme={null} use Illuminate\Foundation\Console\AboutCommand; public function boot(): void { AboutCommand::add('Courier Package', fn () => ['Version' => '1.0.0']); } ``` データを評価するタイミング、`--only` とJSON出力、秘密情報を含めない設計、テストでの登録状態は[パッケージの診断情報をaboutに追加する](/jp/advanced/package-about)で解説しています。 ## ファサードの作成 ファサードを使うと、サービスコンテナのバインディングを静的メソッドのように呼び出せます。 ```php theme={null} 'delivered']; } } ``` `Illuminate\Support\Facades\Facade` を継承し、`getFacadeAccessor()` でサービスコンテナのバインディングキーを返します。 ```php theme={null} ```php theme={null} public function register(): void { $this->app->singleton(\Acme\Courier\CourierManager::class, function ($app) { return new \Acme\Courier\CourierManager($app['config']['courier']); }); } ``` ```json theme={null} "extra": { "laravel": { "providers": [ "Acme\\Courier\\CourierServiceProvider" ], "aliases": { "Courier": "Acme\\Courier\\Facades\\Courier" } } } ``` ファサードのメソッドにはPHPDocの `@method` アノテーションを付けることで、IDEの補完が有効になります。 ```php theme={null} // ファサード経由でサービスを呼び出す use Acme\Courier\Facades\Courier; Courier::send('user@example.com', 'パッケージが届きました'); $status = Courier::track('ABC-123'); ``` ## DeferrableProvider — 遅延読み込みの実装 サービスコンテナへのバインディングのみを行うプロバイダーは、`DeferrableProvider` インターフェースを実装することで遅延読み込みを実現できます。サービスが実際に必要になるまでプロバイダーが読み込まれないため、アプリケーションのパフォーマンスが向上します。 ```php theme={null} app->singleton(CourierManager::class, function ($app) { return new CourierManager($app['config']['courier']); }); } /** * このプロバイダーが提供するサービスの一覧を返す * * @return array */ public function provides(): array { return [CourierManager::class]; } } ``` Laravelは遅延プロバイダーが提供するサービスのリストをコンパイルして保存します。`provides()` に列挙したサービスが解決されるときだけプロバイダーが読み込まれます。 リソースの登録(ビュー、ルート、イベントリスナーなど)が必要なプロバイダーには `DeferrableProvider` を使わないでください。遅延読み込みされると、それらのリソースが登録されないままになります。 ## パッケージのテスト パッケージ単体でテストする場合は [Orchestra Testbench](https://github.com/orchestral/testbench) を使います。通常のLaravelアプリケーション内にいるかのようにパッケージのテストを記述できます。 ```shell theme={null} composer require --dev orchestra/testbench ``` テストケースで `getPackageProviders()` をオーバーライドしてパッケージのサービスプロバイダーを登録します。 ```php theme={null} \Acme\Courier\Facades\Courier::class, ]; } /** * テスト用の環境設定 */ protected function defineEnvironment($app): void { $app['config']->set('courier.api_key', 'test-key'); } } ``` ```php theme={null} assertTrue($result); } } ``` ## Composerへの公開 パッケージを [Packagist](https://packagist.org/) に公開するためのベストプラクティスです。 **`composer.json` の基本設定** ```json theme={null} { "name": "acme/courier", "description": "A Laravel courier package", "type": "library", "license": "MIT", "require": { "php": "^8.2", "illuminate/support": "^11.0||^12.0||^13.0" }, "require-dev": { "orchestra/testbench": "^9.0||^10.0", "phpunit/phpunit": "^11.0" }, "autoload": { "psr-4": { "Acme\\Courier\\": "src/" } }, "autoload-dev": { "psr-4": { "Acme\\Courier\\Tests\\": "tests/" } }, "extra": { "laravel": { "providers": [ "Acme\\Courier\\CourierServiceProvider" ], "aliases": { "Courier": "Acme\\Courier\\Facades\\Courier" } } }, "minimum-stability": "stable", "prefer-stable": true } ``` `illuminate/support` に依存することで、`illuminate/framework` 全体ではなくLaravelの必要なコンポーネントだけを依存に含められます。パッケージの依存ツリーを小さく保ちましょう。 **ディレクトリ構造の例** ``` acme/courier/ ├── config/ │ └── courier.php ├── database/ │ └── migrations/ │ └── 2024_01_01_000000_create_courier_logs_table.php ├── lang/ │ └── ja/ │ └── messages.php ├── resources/ │ └── views/ │ └── dashboard.blade.php ├── routes/ │ └── web.php ├── src/ │ ├── Console/ │ │ └── Commands/ │ │ └── InstallCommand.php │ ├── Facades/ │ │ └── Courier.php │ ├── Http/ │ │ └── Controllers/ │ │ └── TrackingController.php │ ├── CourierManager.php │ └── CourierServiceProvider.php ├── tests/ │ ├── Feature/ │ └── TestCase.php ├── composer.json └── README.md ``` ## 関連ページ サービスプロバイダーの `register` と `boot` メソッド、遅延プロバイダーの詳細を確認します。 LaravelとPHPのメジャーバージョンアップへの対応戦略とGitHub Actionsのテストマトリクス設定を解説します。 # パッケージ自動検出の内部構造 Source: https://kawax.biz/jp/advanced/package-discovery PackageManifestクラスの内部実装を読み解き、composer.jsonのextra.laravelがどのようにキャッシュされ、bootstrap/cache/packages.phpとして読み込まれるのかを解説します。 ## このページについて [パッケージ開発の基礎](/jp/advanced/package-development)では `composer.json` の `extra.laravel` セクションを書けばサービスプロバイダーとファサードが自動登録されることを紹介しました。このページではその裏側、`Illuminate\Foundation\PackageManifest` クラスがどのように動作しているかをソースコードレベルで解説します。 このページは[パッケージ開発の基礎](/jp/advanced/package-development)の姉妹ページです。自動検出の基本的な使い方を先に読むことをおすすめします。 ## 自動検出の仕組み全体像 ```mermaid theme={null} flowchart TD A["composer install / update"] --> B["Composerが post-autoload-dump
    イベントを発火"] B --> C["Illuminate\\Foundation\\ComposerScripts::postAutoloadDump()"] C --> D["bootstrap/cache/packages.phpなどの
    キャッシュファイルを削除"] D --> E["次回起動時にPackageManifest::build()が実行される"] E --> F["vendor/composer/installed.jsonを読み取り"] F --> G["各パッケージのextra.laravelを収集"] G --> H["dont-discoverでの除外処理"] H --> I["bootstrap/cache/packages.phpに書き込み"] I --> J["Applicationがproviders()/aliases()を
    読み取ってサービスプロバイダー登録"] ``` ## `PackageManifest` クラス 自動検出の中核は `Illuminate\Foundation\PackageManifest` です。以下がフレームワーク13.x時点の実装(要約)です。 ```php theme={null} class PackageManifest { public function providers() { return $this->config('providers'); } public function aliases() { return $this->config('aliases'); } public function config($key) { return (new Collection($this->getManifest())) ->flatMap(fn ($configuration) => (array) ($configuration[$key] ?? [])) ->filter() ->all(); } protected function getManifest() { if (! is_null($this->manifest)) { return $this->manifest; } if (! is_file($this->manifestPath)) { $this->build(); } return $this->manifest = is_file($this->manifestPath) ? $this->files->getRequire($this->manifestPath) : []; } } ``` ポイントは次の3つです。 * **マニフェストは一度読み込むとメモリにキャッシュされる**(`$this->manifest` プロパティ)。1リクエスト内で複数回 `providers()` を呼んでもファイルI/Oは1回だけ。 * **マニフェストファイルが存在しない場合のみ `build()` が実行される**。通常運用では毎回ビルドされるわけではない。 * 実体は素朴なPHP配列を `return` するファイル(`bootstrap/cache/packages.php`)で、`require` するだけで読み込める最速の形式。 ## マニフェストのビルド処理 `build()` メソッドが実際に `composer.json` の情報を集約する部分です。 ```php theme={null} public function build() { $packages = []; if ($this->files->exists($path = $this->vendorPath.'/composer/installed.json')) { $installed = json_decode($this->files->get($path), true); $packages = $installed['packages'] ?? $installed; } $ignoreAll = in_array('*', $ignore = $this->packagesToIgnore()); $this->write((new Collection($packages))->mapWithKeys(function ($package) { return [$this->format($package['name']) => $package['extra']['laravel'] ?? []]; })->each(function ($configuration) use (&$ignore) { $ignore = array_merge($ignore, $configuration['dont-discover'] ?? []); })->reject(function ($configuration, $package) use ($ignore, $ignoreAll) { return $ignoreAll || in_array($package, $ignore); })->filter()->all()); } ``` 重要な点は、`composer.json` を直接パースするのではなく **`vendor/composer/installed.json`** を読んでいることです。これはComposerが `composer install` / `composer update` 実行時に生成する、インストール済み全パッケージのメタデータファイルです。各パッケージの `composer.json` に書かれた `extra` セクションがここに集約されているため、Laravel側はComposerの管理下にある情報だけを信頼して読み取ります。 `installed.json` は通常 `.gitignore` されているため、初回セットアップ時(`vendor/` がない状態)では自動検出は機能しません。`composer install` の完了後に初めてキャッシュが構築されます。 ### `dont-discover` の2つの書き方 `dont-discover` はパッケージ側・アプリケーション側どちらの `composer.json` にも書けますが、意味が異なります。 ```json title="パッケージ側のcomposer.json" theme={null} "extra": { "laravel": { "providers": ["Acme\\Courier\\CourierServiceProvider"], "dont-discover": [] } } ``` ```json title="アプリケーション側のcomposer.json" theme={null} "extra": { "laravel": { "dont-discover": [ "acme/courier" ] } } ``` `build()` の実装を見ると、`$ignore` 配列は各パッケージの `configuration['dont-discover']` を `array_merge` して蓄積しています。つまりパッケージ自身が「自分の依存パッケージの自動検出を無効化する」ことも技術的には可能です(例:内部で使っているサブパッケージのプロバイダーを重複登録させたくない場合)。ただし実務でよく使うのはアプリケーション側での無効化です。 ### `dont-discover` に `*` を指定する `packagesToIgnore()` が返す配列に `*` が含まれていると `$ignoreAll = true` となり、**すべてのパッケージの自動検出を丸ごと無効化**します。CIやテスト環境で自動検出のオーバーヘッドを避けたい場合や、`bootstrap/providers.php` で完全に手動管理したい場合に使います。 ```json theme={null} "extra": { "laravel": { "dont-discover": ["*"] } } ``` ## キャッシュファイルの実体 `getCachedPackagesPath()` は環境変数 `APP_PACKAGES_CACHE` があればそれを、なければ `bootstrap/cache/packages.php` を返します。 ```php theme={null} public function getCachedPackagesPath() { return $this->normalizeCachePath('APP_PACKAGES_CACHE', 'cache/packages.php'); } ``` このファイルを直接開くと、単純な連想配列を `return` しているだけだとわかります。 ```php theme={null} array ( 'providers' => array ( 0 => 'Acme\\Courier\\CourierServiceProvider', ), ), ); ``` パッケージ名がキーになっているため、`php artisan package:discover` の出力で「どのパッケージが検出されたか」を確認できます。 ## キャッシュが再構築されるタイミング `Illuminate\Foundation\ComposerScripts` は3つのComposerイベントにフックしており、いずれも同じ `clearCompiled()` を呼びます。 ```php theme={null} public static function postInstall(Event $event) { require_once $event->getComposer()->getConfig()->get('vendor-dir').'/autoload.php'; static::clearCompiled(); } protected static function clearCompiled() { $laravel = new Application(getcwd()); if (is_file($configPath = $laravel->getCachedConfigPath())) { @unlink($configPath); } if (is_file($servicesPath = $laravel->getCachedServicesPath())) { @unlink($servicesPath); } if (is_file($packagesPath = $laravel->getCachedPackagesPath())) { @unlink($packagesPath); } } ``` つまり `composer install` / `composer update` / `composer dump-autoload` のいずれを実行しても、設定キャッシュ・サービスキャッシュ・パッケージキャッシュがまとめて削除されます。次にLaravelが起動したタイミングで `PackageManifest::build()` が走り、`installed.json` の最新状態から再構築されます。 これは `composer.json` の `scripts` に登録されている、標準的なLaravelプロジェクトの挙動です。 ```json title="Laravelアプリのcomposer.json(抜粋)" theme={null} "scripts": { "post-autoload-dump": [ "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump" ], "post-update-cmd": [ "@php artisan vendor:publish --tag=laravel-assets --ansi --force" ], "post-root-package-install": [ "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" ], "post-create-project-cmd": [ "@php artisan key:generate --ansi" ] } ``` ## `php artisan package:discover` で手動再構築 キャッシュが古いままになっている、あるいはComposerを経由せずに `vendor/` を直接変更したような場合は、`package:discover` コマンドで手動再構築できます。 ```shell theme={null} php artisan package:discover ``` このコマンドの実体は非常に薄いラッパーです。 ```php theme={null} #[AsCommand(name: 'package:discover')] class PackageDiscoverCommand extends Command { public function handle(PackageManifest $manifest) { $this->components->info('Discovering packages'); $manifest->build(); (new Collection($manifest->manifest)) ->keys() ->each(fn ($description) => $this->components->task($description)) ->whenNotEmpty(fn () => $this->newLine()); } } ``` `$manifest->build()` を呼んで結果を出力するだけで、コマンド固有のロジックはほぼありません。CIパイプラインで `composer install --no-scripts` を使っている場合など、Composerイベントが発火しない状況ではこのコマンドを明示的に呼ぶ必要があります。 パッケージのテストで [Orchestra Testbench](/jp/advanced/package-testing) を使う場合、Testbenchは独自の `vendor/bin/testbench package:discover` コマンドを提供しています。詳細は[Testbenchでのパッケージテスト](/jp/advanced/package-testing)を参照してください。Artisanの `package:discover` とは別物で、Testbench専用のスケルトンアプリ向けにマニフェストを構築します。 ## デプロイ時の注意点 本番デプロイでは `composer install --no-dev --optimize-autoloader` を実行するのが一般的ですが、`--no-scripts` を付けている場合はパッケージキャッシュが更新されません。デプロイスクリプトに明示的な再構築ステップを入れておくと安全です。 ```shell theme={null} composer install --no-dev --optimize-autoloader --no-scripts php artisan package:discover --ansi php artisan config:cache php artisan route:cache ``` 順序も重要です。`package:discover` は `config:cache` より前に実行してください。設定キャッシュが先に作られると、後から追加されたパッケージ設定(`mergeConfigFrom` で登録される設定など)が反映されない場合があります。 ## まとめ * 自動検出は `composer.json` の静的な内容ではなく、Composerが生成する `vendor/composer/installed.json` を読み取って動作する。 * 結果は `bootstrap/cache/packages.php` にプレーンなPHP配列としてキャッシュされ、`require` だけで高速に読み込まれる。 * キャッシュは `composer install/update/dump-autoload` の各イベントで自動的に削除され、次回起動時に再構築される。 * `--no-scripts` でComposerを実行する環境では `php artisan package:discover` を明示的に呼ぶ必要がある。 * `dont-discover` に `*` を指定すると自動検出全体を無効化できる。 ## 関連ページ * [パッケージ開発の基礎](/jp/advanced/package-development) — サービスプロバイダーと `extra.laravel` の基本的な使い方 * [遅延サービスプロバイダー](/jp/advanced/deferred-provider) — 検出されたプロバイダーの読み込みタイミングを最適化する * [Orchestra Testbenchでのパッケージテスト](/jp/advanced/package-testing) — Testbench独自の `package:discover` コマンド # パッケージのイベントリスナー登録とキャッシュ Source: https://kawax.biz/jp/advanced/package-events Laravel 13のイベント登録とevent:cacheの実装を読み解き、パッケージの明示登録、自動検出との二重登録、依存解決、更新時の検証方法を解説します。 パッケージにイベント駆動の拡張点を用意したいときは、**イベントクラスの提供**と**リスナーの登録**を分けて設計します。利用者が `vendor` 内のディレクトリを自動検出に追加しなくても動く構成にすると、インストール・更新・キャッシュ作成時の前提を減らせます。 このページではLaravel Framework `v13.36.0` の実装を確認し、[イベント](/jp/events)の基本操作をパッケージの長期保守に適用します。プロバイダーの読み込みは[パッケージの自動検出](/jp/advanced/package-discovery)、テスト環境は[パッケージのテスト](/jp/advanced/package-testing)を参照してください。 ## パッケージのリスナーを明示登録する パッケージが標準で実行する処理は、通常の `Illuminate\Support\ServiceProvider` の `boot()` で `Event::listen()` に登録できます。Laravelの既定のリスナー自動検出先はアプリケーションの `app/Listeners` であり、パッケージ内の `src/Listeners` まで自動で走査するわけではありません。 例として、配送完了イベントをログに記録するパッケージを作ります。イベントは通知に必要な値を持つだけのクラスです。 ```php theme={null} logger->info('Shipment delivered', [ 'shipment_id' => $event->shipmentId, ]); } } ``` プロバイダーにはイベントとリスナーのクラス名を登録します。 ```php theme={null} リスナー登録だけを理由に、このプロバイダーへ `DeferrableProvider` を実装しないでください。遅延対象のサービスが解決されなければ、リスナーも登録されません。重いサービスのバインディングは、必要に応じて別の[遅延プロバイダー](/jp/advanced/deferred-provider)へ分離します。 ## 登録とインスタンス生成は別のタイミング `Dispatcher::listen()` は、通常のイベント名に対するリスナーを内部配列へ追加します。上のようにクラス名を渡す場合、ここではリスナーのインスタンスを生成しません。 同期リスナーでは、イベント発火後に `createClassCallable()` がコンテナの `make()` を使ってクラスを解決します。そのため、コンストラクターの依存注入を利用できます。 ```mermaid theme={null} flowchart TD A["プロバイダーのboot()"] --> B["Event::listen()でクラス名を保存"] B --> C["ShipmentDeliveredを発火"] C --> D["Dispatcherがリスナーを選択"] D --> E["コンテナで同期リスナーを解決"] E --> F["handle()へイベントを渡す"] ``` この経路は `handle()` に対してコンテナの `call()` を行うものではありません。**任意のサービスはコンストラクターに注入し、`handle()` ではイベントを受け取る**設計にします。[Artisanコマンドの `handle()` への依存注入](/jp/advanced/package-commands)と同じ仕組みだと考えないでください。 ここで説明しているのは同期リスナーです。`ShouldQueue` を実装する場合はキュー用の別経路になるため、トランザクション、再試行、依存の解決について[イベントのキュー処理](/jp/events)と[キュー](/jp/queues)も確認します。 ## event:cacheは明示登録を止めない `event:cache` は、イベントDispatcherに登録されたすべてのリスナーを丸ごとシリアライズするコマンドではありません。 `EventCacheCommand::getEvents()` は、`Illuminate\Foundation\Support\Providers\EventServiceProvider` とその派生プロバイダーを列挙し、各プロバイダーの自動検出結果と `listens()` のマッピングを保存します。通常の `ServiceProvider::boot()` に書いた `Event::listen()` は、この収集対象には入りません。 一方、キャッシュがある起動でも通常のプロバイダーの `boot()` は実行されます。**キャッシュに含まれないことと、キャッシュがあると登録されないことは別です。** | 登録方法 | `event:cache` に保存される内容 | キャッシュがある起動時 | | - | - | - | | 通常のプロバイダーの `boot()` で `Event::listen()` | この登録自体は収集対象外 | `boot()` で再び登録する | | Foundationの `EventServiceProvider` の `$listen` | プロバイダーのクラス名ごとにマッピングを保存 | `getEvents()` がキャッシュを読む | | 自動検出の対象になったリスナー | 検出結果をマッピングへ保存 | ディレクトリの再走査ではなくキャッシュを使う | Foundationの `EventServiceProvider` を継承したパッケージで `$listen` を使う方法もあります。ただし、確認した実装では `shouldDiscoverEvents()` の既定値はその基底クラス自身に対する判定です。**継承するだけでは、そのパッケージのリスナーディレクトリが自動検出されるわけではありません。** また、同プロバイダーの `$subscribe` と `$observers` は、`getEvents()` の結果を登録するループとは別に処理されます。`$listen` のキャッシュだけを見て、これらの登録まで省略されると判断しないでください。 `Event::subscribe(SomeSubscriber::class)` は、登録時にコンテナでサブスクライバーを生成して `subscribe()` を呼びます。クラス名による `Event::listen()` と同じ遅延性ではありません。サブスクライバーのコンストラクターや `subscribe()` では外部通信などの副作用を避けます。 ## 自動検出との二重登録を避ける 利用者が `withEvents(discover: [...])` でパッケージのリスナーディレクトリを追加し、パッケージのプロバイダーも同じリスナーを明示登録すると、1つのイベントで処理が複数回動く可能性があります。 `Dispatcher::listen()` は通常のイベントに対して配列への追加を行い、Dispatcher全体で同じクラス名を自動的に重複排除しません。Foundationの `EventServiceProvider` が自身のマッピング内で行う `array_unique()` も、別のプロバイダーによる明示登録までまとめる仕組みではありません。 登録の責任を一方に決めます。 * **パッケージが標準リスナーを登録する**場合、利用者にそのディレクトリの自動検出追加を要求しません。 * **利用者が必要なリスナーを選ぶ**場合、パッケージはイベントとリスナークラスを提供し、同じリスナーを標準で登録しません。 * 自動検出から明示登録へ移行する場合、利用者側の検出設定や古いイベントキャッシュも更新対象として案内します。 業務処理の冪等性も重要ですが、二重登録を残したまま冪等性だけで隠すのではなく、登録経路を整理してください。 ## 更新前後の動作を検証する 登録状態を調べるには、パッケージを導入した検証用アプリケーションで `event:list` を実行します。キャッシュなし・ありの両方で対象のリスナーが存在することを確認します。 ```bash theme={null} php artisan event:clear php artisan event:list php artisan event:cache php artisan event:list ``` これは検証用アプリでの手順です。`event:list` の表示だけでは、リスナーが処理を完了することや1回だけ実行されることまでは保証できません。実際のイベント発火を使うテストも追加します。 例として、[TestbenchのベースTestCase](/jp/advanced/package-testing)で `CourierServiceProvider` を登録したうえで、ロガーが1回だけ呼ばれることを検証します。 ```php theme={null} createMock(LoggerInterface::class); $logger->expects($this->once()) ->method('info') ->with('Shipment delivered', ['shipment_id' => 'shipment-123']); $this->app->instance(LoggerInterface::class, $logger); Event::dispatch(new ShipmentDelivered('shipment-123')); } } ``` `Event::fake()` はイベントの発火確認には便利ですが、通常は本物のリスナーの実行を止めます。このテストでは登録と処理を検証するため使用しません。自動検出との組み合わせやキャッシュありの起動も、別の検証用アプリ・新しいアプリケーションインスタンスで確認します。 起動後に `event:cache` を実行しても、そのプロセス内に登録済みのDispatcherがキャッシュから再構築されるわけではありません。キャッシュありの動作確認は、新しい起動で行ってください。更新したリスナーを長時間動くワーカーへ反映するには、通常のデプロイ手順に沿った再起動も必要です。 ## 長期保守のチェックリスト * イベントクラス名、公開プロパティ、リスナーの登録方針を利用者向けの契約として文書化する。 * 利用者が購読するイベントの名前やペイロードを変えるときは、[バージョン互換性管理](/jp/advanced/package-versioning)に沿って変更を案内する。 * 自動検出や `$listen` に変更がある更新では、`event:cache` の再作成をデプロイ手順へ含める。 * パッケージの標準リスナーと利用者の独自リスナーを区別し、同じ標準リスナーを二重に登録させない。 * PHP・Laravelの対応バージョンごとに、リスナーが1回だけ実行されるテストを継続する。 ## 参照した一次情報 * [Laravel公式ドキュメント: イベント登録・自動検出・キャッシュ・サブスクライバー](https://github.com/laravel/docs/blob/73e61270287e4aaa1cecd77cd66aa6ef4f499828/events.md) * [Dispatcher: listen・subscribe・createClassCallable](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Events/Dispatcher.php) * [Foundation EventServiceProvider: register・getEvents・shouldDiscoverEvents](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php) * [EventCacheCommand: キャッシュ対象の収集](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Foundation/Console/EventCacheCommand.php) # パッケージのミドルウェア登録と適用範囲 Source: https://kawax.biz/jp/advanced/package-middleware Laravel 13のRouter実装から、パッケージのミドルウェアエイリアス登録、明示的な適用、グループの上書き回避とキャッシュ時の検証を解説します。 パッケージにHTTP機能を追加するときは、ミドルウェアを利用可能にすることと、利用アプリケーションのリクエストへ適用することを分けます。インストールだけで全ルートの挙動を変えない設計にすると、導入時の影響と将来の互換性を管理しやすくなります。 このページでは[パッケージ開発の基礎](/jp/advanced/package-development)を前提に、Laravel公式ドキュメントの `13.x` とFramework `v13.36.0` の実装を参照します。通常のミドルウェアの書き方は[ミドルウェア](/jp/middleware)を参照してください。 ## 登録・適用・実行を分ける `Router::aliasMiddleware()` は、エイリアスをキー、クラス名を値としてRouterの配列へ登録します。ミドルウェアのインスタンス生成や、ルートへの適用は行いません。 ```mermaid theme={null} flowchart TD A["プロバイダーのboot"] --> B["aliasMiddlewareで
    名前とクラスを登録"] C["ルート定義"] --> D["middlewareで
    適用する名前を指定"] B --> E["リクエストのルートが決まる"] D --> E E --> F["エイリアス・グループを展開
    除外と優先順位を反映"] F --> G["Pipelineがミドルウェアを実行"] ``` | 操作 | 役割 | それだけでは行われないこと | | - | - | - | | `aliasMiddleware()` | 名前からクラスへの対応を登録する | リクエストへの適用 | | ルートの `middleware()` | 対象ルートへ割り当てる | ミドルウェアクラスの独自実装 | | `middlewareGroup()` | 名前付きグループの内容を設定する | グローバルミドルウェアへの追加 | | HTTP Kernelのグローバルスタックへの追加 | 全リクエストを対象にする | ルート単位の除外への対応 | エイリアス名は公開APIです。`auth` や `throttle` のような組み込み名、他のパッケージと衝突しやすい一般名を避け、パッケージ固有のプレフィックスを付けます。同じキーへ再登録すると、`aliasMiddleware()` は以前の対応を上書きします。 ## プロバイダーでエイリアスを公開する 次の例では、レスポンスに固定の識別ヘッダーを付けるミドルウェアを作ります。認証・認可やアクセス制御の例ではありません。ComposerのPSR-4で `Acme\Courier\` を `src/` に対応させ、プロバイダーを[自動検出](/jp/advanced/package-discovery)または手動で登録することを前提にします。 ```php src/Http/Middleware/AddCourierHeader.php theme={null} headers->set('X-Acme-Courier', 'enabled'); return $response; } } ``` Routerの利用は `boot()` で行います。`boot()` の引数はサービスコンテナから注入されるため、ミドルウェアの登録のために利用者の `bootstrap/app.php` を自動編集する必要はありません。 ```php src/CourierServiceProvider.php theme={null} aliasMiddleware('acme-courier.header', AddCourierHeader::class); } } ``` このプロバイダーは通常のプロバイダーとして起動させます。エイリアスの登録を、別サービスが解決された場合だけ実行する遅延プロバイダーへ移すと、ルートが必要とする時点で登録される保証がなくなります。 ### 利用者がルート単位で適用する 利用アプリケーションの `routes/web.php` で、必要なルートにだけ割り当てます。 ```php routes/web.php theme={null} json(['available' => true]); })->middleware('acme-courier.header'); ``` エイリアスを提供せず、`->middleware(AddCourierHeader::class)` のようにクラス名を直接指定することもできます。短い名前を利用者に提供する必要があるかで選びます。 パッケージ自身がルートを提供する場合も、ルートファイルで必要なミドルウェアを明示します。`loadRoutesFrom()` は `web` や `api` を自動追加しません。登録方法とルートキャッシュの扱いは[パッケージのルート登録とキャッシュ](/jp/advanced/package-routes)を参照してください。 ## アプリケーション全体への適用は利用者が選ぶ 利用アプリケーションのすべての `web` ルートへ適用したい場合は、利用者が既存の `withMiddleware()` コールバックに次の設定を追加します。既存の設定を置き換える例ではありません。 ```php theme={null} use Acme\Courier\Http\Middleware\AddCourierHeader; use Illuminate\Foundation\Configuration\Middleware; ->withMiddleware(function (Middleware $middleware): void { $middleware->web(append: [ AddCourierHeader::class, ]); }) ``` この構成ではクラス名を直接使うため、エイリアスの登録順序に依存しません。適用範囲は `web` グループであり、グローバルスタックではありません。`api` や、どちらのグループも付いていないルートへは自動適用されません。 パッケージのインストール時に `web` 全体へ適用する設計と、利用者が明示的に適用する設計は互換ではありません。認証、セッション、レスポンスヘッダー、外部通信などに影響する処理を、自動更新で全ルートへ広げないでください。 ## Routerのグループ操作には差がある パッケージが独自のグループを提供する場合は、固有の名前を使います。次はプロバイダーの `boot(Router $router)` 内に置く例です。 ```php theme={null} $router->middlewareGroup('acme-courier', [ AddCourierHeader::class, ]); ``` 利用者は `->middleware('acme-courier')` で適用できます。ただし、このグループに含まれるのは指定したクラスだけであり、`web` のセッションやCSRF保護を引き継ぐものではありません。 既存グループへ追加するAPIもありますが、`v13.36.0` のRouter実装では次の違いがあります。 | Routerのメソッド | グループが存在する場合 | グループが存在しない場合 | | - | - | - | | `middlewareGroup($name, $items)` | 配列全体を置き換える | 指定配列で作る | | `pushMiddlewareToGroup($name, $item)` | 同じ項目がなければ末尾へ追加 | 空の配列を作ってから追加する | | `prependMiddlewareToGroup($name, $item)` | 同じ項目がなければ先頭へ追加 | 何もしない | `middlewareGroup('web', [...])` でパッケージのクラスだけを設定すると、既存の `web` の内容を置き換えてしまいます。追加用のメソッドと混同しないでください。また、存在しない `web` へ `pushMiddlewareToGroup()` しても、標準の `web` グループが復元されるわけではありません。 追加用メソッドの重複判定は登録した値に対するものです。同じクラスを指すエイリアスとクラス名は異なる文字列なので、登録表の段階では別項目になります。表記を統一し、異なる表記の同一クラスまで登録時に重複排除されるとは想定しないでください。 ### 登録順序だけで実行順序を保証しない Routerは、ルートに割り当てられたミドルウェアのエイリアスとグループを展開し、除外を処理してから優先順位で並べ替えます。先頭・末尾への追加だけで、認証やルートモデルバインディングとの厳密な前後関係が保証されるわけではありません。 さらに、HTTP Kernelの `syncMiddlewareToRouter()` は、Kernelが持つグループをRouterへ `middlewareGroup()` で同期します。Kernel側の構成が後から再同期されると、Routerへ直接追加したグループの内容が置き換わる場合があります。パッケージ間の起動順序で既存グループの変更を競うのではなく、利用アプリケーション側の `withMiddleware()` で構成を集約する方針を基本にします。 セッション開始後、認証後などの順序が必要な機能では、その前提をインストール手順に明記し、利用アプリケーションの優先順位設定と合わせて確認します。パッケージがアプリケーション全体の優先順位リストを丸ごと置き換えることは避けてください。 ## キャッシュと検証の境界 エイリアス登録を `routesAreCached()` の条件内やルートファイルへ置かないでください。`loadRoutesFrom()` はキャッシュがあるとルートファイルを読み込みませんが、キャッシュされたルートに残るエイリアスを実行時に解決するための登録は必要です。プロバイダーの `boot()` で毎回登録します。 ルートへの割り当てを変更した場合は、利用アプリケーションでルートキャッシュを再作成します。 ```bash theme={null} php artisan route:cache php artisan route:list --path=courier-status -vv ``` `-vv` はグループの内容を展開して確認するために使います。ただし、一覧表示が成功しても、ミドルウェア内の処理やセッションの利用まで保証するものではありません。HTTPリクエストのテストも必要です。 次は、上記の `/courier-status` を定義したLaravelアプリケーションのFeatureテストへ追加するメソッドです。 ```php theme={null} public function test_courier_header_is_applied(): void { $this->get('/courier-status') ->assertOk() ->assertHeader('X-Acme-Courier', 'enabled'); } ``` | 確認ケース | 期待する結果 | | - | - | | エイリアスを登録しただけのルート | ヘッダーは付かない | | エイリアスを割り当てたルート | ヘッダーが付く | | クラス名を直接割り当てたルート | 同じ動作になる | | キャッシュ作成後に新しいアプリケーションを起動 | 同じルートで同じ動作になる | | `web` 全体への適用を選択 | 対象の `web` ルートに適用され、対象外には広がらない | | 利用アプリケーション独自のグループ・優先順位を設定 | セッション・認証・バインディングとの順序が要件を満たす | グループの存在確認やエイリアスの配列を調べる単体テストと、実際のHTTP実行は別の検証です。特にキャッシュ検証は、同じプロセスのRouterを眺めるだけでなく、キャッシュ作成後の新しい起動で行います。長寿命ワーカーを利用している場合は、通常のデプロイ手順に従って再読み込みしてください。 ## 関連ページ 作成、ルートへの適用、パラメーター、優先順位の基本を確認します。 ルートの読み込みとキャッシュ作成時の設定の関係を確認します。 ## 参照した一次情報 * [Laravel公式ドキュメント: ミドルウェア](https://github.com/laravel/docs/blob/6bb0a3be3c623f7964c8277625a07b6df7f1de2e/middleware.md) * [Laravel公式ドキュメント: サービスプロバイダーのbootと依存注入](https://github.com/laravel/docs/blob/6bb0a3be3c623f7964c8277625a07b6df7f1de2e/providers.md#the-boot-method) * [Router: エイリアス登録・グループ操作・ミドルウェアの展開と整列](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Routing/Router.php) * [HTTP Kernel: syncMiddlewareToRouter](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Foundation/Http/Kernel.php) # パッケージのマイグレーション公開と更新 Source: https://kawax.biz/jp/advanced/package-migrations Laravel 13の実装から、publishesMigrationsとloadMigrationsFromの違い、公開時のタイムスタンプ変更、再公開のリスク、既存利用者へのスキーマ変更の届け方を解説します。 DBを使うパッケージを継続して保守するには、初回インストールだけでなく、すでにテーブルが存在する利用者へ変更を届ける手順が必要です。マイグレーションの公開、実行、実行履歴は別の処理として設計してください。 このページでは[パッケージ開発の基礎](/jp/advanced/package-development)を前提に、Laravel 13の `ServiceProvider`、`VendorPublishCommand`、`Migrator` を読み解きます。フレームワークの実装は `v13.34.0` を参照しています。 ## コピーして渡すか、パッケージから読み込むか | 方法 | プロバイダーの処理 | 利用者の操作 | ファイルの管理場所 | | - | - | - | - | | 公開する | `publishesMigrations()` | `vendor:publish` 後に `migrate` | アプリケーションの `database/migrations` | | 直接読み込む | `loadMigrationsFrom()` | `migrate` | インストールされたパッケージ内 | `publishesMigrations()` は、コピー元とコピー先を公開対象として登録するだけです。プロバイダーを起動しても、ファイルのコピーやSQLの実行は行いません。 一方、`loadMigrationsFrom()` はMigratorに検索パスを登録します。通常の `migrate` でそのパスのファイルも対象になりますが、プロバイダーの起動だけでは実行されません。 ```mermaid theme={null} flowchart TD A["パッケージのサービスプロバイダー"] --> B["publishesMigrations()
    コピー元とコピー先を登録"] B --> C["vendor:publish
    アプリケーションへコピー"] C --> E["migrate
    未実行のファイルを実行"] A --> D["loadMigrationsFrom()
    Migratorの検索パスに追加"] D --> E E --> F["実行済みのファイル名を
    migrationsテーブルに記録"] ``` 利用者が実行前にテーブル名やカラムを調整する設計なら、公開方式が候補になります。パッケージがスキーマを管理し、利用者によるファイル編集を前提としないなら、直接読み込み方式も検討できます。以下の2つのプロバイダー例は代替案です。 同じマイグレーションを公開しつつ直接読み込ませる設計は避けてください。公開時にタイムスタンプが変わると、コピー元とコピー先は別の実行履歴として扱われ、同じテーブル作成処理が二重に実行されるおそれがあります。 ## 公開方式を実装する パッケージ固有のタグを付け、利用者が他のリソースと区別して公開できるようにします。 ```php theme={null} publishesMigrations([ __DIR__.'/../database/migrations' => database_path('migrations'), ], 'courier-migrations'); } } ``` 初回インストールでは、対象プロバイダーとタグを指定してコピーし、内容を確認してから実行します。両方を指定した場合、Laravelはそのプロバイダーに属する、そのタグの公開対象を選びます。 ```bash theme={null} php artisan vendor:publish --provider="Acme\Courier\CourierServiceProvider" --tag=courier-migrations php artisan migrate:status php artisan migrate ``` ### タイムスタンプの変更には設定が関係する 公式ドキュメントは、公開時にマイグレーションのタイムスタンプを現在日時へ更新する動作を説明しています。ただし、`ServiceProvider::publishesMigrations()` の実装では、`database.migrations.update_date_on_publish` が有効な場合にだけ、コピー元をタイムスタンプ更新対象へ追加します。この設定を取得するときのフォールバックは `false` です。 Laravel 13の標準アプリケーションの `config/database.php` には、次の設定があります。旧構成を引き継いだアプリケーションでは、設定が存在するかも確認してください。 ```php theme={null} 'migrations' => [ 'table' => 'migrations', 'update_date_on_publish' => true, ], ``` さらに `VendorPublishCommand` は、登録されたコピー元の実パスと一致し、コピー先の名前に `YYYY_MM_DD_HHMMSS_` 形式がある場合に日時を書き換えます。コマンド開始時刻を基準に、対象ファイルごとに1秒加算します。名前にこの形式がなければ、その処理では日時を付け足しません。 ```text theme={null} コピー元: 2026_09_01_000000_create_courier_deliveries_table.php 公開後の例: 2026_10_02_120001_create_courier_deliveries_table.php ``` 上の公開後の日時は説明用です。実際のファイル名は公開した時刻によって変わります。 タイムスタンプ更新は、パッケージの登録と利用アプリケーションの設定の両方に依存します。パッケージのプロバイダーからこの設定を一律に変更せず、インストール手順に前提を記載してください。設定キャッシュを利用している場合は、設定変更後の再構築も必要です。 ## 再公開は「未実行のものだけ追加」ではない `vendor:publish` はDBの実行履歴を確認しません。また、`v13.34.0` のコピー処理では、既存ファイルの確認を**タイムスタンプの変更前**のコピー先に対して行います。ディレクトリ公開でも、まずコピー元と同じ相対パスがコピー先にあるかを確認し、その後で日時を書き換えます。 そのため、最初の公開で日時が変わり、コピー元と同じ名前のファイルがアプリケーション側にない場合は、同じタグをもう一度公開すると別の日時のファイルが追加されることがあります。`--force` を付けなければ常に重複を防げる、とは考えないでください。 | 条件 | 再公開時に注意すること | | - | - | | 日時更新が有効で、変更前のコピー先が存在しない | 別の日時のコピーが追加され得る | | 日時更新が無効で、コピー先が同名 | 通常は既存ファイルをスキップする | | `--force` を指定 | コピー条件を満たし、日時更新が有効なら別名になる場合もある | | `--existing` を指定 | 変更前のコピー先の存在を確認するため、日時変更済みファイルだけがあっても対象になるとは限らない | ### 実行済みかどうかはファイル名で判断する `Migrator::getMigrationName()` は、ファイルのベース名から `.php` を除いた文字列を返します。未実行の判定では、この名前と実行履歴を比較します。PHPの内容やテーブル名が同じかどうかによる判定ではありません。 ```text theme={null} 2026_10_02_120001_create_courier_deliveries_table 2026_10_03_090001_create_courier_deliveries_table ``` この2つは別のマイグレーション名です。前者が実行済みでも、後者はその履歴だけでは実行済みになりません。 パッケージ更新のたびに初回インストール用の公開コマンドを無条件で再実行したり、`--force` を標準手順にしたりしないでください。公開済みファイルの編集を上書きするだけでなく、日時変更によって重複した処理を追加する可能性があります。 ## 直接読み込み方式を実装する パッケージ内のマイグレーションをそのまま実行対象にしたい場合は、検索パスを登録します。この方式では、同じファイルを公開する処理は追加しません。 ```php theme={null} loadMigrationsFrom(__DIR__.'/../database/migrations'); } } ``` `loadMigrationsFrom()` はMigratorが解決されたときに `path()` を呼びます。`Migrator::path()` は検索パスの重複を除き、`getMigrationFiles()` は見つけたファイルをマイグレーション名でキー付けして、その名前順に並べます。 利用者がパッケージを更新すると、新しいファイルは次の `migrate` の対象になります。既存ファイルの名前は変えず、新しいスキーマ変更には新しいファイルを追加します。他のパッケージとの衝突を避けるため、`create_courier_deliveries_table` のように機能名も含めてください。同名のファイルは同じキーになり、両方が独立して実行されるわけではありません。 公開方式から直接読み込み方式への変更は、単なるプロバイダーの書き換えではありません。利用者の実行履歴が公開時の名前で記録されている場合、パッケージ側の元の名前と一致しません。既存利用者の履歴・公開済みファイル・ロールバックを含む移行手順が必要です。 ## スキーマ変更を既存利用者へ届ける たとえば配送テーブルへ追跡番号を追加する場合は、公開済みの `create_courier_deliveries_table` を編集するのではなく、変更用の新しいファイルを追加します。既存の作成マイグレーションを編集しても、実行済みの利用者にはその変更が実行されません。 ```php database/migrations/2026_10_02_000000_add_tracking_code_to_courier_deliveries_table.php theme={null} string('tracking_code')->nullable(); }); } public function down(): void { Schema::table('courier_deliveries', function (Blueprint $table) { $table->dropColumn('tracking_code'); }); } }; ``` 既存行があるテーブルへ追加する例なので、ここではnullableにしています。必須化やデータの埋め戻しが必要なら、別途その手順と実行順序を設計します。 公開方式では、利用者の公開済みファイルと照合し、**今回追加したファイルだけ**を届ける更新手順を用意します。新規ファイル専用の公開タグを設ける方法もありますが、日時更新が有効ならそのタグの繰り返し実行にも同じ注意が必要です。初回用タグを再実行するだけの更新手順にはしません。 直接読み込み方式では、更新後のコードで新しいファイルを検出できます。どちらの方式でも、ファイルが存在するだけではDBは変わらないため、リリースノートにマイグレーション実行の必要性を明記します。 ## リリース前に確認すること パッケージのDBテストに加えて、利用アプリケーションで公開と更新の手順を確認してください。テストでマイグレーションを直接読み込むだけでは、ファイル名が変わる公開方式を検証したことにはなりません。 * 空のDBに初回インストールし、必要なテーブルを作成できる。 * 旧リリースのDBと実行履歴から更新し、新しい変更だけが適用される。 * 同じ公開コマンドを繰り返したときのファイル一覧を確認し、更新手順が重複を生まない。 * 日時更新の有効・無効と、公開済みファイルの編集を考慮した手順になっている。 * 新規マイグレーションのロールバックと、アプリケーションの他のマイグレーションとの実行順序を確認する。 ## 関連ページ スキーマ定義、実行履歴、ロールバックの基本を確認します。 パッケージのサービスプロバイダーとDBをテストします。 利用者の設定と設定キャッシュを考慮した更新手順を確認します。 更新手順と互換性の変更をリリース方針に結び付けます。 ## 参照した一次情報 * [Laravel公式ドキュメント: パッケージのマイグレーション](https://github.com/laravel/docs/blob/156fc7fde114548640e13c39aa79b991291c3f91/packages.md#migrations) * [ServiceProvider: 公開対象と検索パスの登録](https://github.com/laravel/framework/blob/v13.34.0/src/Illuminate/Support/ServiceProvider.php) * [VendorPublishCommand: コピーとタイムスタンプ変更](https://github.com/laravel/framework/blob/v13.34.0/src/Illuminate/Foundation/Console/VendorPublishCommand.php) * [Migrator: ファイルの検出と未実行の判定](https://github.com/laravel/framework/blob/v13.34.0/src/Illuminate/Database/Migrations/Migrator.php) * [Laravel 13の標準アプリケーション: DB設定](https://github.com/laravel/laravel/blob/06d016a364a37430eec9cbc52209adce3d7667ce/config/database.php) # パッケージのキャッシュとoptimizeへの統合 Source: https://kawax.biz/jp/advanced/package-optimization Laravel 13のoptimizes()を使い、パッケージ独自のキャッシュ生成・削除をデプロイに統合します。登録キー、除外指定、実行順序、失敗時の終了コードを実装から解説します。 パッケージが独自のメタデータを事前生成する場合、利用者のデプロイ手順に専用コマンドを追加してもらうだけでは、更新時の実行漏れが起こりやすくなります。`ServiceProvider::optimizes()` を使うと、生成と削除のコマンドをLaravelの `optimize` と `optimize:clear` に組み込めます。 このページでは[パッケージ開発の基礎](/jp/advanced/package-development)を前提に、Laravel Framework `v13.35.0` の実装を確認します。キャッシュファイルの形式ではなく、登録と運用の契約を扱います。 ## コマンドの登録とタスクの登録を分ける `commands()` はArtisanから呼べるコマンドクラスを登録します。`optimizes()` は、すでに実行可能なコマンド名を最適化タスクとして登録する別の処理です。後者だけを呼んでも、コマンドクラスは登録されません。 次の例は、パッケージに `CacheMetadataCommand` と `ClearMetadataCommand` が実装済みで、それぞれの `$signature` が `courier:cache` と `courier:clear-cache` であることを前提にしています。 ```php theme={null} app->runningInConsole()) { $this->commands([ CacheMetadataCommand::class, ClearMetadataCommand::class, ]); $this->optimizes( optimize: 'courier:cache', clear: 'courier:clear-cache', key: 'acme-courier', ); } } } ``` `optimizes()` の引数はすべてnullableで、生成だけ、削除だけの登録も可能です。ただし、生成したキャッシュをどの手順で無効化するかは、必ず利用者に案内してください。 ## 登録キーも利用者向けの契約になる `ServiceProvider` は、生成コマンドを静的配列の `$optimizeCommands` に、削除コマンドを `$optimizeClearCommands` に保存します。どちらも `key` が配列キーになります。 `key` を省略すると、プロバイダーのクラス名から名前が生成されます。たとえば `CourierServiceProvider` なら `courier` です。クラス名だけを使うため、別の名前空間にある同名プロバイダーでも衝突し得ます。 同じキーに再登録すると、その側のコマンドが後の値で上書きされます。複数のタスクを登録したい場合は、別々のキーを指定してください。また、`config` や `routes` などLaravel標準タスクのキーは避けます。標準タスクとパッケージタスクをまとめる際にも、同じ文字列キーは上書きされるためです。 `acme-courier` のようにパッケージを識別できるキーを明示し、リリース間で維持してください。キーはタスクの表示名になり、利用者が `--except` で指定する値にもなります。 ## 標準タスクの後で実行される 確認した実装では、両コマンドは標準タスクの配列にパッケージの登録配列を展開してから、順番に呼び出します。衝突しないキーを使った場合、パッケージタスクは標準タスクの後に追加されます。 | コマンド | 標準タスクの実行順序 | その後の処理 | | - | - | - | | `optimize` | `config:cache` → `event:cache` → `route:cache` → `view:cache` | 登録された生成コマンド | | `optimize:clear` | `config:clear` → `cache:clear` → `clear-compiled` → `event:clear` → `route:clear` → `view:clear` | 登録された削除コマンド | ```mermaid theme={null} flowchart TD A["プロバイダーのboot()"] --> B["commands()でArtisanコマンドを登録"] A --> C["optimizes()でキーとコマンド名を登録"] B --> D["php artisan optimize"] C --> D D --> E["標準の設定・イベント・ルート・ビューをキャッシュ"] E --> F["courier:cacheを実行"] C --> G["php artisan optimize:clear"] G --> H["標準キャッシュを削除"] H --> I["courier:clear-cacheを実行"] ``` この順序を前提に、生成コマンドは他の標準キャッシュを作り直すのではなく、パッケージが所有するデータだけを生成します。複数パッケージ間の依存順序を制御するAPIとして `optimizes()` を使わず、厳密な順序が必要なら専用コマンドを明示的に並べます。 `optimize:clear` には `cache:clear` も含まれ、デフォルトのキャッシュストアのデータも削除します。パッケージ専用キャッシュだけを消したい場合は `courier:clear-cache` を直接実行してください。パッケージの削除コマンド自身も、共有ストア全体をflushせず、所有するキーやファイルだけを削除する設計にします。 ## キーまたはコマンド名で除外する 両コマンドの `--except` は、カンマ区切りの値を受け取ります。各値の前後の空白を取り除き、タスクのキーまたはコマンド名に一致したものを除外します。 ```bash theme={null} php artisan optimize --except=acme-courier php artisan optimize --except=courier:cache php artisan optimize:clear --except=acme-courier,cache ``` 最初の2つは同じ生成タスクを除外します。3つ目はパッケージの削除タスクと、標準の `cache:clear` を除外します。`cache` はタスクのキーで、パッケージ専用の名前ではありません。 除外指定はその実行だけに作用します。プロバイダーの登録を無効化したり、以前に作ったパッケージキャッシュを自動削除したりする設定ではありません。 ## タスクのFAILと親コマンドの終了コードを区別する `OptimizeCommand` と `OptimizeClearCommand` は各タスクを `callSilently()` で呼び、終了コードが `0` かどうかをタスク表示に渡します。通常の子コマンドの出力は表示されないため、原因の調査には専用コマンドを直接実行します。 Laravel `v13.35.0` の両 `handle()` は、子コマンドが非ゼロを返してもその値を親の戻り値として返しません。画面に `FAIL` と表示されてもループは続き、例外がなければ親コマンドの終了コードは `0` になります。一方、投げられた例外はタスク表示コンポーネントで再送出されるため、同じ継続動作ではありません。 `php artisan optimize` が終了コード `0` だったことだけで、パッケージキャッシュの生成成功を判断しないでください。この挙動は確認したバージョンの実装に基づくため、対応Laravelバージョンを更新するときにも確認します。 パッケージの生成がデプロイの必須条件なら、子コマンドの終了コードを直接確認できる手順にします。たとえば、以下はパッケージタスクを一括実行から除外し、標準タスクの後に1回だけ直接実行する例です。 ```bash theme={null} php artisan optimize --except=acme-courier && php artisan courier:cache ``` この例は `courier:cache` の失敗を終了コードに反映しますが、標準タスクの非ゼロ終了を集約するものではありません。標準タスクも厳密に検知する必要があるデプロイでは、必要なコマンドを個別に実行して終了コードを確認してください。 ## 更新に耐えるキャッシュの設計と確認 登録だけでなく、コマンドと読み取り側の責務も決めておきます。 * 生成は繰り返し実行しても同じ入力から同じ状態になり、途中失敗で不完全なデータを有効にしない。 * 削除はキャッシュが存在しない場合も正常に完了し、利用者の公開済み設定や永続データを削除しない。 * 生成に失敗したコマンドはエラーを報告して非ゼロを返す。読み取り側も壊れたキャッシュを無条件に正常扱いしない。 * キャッシュ形式を変更したら、利用者に再生成の必要性を案内し、長時間動くプロセスの再起動も検討する。 パッケージのテストに加え、利用アプリケーションでは以下を確認します。登録配列は静的なため、同一プロセスのテスト間では登録状態の持ち越しにも注意します。 | 確認する操作 | 完了条件 | | - | - | | 専用の生成・削除コマンドを直接実行 | 正常時は `0`、生成失敗時は非ゼロ。削除を2回実行しても成功する | | `optimize` / `optimize:clear` を実行 | パッケージタスクが1回ずつ呼ばれ、生成・削除後の状態が正しい | | キーとコマンド名で `--except` を指定 | 対象タスクだけが実行されない | | 生成コマンドを非ゼロ終了させる | `FAIL` 表示と、確認対象バージョンの親コマンドの終了コードを区別できる | | パッケージ更新後に再生成 | 新しいコードと設定で生成され、古い形式を読み続けない | ## 関連ページ 公開済み設定の補完と、設定キャッシュ再構築の関係を確認します。 プロバイダーとArtisanコマンドをテスト環境に登録します。 ## 参照した一次情報 * [Laravel公式ドキュメント: Optimize commands](https://github.com/laravel/docs/blob/13.x/packages.md#optimize-commands) * [ServiceProvider: optimizes()と登録キー](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Support/ServiceProvider.php) * [OptimizeCommand: タスクと除外処理](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Foundation/Console/OptimizeCommand.php) * [OptimizeClearCommand: 削除タスクと実行順序](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Foundation/Console/OptimizeClearCommand.php) * [Command: handle()の戻り値と終了コード](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Console/Command.php) * [Task: 結果表示と例外の再送出](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Console/View/Components/Task.php) # パッケージのルート登録とキャッシュ Source: https://kawax.biz/jp/advanced/package-routes Laravel 13の実装から、loadRoutesFromの役割、ミドルウェアと名前の分離、設定変更をルートキャッシュへ反映する手順を解説します。 パッケージがHTTPエンドポイントを提供する場合は、開発環境でルートが動くだけでなく、利用アプリケーションがルートキャッシュを作成した後も同じ契約で動く必要があります。URLのプレフィックスや有効・無効を設定で変更できる設計では、その変更をいつ反映するかも利用者に案内します。 このページでは[パッケージ開発の基礎](/jp/advanced/package-development)を前提に、登録処理とキャッシュのライフサイクルを分けて考えます。公式ドキュメントはLaravel 13のデフォルトブランチ `13.x`、フレームワークの実装は最新リリースの `v13.34.0` を参照しています。 ## loadRoutesFromはファイルを読み込むだけ `ServiceProvider::loadRoutesFrom()` は、アプリケーションが `CachesRoutes` を実装し、`routesAreCached()` が真の場合にはルートファイルを読み込みません。それ以外の場合は、指定ファイルを `require` します。 このメソッド自体は、URIやルート名のプレフィックス、コントローラーの名前空間、ミドルウェアを追加しません。ファイルの公開や、既存キャッシュへのルートの追加も行いません。 ```mermaid theme={null} flowchart TD A["プロバイダーのboot"] --> B["loadRoutesFromを呼び出す"] B --> C{"ルートキャッシュがある?"} C -->|いいえ| D["パッケージのルートファイルをrequire"] C -->|はい| E["パッケージのファイル読み込みをスキップ"] E --> F["LaravelのRouteServiceProviderが
    アプリケーションのキャッシュを読み込む"] ``` 図は標準のLaravelアプリケーションを前提にしています。パッケージ専用のキャッシュがあるのではなく、アプリケーション全体のルートキャッシュにパッケージのルートも含まれます。 パッケージ内のファイル名を `routes/web.php` にしても、それだけでは `web` ミドルウェアは付きません。アプリケーション側の標準ルートファイルとは読み込み経路が異なるため、必要なミドルウェアをパッケージ側で明示してください。 ## 設定と登録を分ける 次の例では、パッケージが応答可能かを返す公開エンドポイントを作ります。ComposerのPSR-4で `Acme\Courier\` を `src/` へ対応させ、プロバイダーを[自動検出](/jp/advanced/package-discovery)または手動登録していることを前提にします。 ```php config/courier.php theme={null} [ 'enabled' => true, 'prefix' => 'acme-courier', ], ]; ``` 設定のマージは `register()`、ルートの読み込みは `boot()` で行います。HTTPルートを登録するプロバイダーを `DeferrableProvider` にしないでください。ルートが必要な時点でプロバイダーが起動する保証がなくなるためです。 ```php src/CourierServiceProvider.php theme={null} mergeConfigFrom(__DIR__.'/../config/courier.php', 'courier'); } public function boot(): void { if (config('courier.routes.enabled')) { $this->loadRoutesFrom(__DIR__.'/../routes/web.php'); } } } ``` この条件はルートの登録だけを制御します。他のサービスやビューの登録もあるプロバイダーでは、それらをこの条件の内側に入れないようにします。設定を利用者へ公開する方法や、ネストした設定のマージに関する注意点は[パッケージ設定のマージとキャッシュ](/jp/advanced/package-config-merging)を参照してください。 ```php routes/web.php theme={null} prefix(config('courier.routes.prefix')) ->name('acme-courier.') ->group(function () { Route::get('/status', StatusController::class)->name('status'); }); ``` ```php src/Http/Controllers/StatusController.php theme={null} json(['available' => true]); } } ``` デフォルトのURIは `/acme-courier/status`、ルート名は `acme-courier.status` です。`route('acme-courier.status')` でURLを生成すれば、URIのプレフィックスを変更しても呼び出し側は同じルート名を使えます。グループの `name()` は文字列をそのまま連結するため、末尾の `.` も指定します。 `web` は認証・認可の代わりではありません。この例は機密情報を含まない公開エンドポイントです。利用者のデータを返すエンドポイントには、仕様に応じた認証ミドルウェアと認可処理を別途設けてください。 ## URIとルート名の衝突を別々に防ぐ URIのプレフィックスとルート名のプレフィックスは別の仕組みです。片方を付けただけでは、もう片方の衝突を防げません。 | 対象 | この例の設計 | 保守上の注意 | | - | - | - | | URI | `acme-courier` をデフォルトにし、設定で変更可能 | 利用アプリケーションの既存URLと競合しない値を選ぶ | | ルート名 | `acme-courier.` を固定 | パッケージ固有の名前にし、URL生成の契約として維持する | | コントローラー | クラス参照を使用 | アプリ側のコントローラー名前空間に依存させない | | ミドルウェア | `web` を明示 | 対象アプリのミドルウェア構成と合わせて確認する | `AbstractRouteCollection` はキャッシュ用のルートコレクションを作る際、別のルートに同じ名前が付いていると `LogicException` を投げる処理を持ちます。「通常起動でURLを生成できた」だけではキャッシュ可能であることを保証できません。URIが異なる2つのルートでも、名前が同じなら問題になります。 利用アプリケーションのルートを登録順序で上書きすることを、パッケージの拡張方法にしないでください。必要ならルートを無効にする設定と、利用者が別ルートから呼び出せるサービスを提供します。 ## キャッシュ作成時の設定がルート定義に残る `RouteCacheCommand` は、まず `route:clear` を実行し、新しいアプリケーションを起動してルートを収集します。そのルートをシリアライズできる形に準備し、コンパイル結果をキャッシュファイルへ書き込みます。 このときパッケージのルートファイルも読み込まれるため、プレフィックスや登録の有無は**キャッシュ作成時の設定**で決まります。以降の起動では `loadRoutesFrom()` がファイルを読み込まず、キャッシュされたルートが使われます。 | 変更 | 古いルートキャッシュが残った場合 | 必要な対応 | | - | - | - | | パッケージにルートを追加 | 追加したルートが現れない | ルートキャッシュを再作成する | | `routes.prefix` を変更 | 元のURIが残る | 新しい設定で再作成する | | `routes.enabled` を `false` に変更 | キャッシュにあるルートは消えない | 無効化した設定で再作成する | | パッケージを削除 | 削除したクラスを参照する定義が残り得る | 削除後の構成で再作成する | `routes.enabled` は登録を制御する設定であり、リクエストごとのアクセス拒否ではありません。古いキャッシュが残っている状態で設定だけを無効にしても、エンドポイントを停止したことにはなりません。 ユーザーやテナントなど、リクエストごとに変わる条件でルートを登録しないでください。その条件はキャッシュ作成時のCLI環境で評価されます。ルートは安定した構成で登録し、アクセスの可否はミドルウェアやコントローラー内の認可で判断します。 ### デプロイでは設定を先に確定する コードと設定の更新を済ませてから、設定キャッシュを利用する構成では次の順序で再作成します。利用アプリケーションのデプロイ処理に組み込んでください。 ```bash theme={null} php artisan config:cache php artisan route:cache php artisan route:list --name=acme-courier -vv ``` 古い設定キャッシュが残ったまま `route:cache` を実行すると、ルートも古い設定で作られます。`config:cache` だけを再実行しても、ルートキャッシュは更新されません。`-vv` でミドルウェアグループの内容も確認できます。 開発中にキャッシュなしで動作を確認する場合は、必要に応じて両方をクリアします。 ```bash theme={null} php artisan config:clear php artisan route:clear ``` ルートファイルは、キャッシュがある起動では実行されません。そこでイベントリスナーやコンテナのバインディングを登録すると動作が変わるため、ルート定義以外の副作用を持たせないでください。長時間動くプロセスを使用する環境では、キャッシュ更新後の再読み込みも通常のデプロイ手順に含めます。 ## リリース前に確認する組み合わせ パッケージのテストに加え、Laravel 13の利用アプリケーションで次の組み合わせを確認します。インメモリのルート登録だけでなく、Artisanが新しいアプリケーションを起動する経路も対象にします。 * キャッシュなしで `/acme-courier/status` が応答し、ルート名とミドルウェアが想定どおりである。 * `route:cache` が成功し、新しい起動でも同じURI・ルート名で応答する。 * プレフィックスを変更してキャッシュを再作成すると、新しいURIが応答し、旧URIのパッケージルートがなくなる。 * 無効化してキャッシュを再作成すると、`route:list --name=acme-courier` に対象ルートが出ない。 * 利用アプリケーションや他のパッケージと、URI・ルート名が衝突しない。 古いキャッシュを残すケースも確認すると、「設定ファイルは変えたのにURLが変わらない」という利用者の報告を再現できます。キャッシュの再作成をアップグレード手順へ明記し、ルート名やミドルウェアの変更も互換性の検討対象にします。 ## 関連ページ ルートグループ、名前付きルート、一覧表示の基本を確認します。 公開済み設定と設定キャッシュを考慮した更新手順を確認します。 ルートを登録するプロバイダーを遅延させない理由を確認します。 公開APIの変更をリリース方針と継続検証に結び付けます。 ## 参照した一次情報 * [Laravel公式ドキュメント: パッケージのルート](https://github.com/laravel/docs/blob/156fc7fde114548640e13c39aa79b991291c3f91/packages.md#routes) * [Laravel公式ドキュメント: ルーティング](https://github.com/laravel/docs/blob/156fc7fde114548640e13c39aa79b991291c3f91/routing.md) * [ServiceProvider: loadRoutesFromの実装](https://github.com/laravel/framework/blob/v13.34.0/src/Illuminate/Support/ServiceProvider.php) * [RouteServiceProvider: キャッシュの読み込み](https://github.com/laravel/framework/blob/v13.34.0/src/Illuminate/Foundation/Support/Providers/RouteServiceProvider.php) * [RouteCacheCommand: 新しいアプリケーションでの収集と保存](https://github.com/laravel/framework/blob/v13.34.0/src/Illuminate/Foundation/Console/RouteCacheCommand.php) * [AbstractRouteCollection: ルート名の重複検出](https://github.com/laravel/framework/blob/v13.34.0/src/Illuminate/Routing/AbstractRouteCollection.php) # パッケージサービスのライフサイクル設計 Source: https://kawax.biz/jp/advanced/package-service-lifetimes Laravel 13のbind・singleton・scopedの内部実装を読み解き、長寿命ワーカーで状態を持ち越さないパッケージ設計とライフサイクル境界のテストを解説します。 パッケージを通常のHTTPリクエストだけでなく、Octaneやキューワーカーでも使えるようにするには、サービスが**どの期間、何を保持してよいか**を決める必要があります。リクエストごとのデータをsingletonに保存すると、別の処理でも古いデータを参照する可能性があります。 このページではLaravel Framework `v13.36.0` のコンテナとキューワーカー、およびOctaneの実装を確認し、[サービスコンテナ](/jp/service-container)の基本的な登録方法をパッケージの長期保守に結び付けます。`scoped()` を選ぶだけで、オブジェクト間の参照や静的状態がすべてリセットされるわけではありません。 ## 登録方法はサービスの保持期間で選ぶ | 登録 | 通常の `make()` による取得 | 向いている設計 | | - | - | - | | `bind()` | 解決のたびに生成する | 操作単位で生成してよいサービス | | `singleton()` | 同じコンテナでは生成済みインスタンスを再利用する | リクエスト・ジョブ固有の状態を保持しないサービス | | `scoped()` | 同じライフサイクルでは再利用し、境界で保存済みインスタンスを除去する | 1リクエスト・1ジョブ内で共有する状態 | これは通常の解決を比較した表です。パラメーターを明示した解決やcontextual bindingなどの特殊な経路は別に確認してください。 `bind()` でも、取得したサービスを別のsingletonのプロパティに保存すれば、その参照は残ります。登録方法はコンテナの生成・再利用規則であって、利用者が保持するオブジェクトまで強制的に作り直す規則ではありません。 ## scopedは共有登録と除去対象の記録を組み合わせる 確認した `Container::scoped()` は、次の2つを行います。 1. 登録キーを `scopedInstances` に記録する。 2. `singleton()` を呼び、共有バインディングとして登録する。 インスタンス自体はsingletonと同じ `instances` に保存されます。`forgetScopedInstances()` は、記録されたキーについて保存済みインスタンスを `unset()` します。バインディングの生成処理は残るため、次の通常の解決で新しいオブジェクトを作れます。 ```mermaid theme={null} flowchart TD A["register()でscopedを登録"] --> B["生成方法と除去対象のキーを記録"] B --> C["処理Aで初めてmake()"] C --> D["インスタンスAを生成・保存"] D --> E["処理A内では同じインスタンスを取得"] E --> F["ライフサイクル境界で
    forgetScopedInstances()"] F --> G["コンテナの保存済みインスタンスを除去"] G --> H["処理Bのmake()でインスタンスBを生成"] ``` この処理は、古いオブジェクトのプロパティを初期化したり、別オブジェクトが持つ参照を書き換えたりしません。参照が残っている古いオブジェクトは、引き続き利用できてしまいます。 ### 境界を作るのは実行環境 コンテナ自身がHTTPリクエストやジョブの開始を監視しているわけではありません。 * **キューワーカー**: `QueueServiceProvider` がスコープをリセットするコールバックをWorkerに渡します。`Worker::daemon()` は次のジョブを取得する前のループでこれを呼び、コールバック内で `forgetScopedInstances()` とFacadeの解決済みインスタンスのクリアなどを行います。 * **Octane**: 公式の既定設定では `OperationTerminated` に `FlushTemporaryContainerInstances` を登録します。このリスナーが `forgetScopedInstances()` を呼びます。 * **独自の常駐コマンドやループ**: 単に `make()` を繰り返しても境界にはなりません。実行基盤側でライフサイクルを設計する必要があります。 通常のアプリケーション処理やパッケージの `boot()` から、独自判断で `forgetScopedInstances()` を呼ばないでください。コンテナ全体のscopedサービスに影響します。また、同期実行を含むすべてのジョブ経路が、常駐Workerと同じリセット経路を通るとは限りません。 ## 処理内で共有する状態をscopedにする 配送パッケージが、1つのリクエスト・ジョブ内で配送先の識別子を共有したい例を考えます。HTTPのRequestを直接保持せず、必要な値だけを保存します。 ```php theme={null} destinationId = $destinationId; } public function destinationId(): ?string { return $this->destinationId; } } ``` パッケージのサービスプロバイダーでは、生成時に利用者の状態を読み取らず、空のcontextを登録します。 ```php theme={null} app->scoped(DeliveryContext::class); } } ``` 利用アプリケーションのコントローラーやジョブの `handle()` で、入力の検証や権限の確認を済ませた識別子を設定します。同じライフサイクルでこの型を解決する他のサービスは、同じcontextを参照できます。 ```php theme={null} $context = app(\Acme\Courier\DeliveryContext::class); $context->setDestinationId($authorizedDestinationId); ``` このcontextは認可機能ではありません。識別子を設定できることと、その配送先へのアクセスが許可されていることは別です。また、`scoped()` はデータベースや外部キャッシュへの永続化でもありません。次のジョブに渡す値はジョブのデータとして明示的に渡します。 Octaneではサービスプロバイダーの `register()` と `boot()` はワーカーの初期起動時に実行されます。現在のユーザーや配送先を `boot()` でcontextに保存する設計にしないでください。処理ごとの値は、処理が始まってから設定します。 ## singletonからscopedへの参照は自動更新されない 次の登録は、contextだけをscopedにしても意図通りにはなりません。 ```php theme={null} $this->app->singleton(DeliveryService::class, function ($app) { return new DeliveryService($app->make(DeliveryContext::class)); }); ``` `DeliveryService` がコンストラクターでcontextをプロパティに保存するなら、最初に生成したcontextを持ち続けます。コンテナからcontextの保存を除去しても、すでに生成済みの `DeliveryService` のプロパティは変わりません。 対策は、サービスの責務に合わせて選びます。 1. **処理ごとに必要な値をメソッド引数で渡す**。状態を共有する必要がなければ、これが単純です。 2. **保持する側もscopedにする**。`DeliveryService` とcontextの両方を処理単位で作り直します。 3. **利用時に解決するresolverを渡す**。singletonを維持する必要がある場合は、現在のコンテナから必要なサービスをその都度取得します。 3番目の例では、古いcontextをクロージャーに取り込むのではなく、呼び出す時点で解決します。 ```php theme={null} use Illuminate\Container\Container; $this->app->singleton(DeliveryService::class, function () { return new DeliveryService( fn () => Container::getInstance()->make(DeliveryContext::class), ); }); ``` この例の `DeliveryService` は、コンストラクターで `Closure` を受け取り、**操作のたびに呼び出すAPI**へ変更する必要があります。コンストラクター内でresolverを一度だけ呼び、その結果を保存しては解決になりません。これは公式Octaneドキュメントのcontainer resolverを、パッケージ固有のcontextに応用した設計例です。 ### scopedで消えない状態も確認する `scoped()` は、次の状態を自動的には除去しません。 * パッケージ独自の静的プロパティに追加した値 * 別のsingletonやグローバル変数に保存したcontextへの参照 * singletonの内部に保持した「現在のユーザー」「現在の配送先」 * 独自のFacade・キャッシュ層が保持する参照 標準の実行基盤によるリセットと、パッケージ独自の状態のリセットは区別します。処理ごとの状態を保持する場所を減らす方が、すべての実行環境で独自のクリア処理を維持するより単純です。 ## 同じコンテナで境界をまたぐテストを書く HTTPテストが毎回新しいアプリケーションを作るだけでは、常駐プロセスで同じコンテナを使い続ける問題を見逃すことがあります。まずはコンテナ単体で、同じ処理内の共有と境界後の再生成を検証します。 次のテストはPHPUnit形式です。最初のテストは公式のContainerテストと同様に `forgetScopedInstances()` を使い、Worker全体を起動せず境界の作用を確認します。 ```php theme={null} scoped(DeliveryContext::class); $first = $container->make(DeliveryContext::class); $first->setDestinationId('destination-a'); $this->assertSame($first, $container->make(DeliveryContext::class)); $this->assertSame('destination-a', $first->destinationId()); $container->forgetScopedInstances(); $second = $container->make(DeliveryContext::class); $this->assertNotSame($first, $second); $this->assertNull($second->destinationId()); } public function test_singleton_keeps_a_captured_scoped_dependency(): void { $container = new Container; $container->scoped(DeliveryContext::class); $container->singleton('courier.holder', function ($app) { return (object) ['context' => $app->make(DeliveryContext::class)]; }); $holder = $container->make('courier.holder'); $oldContext = $holder->context; $container->forgetScopedInstances(); $this->assertSame($holder, $container->make('courier.holder')); $this->assertSame($oldContext, $holder->context); $this->assertNotSame($oldContext, $container->make(DeliveryContext::class)); } } ``` 2番目は、参照が残る仕様を記録するテストです。実際のパッケージには、採用した対策で新しいcontextや値が使われることを検証するテストも追加します。 | 更新時の確認 | 期待する結果 | | - | - | | 同じ処理内でcontextを複数回解決 | 同じインスタンス・設定した値を共有する | | 同じコンテナで境界をまたいで解決 | 新しいインスタンスで、前の値を持たない | | singletonがcontextを利用 | 古いcontextを保存せず、現在の値を使う | | 利用アプリでOctaneの連続リクエストを実行 | 2回目が1回目の配送先などを引き継がない | | 常駐キューワーカーで連続ジョブを実行 | ジョブごとの設定値を分離する | | Laravel・Octaneを更新 | リセットの呼び出し経路と利用アプリの設定を再確認する | コンテナ単体テストは、Octaneのsandboxやキューワーカーの実際の実行経路を検証するものではありません。対応を表明する実行環境で連続処理を行う結合テストも必要です。登録方法やサービス実装を変更したデプロイでは、常駐プロセスを再読み込みして新しいコードを適用します。 ## 関連ページ afterResolvingと共有インスタンスの取得、再解決時の設定の違いを確認します。 Requestやコンテナの保持、常駐プロセスの運用上の注意点を確認します。 ## 参照した一次情報 * [Laravel公式ドキュメント: Binding scoped singletons](https://github.com/laravel/docs/blob/13.x/container.md#binding-scoped-singletons) * [Laravel公式ドキュメント: Dependency injection and Octane](https://github.com/laravel/docs/blob/13.x/octane.md#dependency-injection-and-octane) * [Container: scoped()・singleton()・forgetScopedInstances()・resolve()](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Container/Container.php) * [ContainerTest: scopedの共有と再生成](https://github.com/laravel/framework/blob/v13.36.0/tests/Container/ContainerTest.php) * [QueueServiceProvider: Workerに渡すresetScope](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Queue/QueueServiceProvider.php) * [Worker: daemon()でのスコープリセット](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Queue/Worker.php) * [Octane: FlushTemporaryContainerInstances](https://github.com/laravel/octane/blob/ecc9254e6d79cd607f191015f634f9be944e2267/src/Listeners/FlushTemporaryContainerInstances.php) * [Octane: OperationTerminatedの既定リスナー](https://github.com/laravel/octane/blob/ecc9254e6d79cd607f191015f634f9be944e2267/config/octane.php) # パッケージのサービス解決後フック Source: https://kawax.biz/jp/advanced/package-service-resolution Laravel 13のcallAfterResolving()を読み解き、未解決・解決済みのサービスを拡張する方法と、singleton・通常バインディング・再登録時の注意点を解説します。 パッケージからLaravelのサービスに機能を追加するとき、使われるまでインスタンスを作らず、すでに作られている場合にも設定を反映したいことがあります。サービスプロバイダーの `callAfterResolving()` は、この2つを組み合わせるためのprotectedメソッドです。 このページではLaravel Framework `v13.35.0` の実装を確認し、[パッケージ開発の基礎](/jp/advanced/package-development)にある `boot()` からの拡張を掘り下げます。アプリケーション全体の起動完了を待つフックではなく、指定したサービスの解決に対するフックです。 ## 未解決なら待ち、解決済みなら今も実行する `ServiceProvider::callAfterResolving()` は、次の2段階の処理を行います。 1. コンテナの `afterResolving()` にコールバックを登録する。 2. `resolved()` がtrueなら `make()` で取得し、その場でもコールバックを呼ぶ。 未解決の場合、このメソッド自身は対象を `make()` しません。通常の解決では、コンテナがオブジェクトを構築し、extenderを適用し、`resolving` コールバックを呼んだ後に `afterResolving` コールバックを呼びます。 ```mermaid theme={null} flowchart TD A["boot()でcallAfterResolving()"] --> B["afterResolvingに登録"] B --> C{"対象はresolved()?"} C -->|No| D["今は対象を生成しない"] D --> E["後で対象が解決される"] E --> F["登録したコールバックを実行"] C -->|Yes| G["make()で対象を取得"] G --> H["その場でもコールバックを実行"] ``` | 方法 | 登録時に未解決 | 登録時に解決済みのsingleton | | - | - | - | | `$this->app->make()` して直接設定 | その場で生成する | 既存インスタンスを設定する | | `$this->app->afterResolving()` | 今後の解決を待つ | 登録だけでは既存インスタンスを設定しない | | `$this->callAfterResolving()` | 今後の解決を待つ | 既存インスタンスにもその場で設定する | 通常のsingleton取得では、コンテナは保存済みインスタンスを早期returnするため、取得のたびに `afterResolving` が発火するわけではありません。単に `afterResolving()` を登録するだけでは、登録前に作られたsingletonへの設定が抜ける点が重要です。 ## バリデーションFactoryにルールを追加する パッケージ独自の文字列ルールを提供する例として、`validator` の解決後にルールを登録します。公式の `ValidationServiceProvider` はこのキーをsingletonとして登録しており、プロバイダー自体は遅延読み込みに対応しています。 ```php theme={null} callAfterResolving('validator', function (Factory $factory): void { $factory->extend( 'courier_tracking_code', static function ($attribute, $value, $parameters, $validator): bool { return is_string($value) && preg_match('/\ACR-[0-9]{8}\z/', $value) === 1; }, 'The :attribute must be a valid courier tracking code.', ); }); } } ``` 利用アプリケーションでは、通常のルールと組み合わせて使います。 ```php theme={null} $request->validate([ 'tracking_code' => ['required', 'string', 'courier_tracking_code'], ]); ``` この例の `extend()` は、`Illuminate\Validation\Factory` のルール登録APIです。後述するコンテナの `extend()` とは別のメソッドです。通常のカスタムルールは空値などで実行されない場合があるため、必須性は `required` で指定します。ルール自体の詳しい設計は[カスタムバリデーションルール](/jp/advanced/custom-validation-rules)を参照してください。 `Factory::extend()` は同じルール名の配列要素を上書きするので、この例のように同じ処理を再登録してもエントリーが増えません。ただし、別パッケージのルールを上書きしないよう、公開するルール名にはパッケージ固有の接頭辞を付けます。 コールバックが実行されるのはFactoryの解決時です。ルールの検証用クロージャが実行されるのは、後でValidatorが対象の値を検証するときです。フックの登録時に入力値を検証するわけではありません。 ## 対象のキーと解決済み判定をそろえる コンテナの通常の解決イベントでは、登録したキーとの一致だけでなく、取得したオブジェクトの型に一致するコールバックも選ばれます。一方、`callAfterResolving()` がその場で使う `resolved($name)` は、別名を正規化したキーについて、解決済みフラグまたは保存済みインスタンスがあるかを調べます。生成済みオブジェクトをすべて検索する処理ではありません。 そのため、あるインターフェースやクラスが解決済みだからといって、別のキーでの `resolved()` も必ずtrueになるとは限りません。対象サービスの実際のバインディングとaliasを確認し、解決済みケースも同じキーでテストします。上の例では、公式プロバイダーが登録する `validator` を指定しています。 また、`resolved()` は「過去に解決された」という判定も含みます。現在のインスタンスが必ず残っている、という意味ではありません。 ## 「一度だけ実行」を保証するAPIではない `callAfterResolving()` は、コールバックを登録したままにします。その場で実行された後も、将来オブジェクトが新しく解決されれば登録済みコールバックが実行されます。さらに、メソッドを再度呼べばコールバック自体が追加されます。 特に、通常の非共有バインディングをすでに解決した場合は注意が必要です。確認した実装では次のようになります。 1. 新しいコールバックを登録する。 2. `resolved()` がtrueなので `make()` を呼ぶ。 3. `make()` が新しいオブジェクトを作り、その解決イベントでコールバックが実行される。 4. `make()` から戻った同じオブジェクトに、その場のコールバックも実行される。 この経路では、今回登録したコールバックが同じオブジェクトに2回適用されます。singletonの取得だけを想定した設計を、通常バインディングにそのまま流用しないでください。 コールバック内でメール送信、外部API呼び出し、課金、無条件のリスナー追加などを行わないでください。サービスへの設定を冪等に適用する用途に絞り、再登録や再解決で副作用が重複しない設計にします。 ## オブジェクトの置換には別のAPIを使う `afterResolving` のコールバックの戻り値は、コンテナが返すオブジェクトの置換には使われません。デコレーターを返してサービスそのものを差し替えたい場合は、コンテナの `extend()` を検討します。このAPIのクロージャは変更後のサービスを返す契約です。 同様に、`callAfterResolving()` は別オブジェクトにすでに保存された依存関係をすべて更新する仕組みでもありません。再バインド時の依存関係の更新が目的なら、公式ドキュメントの `rebinding()` と対象クラスの設計を確認します。 サービスを本当に遅延読み込みするプロバイダーの要件は[DeferrableProvider](/jp/advanced/deferred-provider)を参照してください。フックを使うことと、そのフックを登録するプロバイダー自身を遅延読み込みにすることは別です。 ## 更新時に確認する組み合わせ パッケージのリリース前には、通常の起動順だけでなく、先に別プロバイダーがサービスを利用した場合も検証します。 | ケース | 確認すること | | - | - | | 対象が未解決の状態でフックを登録 | 登録だけでは対象を生成せず、初回解決で設定が反映される | | singletonを先に解決してから登録 | 同じインスタンスにその場で設定が反映される | | 設定済みsingletonを通常の `make()` で再取得 | 同じインスタンスが返り、解決コールバックが重複実行されない | | 過去に解決した通常バインディングに登録 | 登録時の `make()` と明示呼び出しの2回適用でも壊れない | | 複数回のフック登録やインスタンス再生成 | 同じルール・リスナー・パスなどが意図せず蓄積しない | | Laravelの対応バージョンを更新 | バインディングキー、共有性、フックの実行経路を再確認する | アプリケーション全体で一度だけ使ったことを静的フラグで記録すると、新しいコンテナやテストで作り直されたサービスへの設定が抜ける可能性があります。設定の冪等性は、できるだけ対象オブジェクトや登録するキーの単位で確保します。 ## 関連ページ loadViewsFrom()が解決後フックを使ってビュー名前空間を登録する例を確認します。 Translatorの名前空間登録と、読み込み済み翻訳の扱いを確認します。 ## 参照した一次情報 * [Laravel公式ドキュメント: パッケージ開発](https://github.com/laravel/docs/blob/13.x/packages.md) * [Laravel公式ドキュメント: Container events・Extending bindings・Rebinding](https://github.com/laravel/docs/blob/13.x/container.md#container-events) * [Laravel公式ドキュメント: カスタムバリデーションルール](https://github.com/laravel/docs/blob/13.x/validation.md#custom-validation-rules) * [ServiceProvider: callAfterResolving()とリソース登録での利用](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Support/ServiceProvider.php) * [Container: resolved()・resolve()・afterResolving()](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Container/Container.php) * [ValidationServiceProvider: validatorのsingleton登録](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Validation/ValidationServiceProvider.php) * [Validation Factory: extend()によるルール登録](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Validation/Factory.php) # パッケージの静的解析(PHPStan / Larastan) Source: https://kawax.biz/jp/advanced/package-static-analysis PHPStan と Larastan を使って Laravel パッケージの型安全性と保守性を高めるためのセットアップ、設定、型注釈、CI 自動実行を解説します。 Laravel パッケージは一度リリースして終わりではありません。Laravel 本体、PHP、依存パッケージの更新に追従しながら年単位でメンテナンスするには、テストに加えて静的解析も継続的に回す必要があります。 このページは [Laravelパッケージ開発](/jp/advanced/package-development)、[Orchestra TestbenchでLaravelパッケージをテストする](/jp/advanced/package-testing)、[パッケージのバージョン互換性管理](/jp/advanced/package-versioning) の補助ガイドです。実装・テスト・バージョン戦略に静的解析を追加すると、長期保守しやすいパッケージになります。 ## 静的解析とは 静的解析は、コードを実行せずに型の不整合や潜在的なバグを検出する手法です。Laravel パッケージでは、サービスコンテナ、Facade、Eloquent など動的な仕組みが多いため、動作確認だけでは見落としやすい問題を早い段階で拾えます。 静的解析を導入すると、特に次の効果があります。 * **型安全性の向上** — 間違った引数や戻り値をレビュー前に検出できる * **バグの早期発見** — 存在しないメソッド呼び出しや nullable の見落としをテスト前に把握できる * **IDE 補完の改善** — PHPDoc と Generics を整えることで補完精度が上がる * **長期保守の安定化** — Laravel や PHP のアップグレード時に壊れた箇所を洗い出しやすい ## PHPStan のセットアップ まずは PHPStan 単体で解析基盤を用意します。PHPStan 2.x は `mixed` や nullable の扱いが以前より厳密なので、パッケージの公開 API を整えるきっかけにもなります。 ```bash theme={null} composer require --dev phpstan/phpstan:^2.0 ``` パッケージでは `src` と `tests` を最初の対象にすることが多いです。 ```bash theme={null} vendor/bin/phpstan analyse src tests ``` CI とローカルで同じコマンドを使えるように、`composer.json` に script を定義しておくと運用しやすくなります。 ```json theme={null} { "scripts": { "analyse": "phpstan analyse" } } ``` ## Larastan のセットアップ Laravel パッケージでは PHPStan だけだと、コンテナ解決、Facade、Eloquent リレーションなど Laravel 固有の仕組みを十分に理解できません。そこで PHPStan の Laravel 拡張である Larastan を併用します。 現在のパッケージ名は `larastan/larastan` です。古い記事では `nunomaduro/larastan` が出てくることがあります。 ```bash theme={null} composer require --dev larastan/larastan:^3.0 ``` Larastan は Laravel アプリケーションコンテナを起動して型を解決します。Laravel パッケージを単体解析する場合は、`orchestra/testbench` が必要になることがあります。 ```bash theme={null} composer require --dev orchestra/testbench ``` `phpstan.neon` で Larastan の設定を include します。 ```neon theme={null} includes: - vendor/larastan/larastan/extension.neon ``` ## phpstan.neon の設定 `phpstan.neon` は解析レベル、対象パス、除外パス、例外ルールをまとめる中心設定です。Laravel パッケージなら最初から 100 点を目指すより、継続的に上げられる設定にしておく方が現実的です。 以下は `phpstan.neon` の例です。 ```neon theme={null} includes: - vendor/larastan/larastan/extension.neon parameters: level: 5 paths: - src - tests excludePaths: analyse: - vendor - workbench - bootstrap/cache/* ignoreErrors: - message: '#Call to an undefined method Illuminate\\Support\\HigherOrderCollectionProxy::#' path: tests/Fixtures/* ``` ### レベル 0〜9 の選び方 PHPStan は段階的に導入できます。まずは現在のコードベースを回しきれるレベルから始めて、修正が進んだら上げていく運用が安全です。 | レベル | 向いている段階 | | - | - | | 0〜3 | まず未知のクラス・関数・メソッド呼び出しを減らしたい段階 | | 4〜6 | 公開 API の引数・戻り値・プロパティ型を整えたい段階 | | 7〜9 | nullable、union、`mixed` を厳密に扱いたい長期保守フェーズ | PHPStan 2.x には level 10 もありますが、Laravel パッケージではまず 5〜7 を安定させてから 8〜9 に進めると現実的です。新規パッケージなら最初から高めのレベルで始める方が後戻りが少なくなります。 ### `paths` `paths` には解析したいディレクトリを明示します。パッケージでは `src` だけでなく、型の崩れやすい `tests` も含めると効果的です。 ### `excludePaths` 生成ファイル、キャッシュ、検証用の Workbench など、静的解析の価値が低い場所だけを除外します。広く除外しすぎると、本来検出したいエラーまで見えなくなります。 ### `ignoreErrors` `ignoreErrors` は最後の手段です。メッセージを正規表現で絞り、`path` も併用して影響範囲を限定してください。将来 Laravel や Larastan が改善されたときに戻しやすくなります。 ## よく使う型アノテーション PHPStan と Larastan の精度は、PHPDoc の書き方で大きく変わります。Laravel パッケージでは特に `@param`、`@return`、`@var`、Generics (`@template`) を揃える価値が高いです。 ```php theme={null} $modelClass */ public function __construct(private string $modelClass) { } /** * @return TModel|null */ public function find(int $id): ?Model { return $this->modelClass::query()->find($id); } /** * @return Collection */ public function all(): Collection { /** @var Collection $models */ $models = $this->modelClass::query()->get(); return $models; } } ``` この例では、次の情報を PHPStan に渡しています。 * `@param class-string` — 文字列が任意の文字列ではなく Model クラス名であることを示す * `@return TModel|null` — `find()` が具体的なモデル型を返すことを伝える * `@var Collection` — コレクションの key / value 型を明示する * `@template TModel of Model` — 再利用可能な generic repository を表現する ## Laravel に特化した注意点 Laravel の「便利な magic」は、そのままだと静的解析に伝わりません。パッケージ側で型情報を足して、解析器が理解できる形に寄せる必要があります。 ### Facade カスタム Facade は、利用者が呼ぶメソッドを PHPDoc で補足すると IDE と静的解析の両方に効きます。 ```php theme={null} 静的解析のためだけに `ignoreErrors` を増やし続けると、Facade や Macro の本当の壊れ方を隠してしまいます。まずは明示的なメソッド、型付き value object、PHPDoc 追加で解決できないかを優先してください。 ### Eloquent モデルの型指定 Eloquent リレーションや動的プロパティには、`@property` と relation の Generics を組み合わせると効果的です。 ```php theme={null} */ public function team(): BelongsTo { return $this->belongsTo(Team::class); } } ``` ## CI での自動実行 静的解析はローカルだけでなく CI でも必ず実行してください。特に複数 Laravel バージョンをサポートするパッケージでは、[パッケージのバージョン互換性管理](/jp/advanced/package-versioning) の test matrix と同じ考え方で回すと、互換性と型安全性を同時に監視できます。 以下は `.github/workflows/static-analysis.yml` の例です。 ```yaml theme={null} name: Static analysis on: push: pull_request: jobs: static-analysis: runs-on: ubuntu-latest strategy: fail-fast: false matrix: php: [8.2, 8.3, 8.4] laravel: ["^12.0", "^13.0"] include: - laravel: "^13.0" testbench: "^10.0" - laravel: "^12.0" testbench: "^9.0" name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - PHPStan steps: - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} extensions: dom, curl, libxml, mbstring, zip coverage: none - name: Install dependencies run: | composer require "laravel/framework:${{ matrix.laravel }}" \ "orchestra/testbench:${{ matrix.testbench }}" \ --no-interaction --no-update composer update --prefer-dist --no-interaction - name: Run static analysis run: vendor/bin/phpstan analyse --error-format=github ``` この例ではテスト matrix と同じ Laravel / Testbench の対応表を使っています。テストと静的解析の両方で同じ組み合わせを監視すると、「実行は通るが型が壊れている」状態を見逃しにくくなります。 ## よくある誤検知と対処法 静的解析の警告は、まず「型情報が不足していないか」を疑ってください。誤検知に見えても、実際には PHPDoc が足りないだけのことがよくあります。 ### `@phpstan-ignore-next-line` 一時的な回避としては使えますが、理由を自分で説明できる行だけに限定してください。 ```php theme={null} // @phpstan-ignore-next-line Laravel の macro が実行時に登録されるため $builder->whereLike('name', $keyword); ``` ### `ignoreErrors` 同じエラーが複数箇所に出るときだけ検討します。必ず path を絞り、雑な正規表現で全体を握りつぶさないでください。 ```neon theme={null} parameters: ignoreErrors: - message: '#Call to an undefined method Illuminate\\Database\\Eloquent\\Builder::whereLike\(\)#' path: tests/Fixtures/* ``` ### まず優先するべき対処 1. PHPDoc を足す 2. Facade や relation の戻り値型を明示する 3. `mixed` を具体的な型に置き換える 4. それでも説明できる誤検知だけ ignore する ## 関連ページ サービスプロバイダーや公開リソースを含むパッケージ実装の基礎を確認します。 静的解析と合わせて運用したいパッケージ向けテスト基盤を解説します。 Laravel / PHP の対応表と GitHub Actions の matrix 戦略を確認します。 # Orchestra TestbenchでLaravelパッケージをテストする Source: https://kawax.biz/jp/advanced/package-testing Orchestra Testbenchを使ってLaravelパッケージのテスト環境を構築し、サービスプロバイダー・ファサード・設定・DBを検証する手順を解説します。 ## Orchestra Testbenchとは [Orchestra Testbench](https://github.com/orchestral/testbench) は、パッケージ開発向けのLaravelテストヘルパーです。`Orchestra\Testbench\TestCase` を継承すると、パッケージ単体でもLaravelアプリケーション内と同じ感覚でテストできます。 Laravel公式ドキュメントの[パッケージ開発](/jp/advanced/package-development)でも、パッケージテストにはTestbenchを使うことが案内されています。 ```mermaid theme={null} flowchart TD A["PHPUnit / Pest"] --> B["Package TestCase
    extends Orchestra\\Testbench\\TestCase"] B --> C["getPackageProviders()で
    サービスプロバイダー登録"] C --> D["インメモリLaravelアプリを起動"] D --> E["パッケージの機能を検証"] ``` ## セットアップ ```bash theme={null} composer require --dev orchestra/testbench ``` ```php theme={null} `getPackageAliases()` では、`config/app.php` の `aliases` と同様に、テスト中に使うファサードエイリアスを登録できます。 ```php theme={null} protected function getPackageAliases($app): array { return [ 'Package' => \Vendor\Package\Facades\Package::class, ]; } protected function defineEnvironment($app): void { // テスト用設定をここで上書きする $app['config']->set('package.enabled', true); } ``` ## 基本的なテストの書き方 サービスプロバイダーが読み込まれているか、ファサードが期待どおり動くか、設定が反映されているかを最初に確認します。 ```php theme={null} assertTrue($this->app->providerIsLoaded(PackageServiceProvider::class)); } public function test_facade_returns_expected_value(): void { // ファサード経由で機能を検証する $this->assertSame('ok', Package::status()); } public function test_package_config_is_available(): void { // defineEnvironment()で設定した値を確認する $this->assertTrue(config('package.enabled')); } } ``` ## ファイルシステム・データベースのテスト DBを使うテストでは、`defineEnvironment()` でSQLiteインメモリを設定し、テスト対象のマイグレーションを読み込みます。 ```php theme={null} set('database.default', 'testing'); $app['config']->set('database.connections.testing', [ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', ]); } protected function setUp(): void { parent::setUp(); // パッケージのマイグレーションを読み込む $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); } } ``` ```php theme={null} public function test_it_persists_data(): void { \DB::table('widgets')->insert(['name' => 'test']); $this->assertDatabaseHas('widgets', ['name' => 'test']); } ``` ## 複数Laravelバージョンでのテスト TestbenchのメジャーバージョンはLaravelのメジャーバージョンに対応します。詳細は公式の[Version Compatibility](https://packages.tools/testbench)を確認してください。 | Laravel | Testbench | | - | - | | 12.x | 10.x | | 13.x | 11.x | 複数バージョンを継続検証する場合は、GitHub Actionsのマトリクスと組み合わせます。設計の考え方は[パッケージのバージョン互換性管理](/jp/advanced/package-versioning)を参照してください。 ```yaml theme={null} strategy: fail-fast: false matrix: php: [8.3, 8.4] laravel: ["^12.0", "^13.0"] include: - laravel: "^13.0" testbench: "^11.0" - laravel: "^12.0" testbench: "^10.0" ``` ## まとめ Testbenchを使うと、Laravelアプリを手動で用意しなくても、パッケージの振る舞いを実運用に近い形で検証できます。サービスプロバイダー登録、設定、ファサード、DBまでテスト対象に含めることで、リリース後の不具合を大きく減らせます。 ## 関連ページ サービスプロバイダーを中心にしたパッケージ実装の基本を確認します。 LaravelとTestbenchのバージョン戦略とCIマトリクス運用を確認します。 # パッケージ翻訳の上書きと更新 Source: https://kawax.biz/jp/advanced/package-translations Laravel 13の翻訳ローダーを読み解き、PHP翻訳の部分上書き、JSON翻訳の共有キー、公開済み翻訳を壊さない更新方法を解説します。 ## このページで達成すること パッケージのメッセージを多言語で配布し、利用アプリケーションが必要な文言だけを変更できるようにします。翻訳キー・プレースホルダーを公開APIとして扱い、パッケージ更新時にカスタマイズを維持する方法まで整理します。 [ローカライゼーション](/jp/localization)がアプリケーションでの基本操作を、[Laravelパッケージ開発](/jp/advanced/package-development)が登録と公開の基本を扱います。このページはLaravel 13の `ServiceProvider`、`FileLoader`、`Translator` の実装に踏み込みます。 `loadTranslationsFrom()` は読み込み先の登録、`publishes()` はファイルのコピー先の登録です。翻訳を使うために、利用者が必ず `vendor:publish` を実行する必要はありません。 ## PHP翻訳を名前空間付きで配布する パッケージ専用のキーを持ちたい場合は、PHP配列形式と名前空間を使います。以下は `Acme\Courier` というパッケージの例です。 ```text theme={null} courier/ ├── src/ │ └── CourierServiceProvider.php └── lang/ ├── en/ │ └── messages.php └── ja/ └── messages.php ``` `lang/ja/messages.php` に日本語のデフォルト値を用意します。 ```php theme={null} [ 'queued' => ':nameさんへの配送を受け付けました。', 'failed' => '配送できませんでした。', ], ]; ``` `lang/en/messages.php` にフォールバック用の英語も用意します。 ```php theme={null} [ 'queued' => 'Delivery to :name has been queued.', 'failed' => 'Delivery failed.', ], ]; ``` サービスプロバイダーの `boot()` で読み込みと任意の公開を登録します。 ```php theme={null} loadTranslationsFrom(__DIR__.'/../lang', 'courier'); $this->publishes([ __DIR__.'/../lang' => $this->app->langPath('vendor/courier'), ], 'courier-translations'); } } ``` 使用側は名前空間・ファイル名・配列キーを指定します。言語コードはLaravelの設定に合わせて `ja` を使います。このドキュメントサイトのURLに使う `jp` とは別です。 ```php theme={null} echo __('courier::messages.delivery.queued', ['name' => '山田'], 'ja'); ``` 名前空間の `courier` は `loadTranslationsFrom()` の第2引数です。Composerのパッケージ名から自動で決まるわけではありません。 ## PHP翻訳はファイルを丸ごと置き換えない 利用アプリケーションでは、標準の言語ディレクトリなら `lang/vendor/courier/ja/messages.php` に変更するキーだけを書けます。言語ディレクトリを変更している場合も、`$this->app->langPath('vendor/courier')` の配下を使います。 ```php theme={null} [ 'queued' => ':name様への配送予約が完了しました。', ], ]; ``` この例では `queued` だけが変わり、`failed` はパッケージの日本語翻訳を使います。 ### FileLoaderの読み込み順序 `ServiceProvider::loadTranslationsFrom()` はTranslatorの解決後に名前空間を登録します。実際のファイル取得は翻訳が要求されたときに行われます。 `FileLoader::loadNamespaced()` は、登録されたパッケージの言語ファイルを読み込み、その配列を `loadNamespaceOverrides()` に渡します。そこでローダーの各言語パスにある `vendor/{namespace}/{locale}/{group}.php` を読み、`array_replace_recursive()` で置換します。 ```mermaid theme={null} flowchart TD A["courier::messages.delivery.queued
    locale: ja"] --> B["パッケージの
    lang/ja/messages.php"] B --> C["アプリケーションの
    lang/vendor/courier/ja/messages.php"] C --> D["array_replace_recursiveで
    指定キーを置換"] D --> E["翻訳文字列の取得と
    プレースホルダー置換"] ``` 標準の `TranslationServiceProvider` はフレームワークの言語パスとアプリケーションの言語パスを、この順序でローダーに渡します。追加パスを登録する拡張がある場合も、後から読み込む上書き配列が同じキーを優先します。 | 状態 | 結果 | | - | - | | アプリケーション側にキーがある | その値でパッケージの値を上書きする | | 同じファイルはあるがキーがない | パッケージの同じ言語の値を維持する | | 要求した言語でキーが見つからない | 通常は `fallback_locale` のPHP翻訳を探す | | フォールバックにもキーがない | 標準動作では要求したキーを返す | 名前空間が登録されていない場合、`FileLoader::loadNamespaced()` は空の配列を返します。`lang/vendor/courier` にファイルを置くだけでは、サービスプロバイダーの登録漏れを補えません。また、同じ名前空間を別パッケージが登録すると登録先が置き換わるため、衝突しない名前を選びます。 ## JSON翻訳はパッケージ専用の名前空間を持たない 文章をキーにするJSON翻訳は、次のようにディレクトリを登録します。これは先ほどのPHP翻訳とは別の選択肢です。 ```php theme={null} public function boot(): void { $this->loadJsonTranslationsFrom(__DIR__.'/../lang'); } ``` パッケージの `lang/ja.json` の例です。 ```json theme={null} { "Courier delivery queued": "配送を受け付けました。" } ``` ```php theme={null} echo __('Courier delivery queued', [], 'ja'); ``` `loadJsonTranslationsFrom()` には名前空間の引数がありません。登録されたJSON翻訳は、他のパッケージやアプリケーションと同じキー空間を共有します。 ### JSONの上書き先はアプリケーションのja.json `FileLoader::loadJsonPaths()` は、登録されたJSONパスを先に、その後に通常の言語パスを読み、`array_merge()` します。標準構成ではアプリケーションの `lang/ja.json` の同じ文字列キーがパッケージの値を上書きします。 ```json theme={null} { "Courier delivery queued": "配送予約が完了しました。" } ``` * パッケージ間で同じ文字列キーを使うと、後から読み込むJSONの値が優先されます。プロバイダーの順序に頼る設計は避けます。 * `lang/vendor/courier/ja.json` は、標準のJSONローダーの上書き先ではありません。PHP用の公開設定をそのままJSONに流用しても、この場所は自動で読まれません。 * アプリケーションの `lang/ja.json` へパッケージのJSONを `publishes()` しても、ファイル内容はマージされません。既存翻訳を壊さないよう、必要なキーだけを利用者が追加する手順を案内します。 `Translator::get()` は、まず要求した言語のJSONを確認し、見つからなければPHP形式のキーとして探索します。PHP翻訳と同じようにフォールバック言語のJSONまで順番に探す処理ではありません。JSONを英語の文章キーにする場合は、翻訳がないと原文のキーが表示される標準動作と区別してください。 PHP翻訳の名前空間は他パッケージのPHPキーを分離します。ただし、`Translator::get()` はJSONの完全一致キーを先に調べるため、JSONに `courier::messages.delivery.queued` のようなキーを定義するとPHP側より優先されます。通常は文章キーとPHP形式のキーを混在させない方針にします。 ## 公開済み翻訳を壊さずに更新する PHP翻訳を一括公開したい利用者には、対象を絞ったコマンドを案内できます。 ```bash theme={null} php artisan vendor:publish --provider="Acme\Courier\CourierServiceProvider" --tag=courier-translations ``` ただし、すべてのデフォルト値をコピーすると、そのコピーも以後は上書き値です。パッケージ側で誤字を修正しても、同じキーが公開済みファイルに残っていれば新しい値は見えません。一方、コピーにない新しいキーはパッケージ側から補われます。 少数の文言だけを変更する場合は、全ファイルを公開せず、上書きファイルに必要なキーだけを置く方が更新を取り込みやすくなります。これはPHP翻訳の部分上書きを利用する運用です。 長期保守では、次の順序で更新を設計します。 1. **キーと名前空間を維持する** — キーの削除・移動は、利用者の `__()` 呼び出しと上書き先に影響します。新しいキーを追加し、旧キーを残す移行期間を検討します。 2. **プレースホルダーを維持する** — `:name` を `:recipient` に変えると、呼び出し側の置換配列も変更が必要です。翻訳ファイルだけの修正と考えないようにします。 3. **公開済みファイルを差分確認する** — 利用者の上書きと新しいデフォルトを比較します。不要になった上書きキーを削除すると、パッケージの値に戻せます。 4. **無条件の再公開を避ける** — `--force` による再公開は利用者のカスタマイズを上書きします。JSONをアプリケーションのファイルへコピーする設計では、他の翻訳まで失う可能性があります。 5. **常駐プロセスで確認する** — `Translator::load()` は名前空間・グループ・言語ごとの配列をインスタンス内に保持します。既に読み込み済みのTranslatorが残るプロセスでは、ファイル変更だけで再読込されるとは限りません。運用に応じてワーカー等を再起動します。 公開操作の対象選択や上書きオプションは[パッケージの公開アセットと更新](/jp/advanced/package-assets)で、バージョン更新時の互換性判断は[パッケージのバージョン互換性管理](/jp/advanced/package-versioning)で補足しています。 ## 利用アプリケーションで確認する項目 サービスプロバイダーを登録した検証用アプリケーションで、次の組み合わせを確認します。パッケージ内のテスト環境構築は[パッケージのテスト](/jp/advanced/package-testing)を参照してください。 | ケース | 確認内容 | | - | - | | PHP翻訳を公開していない | パッケージの日本語・英語が取得できる | | 日本語の `queued` だけ上書き | `queued` が変わり、`failed` はデフォルトのまま | | パッケージ更新でキーを追加 | 既存の上書きファイルにないキーも取得できる | | 要求言語にキーがない | PHP翻訳は設定したフォールバック言語から取得できる | | JSONに同じキーを定義 | 標準構成ではアプリケーションのJSONが優先される | | JSONを `lang/vendor/courier` にだけ配置 | 標準構成ではJSONの上書きにならない | | `:name` を含む翻訳 | 呼び出し側の置換配列と一致し、未置換の文字列が残らない | 読み込み後に上書きファイルを作るテストでは、Translatorの既存の読み込み結果が影響しないようにします。ファイルを先に用意してから取得するか、ケースごとに新しいアプリケーションインスタンスを使って確認します。 ## 参照した一次情報 公式ドキュメントは最新のデフォルトブランチ `13.x`、内部実装は参照時点の最新リリース `v13.35.0` を確認しています。 * [Laravel公式ドキュメント: パッケージの言語ファイル](https://github.com/laravel/docs/blob/13.x/packages.md#language-files) * [Laravel公式ドキュメント: パッケージ翻訳の上書き](https://github.com/laravel/docs/blob/13.x/localization.md#overriding-package-language-files) * [ServiceProvider: 翻訳の登録](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Support/ServiceProvider.php) * [TranslationServiceProvider: 標準の言語パス](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Translation/TranslationServiceProvider.php) * [FileLoader: PHPの再帰的置換とJSONの読み込み順序](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Translation/FileLoader.php) * [Translator: JSON優先の取得と読み込み済み配列](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Translation/Translator.php) * [公式テスト: 翻訳ローダー](https://github.com/laravel/framework/blob/v13.35.0/tests/Translation/TranslationFileLoaderTest.php) # パッケージのバージョン互換性管理 Source: https://kawax.biz/jp/advanced/package-versioning LaravelとPHPのメジャーバージョンアップに伴うパッケージメンテナンス戦略を、composer.jsonの更新からGitHub Actionsのテストマトリクス設定まで実践的に解説します。 Laravelは毎年2〜3月頃にメジャーバージョンアップします。パッケージ開発者にとって、新バージョンへの対応を素早く完了させることはエコシステム全体への貢献であり、自パッケージの信頼性を高める重要な活動です。 このページは[パッケージ開発の基礎](/jp/advanced/package-development)の姉妹ページです。パッケージ開発の基本的な知識(サービスプロバイダー、`composer.json` の構成など)を前提としています。 ## Laravelのメジャーバージョンアップへの対応 ### 対応の流れ 新バージョンを依存範囲に追加します。古いバージョンのサポートを続ける場合は `||` で並記します。 ```json theme={null} "require": { "php": "^8.2", "illuminate/support": "^12.0||^13.0" } ``` 新バージョンへの対応が完了し、古いバージョンのサポートを終了する場合は古い制約を削除します。 ```json theme={null} "require": { "php": "^8.3", "illuminate/support": "^13.0||^14.0" } ``` `illuminate/framework` 全体ではなく `illuminate/support` など必要なコンポーネントだけに依存することで、依存ツリーを小さく保てます。 ローカルまたはCI環境で新しいLaravelバージョンを使ってテストを実行します。 ```shell theme={null} composer update vendor/bin/pest ``` テストが通れば基本的な互換性は確認できています。通らない場合は、変更されたAPIや削除されたメソッドを修正します。 CIのテストマトリクスにLaravelの新バージョンを追加して、継続的に互換性を確認します。詳細は[テストマトリクスの設定例](#テストマトリクスの設定例)を参照してください。 `composer.json` の変更を含む新しいバージョンをタグ付けしてリリースします。ユーザーは `composer update` だけで新しいLaravelバージョンにインストールできるようになります。 ### 公式パッケージを参考にする 対応方法に迷ったときは、Laravelの公式パッケージの `composer.json` を参照するのが最も確実です。 * [laravel/socialite](https://github.com/laravel/socialite) * [laravel/horizon](https://github.com/laravel/horizon) * [laravel/telescope](https://github.com/laravel/telescope) これらのパッケージはLaravelチームが管理しており、新バージョンへの対応が最も早く、`illuminate/*` の依存バージョンの書き方も参考になります。 ## PHPバージョン要件の更新 LaravelのバージョンアップはPHPの最低要件も引き上げることがあります。パッケージの `composer.json` でPHP要件もあわせて更新します。 | Laravel | 最低PHP要件 | | - | - | | Laravel 11 | PHP 8.2 | | Laravel 12 | PHP 8.2 | | Laravel 13 | PHP 8.3 | | Laravel 14 | PHP 8.4 | ```json theme={null} "require": { "php": "^8.2", "illuminate/support": "^11.0||^12.0||^13.0" } ``` ### 新しいPHP機能を積極的に使う場合 PHP 8.3以降の機能(型付き定数、新しいランダム関数など)を使いたい場合は、最低要件を引き上げる判断が必要です。最低要件の引き上げはセマンティックバージョニング上は破壊的変更なので、メジャーバージョンアップ時に行うのが適切です。 ```json theme={null} "require": { "php": "^8.3", "illuminate/support": "^12.0||^13.0" } ``` PHPの最低要件を引き上げると、古いPHPを使っているユーザーがパッケージを更新できなくなります。READMEやCHANGELOGに明記して事前に周知することが重要です。 ## 旧バージョンサポートの終了戦略 旧バージョンをいつまでも維持することは、長期的にはメンテナンスコストを増大させます。明確なサポートポリシーを定めて、定期的に古いバージョンのサポートを終了することが健全なパッケージ運営につながります。 ### サポート終了のタイミング 一般的なアプローチは、**Laravelのサポート期間に合わせる**ことです。Laravelは各バージョンに対してバグ修正18ヶ月、セキュリティ修正2年のサポートを提供しています。パッケージも同様のポリシーを採用するとユーザーに分かりやすくなります。 | 判断基準 | 説明 | | - | - | | Laravelの公式サポート終了 | そのLaravelバージョンのサポートが終了したタイミングで対応をやめる | | 利用状況 | Packagistのダウンロード統計で旧バージョン利用者が少なくなったタイミング | | 機能追加の障壁 | 旧バージョン対応のために新機能の実装が複雑になってきたタイミング | ### サポートポリシーをREADMEに明記する ユーザーが期待値を正しく持てるよう、サポートポリシーを `README.md` に記載します。 ```markdown theme={null} ## Version Compatibility | Package | Laravel | PHP | |---------|---------|------| | 3.x | 13.x | ^8.2 | | 2.x | 11.x, 12.x | ^8.2 | | 1.x | 10.x | ^8.1 | Only the latest major version receives new features. Security fixes are backported to the previous major version for 12 months. ``` ### 旧バージョン維持のコスト 旧バージョンサポートを続けると次のコストが発生します。 * **バグ修正の二重対応** — 同じバグを複数ブランチで修正する必要がある * **分岐コードの増加** — バージョンごとに異なる実装を条件分岐で吸収する複雑さ * **テストマトリクスの肥大化** — CI実行時間が長くなる * **セキュリティパッチの複雑化** — 古いバージョンにも安全にバックポートする必要がある エコシステム全体がバージョンアップしていく中で、旧バージョンのサポートを適切に終了することは、ユーザーに最新環境への移行を促す効果もあります。 ## 早期対応のメリット Laravelのリリースと同時に対応が完了していると、ユーザーはすぐに新バージョンへ移行できます。これはパッケージの信頼性を示す重要なシグナルです。 ### 開発版で事前検証する Laravelはbeta/RCを公開しない代わりに、`dev-master` または `@dev` を指定することで開発中の次期バージョンをインストールできます。メジャーリリース前に動作確認することで、ゼロデイ対応が可能になります。 ```shell theme={null} # dev-master で次期バージョンをインストール composer require laravel/framework:dev-master --no-update composer update vendor/bin/pest ``` または `@dev` 制約でインストールすることもできます。 ```shell theme={null} composer require laravel/framework:^14.0@dev --no-update composer update vendor/bin/pest ``` `minimum-stability` を `dev` に設定することで、開発版との依存関係を解決できます。 ```json theme={null} { "minimum-stability": "dev", "prefer-stable": true } ``` `prefer-stable: true` を指定すると、安定版が存在する場合は安定版が優先されます。開発版でテストしながらも、安定版への自動切り替えが保証されます。 ### 最初のステップは composer.json の変更だけでよい 完全な互換性確認の前でも、`composer.json` の依存範囲を更新してリリースするだけで、ユーザーはパッケージをインストールできるようになります。 ```json theme={null} "illuminate/support": "^12.0||^13.0" ``` 小さな不具合はリリース後でもパッチバージョンで修正できます。完璧な対応を待つより、インストール可能な状態にすることを優先しましょう。 ### リリース情報の追いかけ方 新バージョンのリリース情報をタイムリーに把握するための方法です。 | 情報源 | 内容 | | - | - | | GitHub Watch機能 | リポジトリの「Watch → Custom → Releases」でメール通知を受け取る。framework以外の主要パッケージも登録しておくと良い | | [Laravel公式ブログ](https://laravel.com/blog) | メジャーリリースの発表と新機能解説 | | [Laravel公式ドキュメント アップグレードガイド](https://laravel.com/docs/upgrade) | 破壊的変更の一覧 | | [laravel/framework コミットログ](https://github.com/laravel/framework/commits/master) | 次期バージョンの変更を先取りする場合はコミットログを直接追う | ## テストマトリクスの設定例 GitHub Actionsで複数のLaravelバージョンとPHPバージョンの組み合わせを自動テストするワークフロー例です。 ```yaml theme={null} name: Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: php: [8.2, 8.3, 8.4] laravel: ["^12.0", "^13.0"] include: - laravel: "^13.0" testbench: "^10.0" - laravel: "^12.0" testbench: "^9.0" name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} steps: - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} extensions: dom, curl, libxml, mbstring, zip coverage: none - name: Install dependencies run: | composer require "laravel/framework:${{ matrix.laravel }}" \ "orchestra/testbench:${{ matrix.testbench }}" \ --no-interaction --no-update composer update --prefer-dist --no-interaction - name: Run tests run: vendor/bin/pest ``` ### fail-fast: false の重要性 `fail-fast: false` を設定することで、一つの組み合わせが失敗しても他の組み合わせのテストを継続します。どのバージョンの組み合わせで問題が発生しているかを一度のCI実行で把握できます。 ### テストマトリクスの段階的な更新 新しいLaravelバージョンが出たら、マトリクスに追加します。古いバージョンのサポートを終了したら、マトリクスから削除します。 ```yaml theme={null} # Laravel 14がリリースされたら追加 matrix: laravel: ["^13.0", "^14.0"] include: - laravel: "^14.0" testbench: "^11.0" - laravel: "^13.0" testbench: "^10.0" ``` ## composer.json のサンプル 複数バージョン対応の `composer.json` の完全な例です。 ```json theme={null} { "name": "acme/courier", "description": "A Laravel courier package", "type": "library", "license": "MIT", "require": { "php": "^8.2", "illuminate/support": "^12.0||^13.0" }, "require-dev": { "orchestra/testbench": "^9.0||^10.0", "pestphp/pest": "^3.0", "pestphp/pest-plugin-laravel": "^3.0" }, "autoload": { "psr-4": { "Acme\\Courier\\": "src/" } }, "autoload-dev": { "psr-4": { "Acme\\Courier\\Tests\\": "tests/" } }, "extra": { "laravel": { "providers": [ "Acme\\Courier\\CourierServiceProvider" ] } }, "minimum-stability": "stable", "prefer-stable": true } ``` ## バージョンアップ対応チェックリスト * [ ] 開発版(`dev-master` / `@dev`)で動作確認を実施する * [ ] 公式アップグレードガイドで破壊的変更を確認する * [ ] テスト環境で新バージョンへの対応を実装する * [ ] テストマトリクスに新バージョンを追加してCIで確認する * [ ] `composer.json` の `illuminate/*` 要件に新バージョンを追加する * [ ] 必要に応じてPHP要件を更新する * [ ] `require-dev` の `orchestra/testbench` も対応バージョンに更新する * [ ] 新バージョンをタグ付けしてPackagistに反映する * [ ] READMEのバージョン互換性表を更新する * [ ] CHANGELOG/READMEにサポート終了を明記する * [ ] `composer.json` から旧バージョンの制約を削除する * [ ] テストマトリクスから旧バージョンを削除する * [ ] 旧バージョン用の条件分岐コードを整理する ## 関連ページ サービスプロバイダーを核としたLaravelパッケージの開発方法を解説します。 PestのExpectation APIやデータセットを使ったパッケージテストの書き方を解説します。 # パッケージビューの上書きと更新 Source: https://kawax.biz/jp/advanced/package-views Laravel 13の実装から、名前空間付きビューの検索順序、公開済みBladeテンプレートの保守、ビューキャッシュと検索結果のキャッシュの違いを解説します。 利用者が画面やメールのテンプレートをカスタマイズできるパッケージでは、ビューを公開するだけでなく、公開済みファイルを残したまま更新できる契約が必要です。パッケージ側のBladeファイルを修正しても、利用アプリケーションがそのファイルを描画しているとは限りません。 このページでは[パッケージ開発の基礎](/jp/advanced/package-development)を前提に、ビューの選択とファイルの公開、キャッシュを分けて考えます。公式ドキュメントはLaravel 13、フレームワークの実装は最新リリースの `v13.34.0` を参照しています。 ## 登録と公開は別の処理 `loadViewsFrom()` は名前空間に検索パスを登録します。`publishes()` はコピー元とコピー先を登録し、実際のコピーは `vendor:publish` が行います。次の例では、公開しなくても `courier::deliveries.show` を利用できます。 ```php src/CourierServiceProvider.php theme={null} loadViewsFrom(__DIR__.'/../resources/views', 'courier'); $this->publishes([ __DIR__.'/../resources/views' => resource_path('views/vendor/courier'), ], 'courier-views'); } } ``` パッケージ側のファイルは `resources/views/deliveries/show.blade.php` に置きます。ビュー名のドットは、検索時にディレクトリ区切りへ変換されます。 ```php theme={null} return view('courier::deliveries.show', [ 'trackingCode' => 'TRACK-001', ]); ``` ```blade resources/views/deliveries/show.blade.php theme={null}

    追跡番号: {{ $trackingCode }}

    ``` ビューの名前空間はComposerのパッケージ名やPHPの名前空間とは別です。ここでは `loadViewsFrom()` の第2引数に指定した `courier` が、ビュー参照と上書き先ディレクトリの契約になります。 ## ファイル単位で上書き先を探す `ServiceProvider::loadViewsFrom()` は `view` が解決されたときに、設定の `view.paths` を順番に確認します。各パスに `vendor/courier` ディレクトリが存在する場合は、そのディレクトリを名前空間へ追加し、最後にパッケージ側のパスを追加します。 `FileViewFinder` は、その名前空間のパスを順番に検索し、最初に見つかったファイルを返します。標準の `resources/views` を使う構成では、次の順序です。 ```mermaid theme={null} flowchart TD A["courier::deliveries.show"] --> B["resources/views/vendor/courier/
    deliveries/show.blade.phpを検索"] B --> C{"ファイルがある?"} C -->|はい| D["利用アプリケーションのビューを使用"] C -->|いいえ| E["パッケージのresources/views/
    deliveries/show.blade.phpを検索"] E --> F{"ファイルがある?"} F -->|はい| G["パッケージのビューを使用"] F -->|いいえ| H["ビューが見つからない例外"] ``` これはディレクトリ全体の切り替えではありません。利用者が `deliveries/show.blade.php` だけを上書きしても、上書きしていない他のビューはパッケージ側から読み込まれます。 | 利用アプリケーションの状態 | 選ばれるビュー | | - | - | | 上書きファイルがない | パッケージのファイル | | 同じ相対パスの上書きファイルがある | 利用アプリケーションのファイル | | 上書きファイルだけを除去した | 新しい起動ではパッケージのファイルへ戻る | | 両方に対象ファイルがない | `View [...] not found.` の例外 | `view.paths` が複数ある構成では、上書き先も複数になり得ます。`resource_path('views/vendor/courier')` はこの例の公開先であり、検索対象をそこだけに固定する処理ではありません。名前空間はパッケージ固有のものにし、複数のプロバイダーで同じ名前へパスを追加する設計を避けてください。 ## 必要なビューだけをカスタマイズする 利用者は次のコマンドでテンプレートをコピーできます。プロバイダーとタグを指定し、他のリソースを巻き込まないようにします。 ```bash theme={null} php artisan vendor:publish --provider="Acme\Courier\CourierServiceProvider" --tag=courier-views ``` この登録ではビューディレクトリ全体が公開されます。すべてを上書きする必要がなければ、内容を確認してカスタマイズするファイルだけを残す、または必要なファイルだけを同じ相対パスへ手動でコピーする方法もあります。編集していないコピーも存在する限り上書きとして扱われるためです。 公開済みテンプレートはパッケージの更新と自動同期されません。古いコピーが優先される状態でパッケージ側だけを修正しても、そのビューの変更は反映されません。表示の不具合修正やフォーム変更なども、上書きファイルとの比較が必要です。 ### 再公開では差分のマージをしない `VendorPublishCommand` は通常、同名の公開先ファイルが存在する場合にコピーをスキップします。`--force` は既存ファイルを上書きします。また、`--existing` も「公開済みのファイルを上書きする」オプションであり、利用者の編集を保持するモードではありません。 | 操作 | ビューファイルへの影響 | | - | - | | 通常の再公開 | 既存ファイルは保持し、存在しない対象ファイルはコピーする | | `--force` を付けて公開 | 既存のカスタマイズも上書きする | | `--existing` を付けて公開 | 公開先に存在する対象ファイルだけを上書きする | どの方法も、旧版・新版・利用者の編集を比較したマージではありません。パッケージから削除されたビューを公開先から自動で取り除く処理でもありません。更新手順を「同じタグを再公開するだけ」にしないでください。 ## ビューも公開APIとして保守する ビュー名だけでなく、受け取るデータや参照する部品も利用者のカスタマイズに影響します。たとえば、新版で `trackingCode` を別の変数名へ変更すると、旧テンプレートを残している利用者は新しいコードから必要な値を受け取れなくなります。 リリース前に、次の契約を確認します。 * 名前空間と `deliveries.show` などのビュー名を不用意に変更しない。 * 渡す変数、型、必須・任意の区別を記録する。 * `@include` や `@extends` の参照先、Bladeコンポーネントのpropsも変更点に含める。 * 修正したビューと、公開済みの旧版へ適用すべき変更をリリースノートに記載する。 利用者向けには、旧版のパッケージビューと新版を比較し、カスタマイズ済みファイルへ必要な変更を手動で取り込む手順を用意します。上書きが不要になったファイルは、バックアップやバージョン管理で変更を保全してから取り除くと、パッケージ側のビューへ戻せます。 ## Bladeキャッシュは上書きファイルを更新しない `view:cache` はBladeテンプレートをPHPへ事前コンパイルします。`ViewCacheCommand` は最初に `view:clear` を実行し、通常のビューパスと名前空間に登録されたパスを集めてコンパイル対象を探します。 ```bash theme={null} php artisan view:cache ``` この処理は公開済みのBladeファイルを書き換えず、ビューの検索優先順位も変えません。古い上書きファイルが存在すれば、キャッシュを再構築してもそのファイルが引き続き選ばれます。デプロイでは、コードと上書きファイルの更新を済ませてからコンパイルします。 開発中にコンパイル済みファイルを消して再描画したい場合は、次のコマンドを使います。 ```bash theme={null} php artisan view:clear ``` 通常のタイムスタンプ確認が有効なら、Bladeコンパイラーは元ファイルとコンパイル済みファイルの更新時刻を比較します。ただし、タイムスタンプ確認を無効にする構成もあるため、デプロイ時の再構築を自動判定だけに任せないでください。 ### 検索結果のキャッシュとは区別する `FileViewFinder::find()` は、見つけたパスをそのFinderインスタンスの `$views` 配列へ保存します。また、上書きディレクトリの存在確認は `loadViewsFrom()` のコールバックで行われます。起動後に新しいディレクトリを追加しても、すでに登録済みの検索パスへ自動追加されるわけではありません。 | 管理対象 | 役割 | 更新時の考え方 | | - | - | - | | 公開済みBladeファイル | 利用者のカスタマイズ | 差分を取り込むか、上書きをやめる | | コンパイル済みPHP | Bladeのコンパイル結果 | `view:cache` / `view:clear` で管理する | | Finderの登録パス・検索結果 | 実行中インスタンスのビュー選択 | 長時間動くプロセスを新しいコード・構成で再起動する | `view:clear` は、別の実行中プロセスが保持するFinderの状態を一括で消すコマンドではありません。Octaneなどの長時間動くプロセスでは、通常のデプロイ手順に従い再読み込みしてください。Finderの `flush()` は検索結果を消しますが、新しい上書きディレクトリの登録まで行う処理ではありません。 ## リリース前に確認すること パッケージのテストに加え、利用アプリケーションで次の組み合わせを確認します。最新テンプレートだけを描画するテストでは、旧版を公開済みの利用者への互換性は検証できません。 * 未公開の状態で、パッケージのビューが描画される。 * 1つだけ上書きすると、そのファイルだけが優先され、他はパッケージへフォールバックする。 * 旧版の公開済みテンプレートを残した状態でも、新版が渡すデータで描画できる。 * 通常の再公開でカスタマイズが保持され、未公開ファイルの追加も意図どおりである。 * 上書きファイルの変更後に `view:cache` が成功し、新しい起動で変更後の表示になる。 ## 関連ページ ビューの作成、データの受け渡し、事前コンパイルの基本を確認します。 レイアウト、include、コンポーネントの使い方を確認します。 テンプレートの契約変更をリリース方針に結び付けます。 長時間動くアプリケーションのライフサイクルと再読み込みを確認します。 ## 参照した一次情報 * [Laravel公式ドキュメント: パッケージのビュー](https://github.com/laravel/docs/blob/156fc7fde114548640e13c39aa79b991291c3f91/packages.md#views) * [ServiceProvider: ビューパスの登録](https://github.com/laravel/framework/blob/v13.34.0/src/Illuminate/Support/ServiceProvider.php) * [FileViewFinder: 検索順序と検索結果の保持](https://github.com/laravel/framework/blob/v13.34.0/src/Illuminate/View/FileViewFinder.php) * [VendorPublishCommand: ファイルの公開条件](https://github.com/laravel/framework/blob/v13.34.0/src/Illuminate/Foundation/Console/VendorPublishCommand.php) * [ViewCacheCommand: ビューパスの収集とコンパイル](https://github.com/laravel/framework/blob/v13.34.0/src/Illuminate/Foundation/Console/ViewCacheCommand.php) * [ViewClearCommand: コンパイル済みファイルの削除](https://github.com/laravel/framework/blob/v13.34.0/src/Illuminate/Foundation/Console/ViewClearCommand.php) * [Compiler: 更新時刻の確認](https://github.com/laravel/framework/blob/v13.34.0/src/Illuminate/View/Compilers/Compiler.php) # Testbench Workbenchでパッケージ開発を進める Source: https://kawax.biz/jp/advanced/package-workbench Orchestra Testbench Workbenchを使って、ルート・マイグレーション・サービス起動を備えた実アプリに近い検証環境を作る方法を解説します。 ## Testbench Workbenchとは [Orchestra Testbench](https://github.com/orchestral/testbench) はテスト向けですが、Workbenchを組み合わせるとパッケージリポジトリ内に小さなLaravelアプリを作って手元で動かせます。 `package-testing` で作ったテストを補完しつつ、UI確認、ルート疎通、シーダー付き検証を進めたいときに使います。 ```mermaid theme={null} flowchart TD A["パッケージ本体
    `src/`"] --> B["Workbench
    `workbench/`"] B --> C["WorkbenchServiceProvider
    デモ用サービス登録"] B --> D["ルート・コントローラ
    画面確認"] B --> E["マイグレーション・Seeder
    データ検証"] B --> F["`composer serve`
    ローカル起動"] C --> G["testbench.yaml
    プロバイダー/ビルド設定"] ``` ## セットアップ ```bash theme={null} composer require --dev orchestra/testbench ``` ```bash theme={null} vendor/bin/testbench workbench:install ``` このコマンドは以下をまとめて行います。 * `workbench/` ディレクトリ構造を作成する * `composer.json` の `autoload-dev` にWorkbench名前空間を追加する * `composer.json` の `scripts` にビルド用コマンドを追加する 追加オプション: * `--force` : 既存ファイルを上書きする * `--basic` : ルート・package discovery設定を省略したシンプルな構成 * `--devtool` : DevToolサポートを有効化する ```bash theme={null} composer clear composer prepare composer build vendor/bin/testbench serve ``` `workbench:install` は `workbench/` ディレクトリや `autoload-dev`、関連スクリプトをまとめて整備します。 インストール後、`composer.json` には以下のスクリプトが追加されます。 ```json theme={null} { "autoload-dev": { "psr-4": { "Tests\\": "tests/", "Workbench\\App\\": "workbench/app/", "Workbench\\Database\\Factories\\": "workbench/database/factories/", "Workbench\\Database\\Seeders\\": "workbench/database/seeders/" } }, "scripts": { "post-autoload-dump": [ "@clear", "@prepare" ], "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", "prepare": "@php vendor/bin/testbench package:discover --ansi", "build": "@php vendor/bin/testbench workbench:build --ansi", "serve": [ "Composer\\Config::disableProcessTimeout", "@build", "@php vendor/bin/testbench serve --ansi" ] } } ``` ## testbench.yamlで開発環境を定義する Workbenchの動作はルートに置く `testbench.yaml` で管理します。 `testbench.yaml` は環境ごとに異なる設定を含むことがあるため、`.gitignore` に追加してコミット対象から外すことが推奨されます。代わりに `testbench.yaml.example` をテンプレートとしてリポジトリに含めてください。 ```yaml theme={null} providers: - Vendor\Package\PackageServiceProvider - Workbench\App\Providers\WorkbenchServiceProvider migrations: - workbench/database/migrations workbench: start: '/' install: true health: false discovers: web: true api: false commands: false components: false views: false config: true build: - asset-publish - create-sqlite-db - db-wipe - migrate-fresh: --seed: true --seeder: Workbench\Database\Seeders\DatabaseSeeder assets: - laravel-assets ``` 主な設定オプション: | キー | 説明 | | - | - | | `providers` | Workbench環境で読み込むサービスプロバイダー一覧 | | `migrations` | 実行するマイグレーションディレクトリ | | `workbench.start` | `composer serve` 起動時のデフォルトURL | | `workbench.discovers` | Laravelのパッケージ自動発見の対象を制御する | | `workbench.build` | ビルド時に実行するコマンド一覧 | 環境変数も `testbench.yaml` で管理できます。 ```yaml theme={null} env: - APP_ENV=testing - APP_KEY=base64:your-app-key - DB_CONNECTION=sqlite - DB_DATABASE=:memory: ``` ## workbenchディレクトリ構造 ## Workbenchが提供する主要機能 ### WorkbenchServiceProvider Workbench専用のサービスプロバイダーを作成して、デモ用の登録処理を行います。 ```php theme={null} app->singleton(DemoService::class); } public function boot(): void { SomeClass::register('demo', DemoService::class); $this->loadRoutesFrom(__DIR__.'/../../routes/web.php'); $this->loadViewsFrom(__DIR__.'/../../resources/views', 'workbench'); } } ``` ### ルートとコントローラ `workbench/routes/web.php` や `workbench/routes/api.php` に検証ルートを置けます。 ```php theme={null} use Illuminate\Support\Facades\Route; use Vendor\Package\Facades\Package; Route::get('/', function () { return Package::status(); }); ``` ### マイグレーションとSeeder `workbench/database/migrations` と `workbench/database/seeders` を使うと、実運用に近いデータ構造で検証できます。 ```php theme={null} use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; Schema::create('widgets', function (Blueprint $table) { $table->id(); $table->string('name'); $table->timestamps(); }); ``` Seederでテストデータを生成します。 ```php theme={null} count(10)->create(); } } ``` ### サービス起動とCLI確認 `vendor/bin/testbench` 経由でArtisanコマンドを実行できます。 ```bash theme={null} vendor/bin/testbench list vendor/bin/testbench route:list vendor/bin/testbench migrate:fresh --seed ``` ## WithWorkbenchトレイトを使ったテスト `WithWorkbench` トレイトを使うと、`testbench.yaml` の設定が自動テストにも適用されます。 ```php theme={null} set('database.default', 'testing'); $app['config']->set('your-package.key', 'test-value'); } } ``` ## テストとの連携 Workbenchは「手動確認・デモ用アプリ」、Testbenchのテストコードは「自動検証」として役割分担すると運用しやすくなります。 * 自動化: `tests/` で回帰を防ぐ * 手動確認: `workbench/` で画面・導線・統合挙動を確認する ## トラブルシューティング ```bash theme={null} # クリアしてフルリビルド composer clear && composer prepare && composer build # パッケージ自動発見を確認する vendor/bin/testbench package:discover --ansi # 設定を確認する vendor/bin/testbench about # ルート一覧を確認する vendor/bin/testbench route:list ``` `testbench.yaml` の構文とプロバイダー設定を確認してください。YAMLのインデントエラーが原因になることがあります。 ルートファイルのパスとWorkbenchServiceProviderの登録を確認してください。`testbench.yaml` の `discovers.web` が `true` になっているか確認します。 マイグレーションのパスが正しいか確認してください。SQLiteを使う場合は `create-sqlite-db` ビルドステップが含まれているか確認します。 ## 関連ページ パッケージテスト基盤の作り方を先に確認します。 LaravelとTestbenchの対応表とCIマトリクス運用を確認します。 Source: [invokable/laravel-bluesky docs/workbench.md](https://github.com/invokable/laravel-bluesky/blob/main/docs/workbench.md), [Orchestra Testbench](https://github.com/orchestral/testbench) # PHP AST Source: https://kawax.biz/jp/advanced/php-ast PHP AST(抽象構文木)の基本、PHP内部ASTの位置づけ、nikic/PHP-Parserによる実践的なコード解析・変換パターンを解説します。 ## PHP AST とは AST(Abstract Syntax Tree / 抽象構文木)は、ソースコードを「意味のある構造」に分解した木構造です。 あなたがコード生成ツール、静的解析ツール、自動リファクタリングツールを作るとき、ASTはほぼ必須の基盤になります。文字列置換では壊れやすい変換でも、ASTを使えば「function call」「class declaration」「`use` statement」のような構文単位で安全に扱えます。 ```mermaid theme={null} graph TD A["echo add(1, 2);"] --> B["Stmt\\Expression"] B --> C["Expr\\FuncCall: add"] C --> D["Arg: 1"] C --> E["Arg: 2"] ``` ## PHP の内部 AST PHP 7以降のZend Engineは、PHPコードをいきなりopcodeへ変換するのではなく、まず内部ASTへ変換してからコンパイルします。 * `php -r` で実行する1行コードでも、内部的には同じコンパイルパイプラインを通ります * OPcacheは最終的に生成されたopcodeをキャッシュして再利用します * 通常のアプリ開発では、この内部ASTを直接操作する機会はほとんどありません つまり「ASTを使ってコードを編集したい」と考えたときは、PHPランタイム内部ASTではなく、ユーザーランドで扱えるASTライブラリを使うのが実用的です。 ## nikic/PHP-Parser パッケージ [nikic/PHP-Parser](https://github.com/nikic/PHP-Parser) は、PHPコードをASTとして解析・走査・再生成するための標準的なライブラリです。READMEと公式ドキュメントでは、次の3ステップが基本として説明されています。 ### インストール ```bash theme={null} composer require nikic/php-parser ``` ### 基本的なパース例(コードをASTへ変換) ```php theme={null} createForNewestSupportedVersion(); try { $ast = $parser->parse($code); } catch (Error $error) { echo "Parse error: {$error->getMessage()}\n"; return; } ``` ### NodeVisitor パターンでASTを走査・変更 ```php theme={null} addVisitor(new class extends NodeVisitorAbstract { public function leaveNode(Node $node) { if ($node instanceof Node\Scalar\Int_) { return new Node\Scalar\String_((string) $node->value); } } }); $modifiedAst = $traverser->traverse($ast); ``` `NodeVisitorAbstract` を継承すると、必要なフック(`enterNode` / `leaveNode`)だけを実装できます。複雑な変換では、`enterNode` で情報を集めて `leaveNode` で置換する流れが有効です。 ### Pretty Printer でコードを再生成 ```php theme={null} prettyPrintFile($modifiedAst); echo $newCode; ``` この流れを使うと、「文字列としてのコード」ではなく「構文木としてのコード」を変換対象にできます。 ## Laravel/Chisel での使用例 [laravel/chisel](/jp/blog/chisel-introduction) は、スターターキットを後処理で削るためのライブラリです。`composer.json` では `nikic/php-parser` の5.x系を依存関係に含めています。 Chiselの `Laravel\Chisel\Ast\Source` では、次の手順でAST編集を実行しています。 1. `ParserFactory::createForNewestSupportedVersion()` でコードをパース 2. `NodeTraverser` に複数のVisitor(`RemoveImportVisitor` など)を追加して変換 3. `PhpParser\PrettyPrinter\Standard` の `printFormatPreserving()` で元のフォーマットを保ちながら書き戻し この設計により、`use` 文・trait・interface の削除をテキスト置換ではなく構文ベースで安全に実行できます。 ## ユースケース ASTを使うと次のような開発を進めやすくなります。 * コード生成CLI(雛形作成後に構文単位で編集) * 静的解析ツール(特定構文の検出、ルール違反の検出) * 自動リファクタリング(API移行や命名変更の半自動化) * プロジェクトテンプレートの後処理(Chiselのような機能削除・置換) ## まとめ PHP ASTは、一般的なLaravelアプリ開発では毎日使う機能ではありません。 ただし、あなたがツール開発やパッケージ開発を進めるなら、ASTは「壊れにくく再現性のあるコード変更」を実現する強力な基盤になります。まずは `nikic/php-parser` のParser・Visitor・Pretty Printerの3点を小さなCLIで試してください。 # PHPアトリビュート Source: https://kawax.biz/jp/advanced/php-attributes Laravel 13で導入・強化されたPHPアトリビュートを使って、ジョブやモデルの設定をより宣言的に記述する方法を解説します。 ## PHPアトリビュートとは PHPアトリビュート(PHP Attributes)は、PHP 8.0で導入されたネイティブのメタデータ構文です。クラス、メソッド、プロパティ、関数などに対して、`#[AttributeName]` の形式でメタ情報を付与できます。 Laravelはフレームワーク本体でPHPアトリビュートを積極的に採用しており、ジョブやEloquentモデルの設定を宣言的に記述できます。Laravel 13(v13.2.0)ではキューアトリビュートがenumを受け入れるようになりました。従来のクラスプロパティやメソッドオーバーライドの代わりに、アトリビュートを使ってより読みやすく簡潔なコードを書けます。 ```php theme={null} // 従来の書き方 class ProcessOrder implements ShouldQueue { public string $queue = 'orders'; public string $connection = 'redis'; public int $tries = 3; public array $backoff = [30, 60, 120]; } // アトリビュートを使った書き方 #[Queue('orders')] #[Connection('redis')] #[Tries(3)] #[Backoff(30, 60, 120)] class ProcessOrder implements ShouldQueue { } ``` アトリビュートはPHP 8.0以降で利用できます。Laravel 13はPHP 8.3以上を必要とするため、すべての環境でアトリビュートを使用できます。 ## キュー関連アトリビュート キュージョブに関するアトリビュートはすべて `Illuminate\Queue\Attributes` 名前空間にあります。 ### `#[Queue]` — キュー名を指定する ジョブが送られるデフォルトのキュー名を指定します。 ```php theme={null} use Illuminate\Queue\Attributes\Queue; #[Queue('emails')] class SendWelcomeEmail implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function handle(): void { // ... } } ``` v13.2.0以降、文字列の代わりにenumを渡すこともできます。 ```php theme={null} enum QueueName: string { case Emails = 'emails'; case Orders = 'orders'; case Notifications = 'notifications'; } #[Queue(QueueName::Emails)] class SendWelcomeEmail implements ShouldQueue { // ... } ``` `#[Queue]` アトリビュートは `Attribute::TARGET_CLASS` をターゲットに設定されているため、クラスに対してのみ適用できます。 ### `#[Connection]` — コネクションを指定する ジョブが使用するデフォルトのキューコネクションを指定します。 ```php theme={null} use Illuminate\Queue\Attributes\Connection; #[Connection('sqs')] class ProcessPayment implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function handle(): void { // ... } } ``` こちらもenumを使えます。 ```php theme={null} enum QueueConnection: string { case Redis = 'redis'; case Sqs = 'sqs'; case Database = 'database'; } #[Connection(QueueConnection::Sqs)] class ProcessPayment implements ShouldQueue { // ... } ``` ### `#[Backoff]` — リトライのバックオフ時間を指定する ジョブが失敗したときのリトライまでの待機時間(秒)を指定します。複数の値を渡すと、リトライごとに異なる待機時間を設定できます(可変長引数対応)。 ```php theme={null} use Illuminate\Queue\Attributes\Backoff; // 固定の待機時間(全リトライ共通で60秒待機) #[Backoff(60)] class SendEmail implements ShouldQueue { // ... } // リトライごとに異なる待機時間(指数バックオフ) #[Backoff(30, 60, 120)] class ProcessOrder implements ShouldQueue { // ... } ``` `Backoff` クラスの実装を見ると、可変長引数を受け取る設計になっています。 ```php theme={null} // Illuminate\Queue\Attributes\Backoff の実装 #[Attribute(Attribute::TARGET_CLASS)] class Backoff { public array|int $backoff; public function __construct(array|int ...$backoff) { $this->backoff = count($backoff) === 1 ? $backoff[0] : $backoff; } } ``` 単一の値の場合は `int` として、複数の場合は `array` として格納されます。 ### `#[Tries]` — リトライ回数を指定する ジョブが失敗したときの最大リトライ回数を指定します。 ```php theme={null} use Illuminate\Queue\Attributes\Tries; #[Tries(5)] class ProcessPayment implements ShouldQueue { // ... } ``` ### `#[Timeout]` — タイムアウトを指定する ジョブの最大実行時間(秒)を指定します。この時間を超えるとジョブは強制終了されます。 ```php theme={null} use Illuminate\Queue\Attributes\Timeout; #[Timeout(120)] class GenerateReport implements ShouldQueue { // ... } ``` ### `#[MaxExceptions]` — 許容例外数を指定する 指定した回数以上の例外が発生した場合にジョブを失敗とみなします。`#[Tries]` と組み合わせて使います。 ```php theme={null} use Illuminate\Queue\Attributes\MaxExceptions; use Illuminate\Queue\Attributes\Tries; #[Tries(10)] #[MaxExceptions(3)] class ProcessWebhook implements ShouldQueue { // ... } ``` ### `#[UniqueFor]` — ユニーク期間を指定する ジョブの重複実行を防ぐロック期間(秒)を指定します。`ShouldBeUnique` と組み合わせて使います。 ```php theme={null} use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Queue\Attributes\UniqueFor; #[UniqueFor(3600)] class SyncUserData implements ShouldQueue, ShouldBeUnique { // ... } ``` ### `#[DeleteWhenMissingModels]` — モデル未存在時に削除する ジョブが依存するEloquentモデルが見つからない場合、ジョブを失敗ではなく削除(スキップ)として処理します。 ```php theme={null} use Illuminate\Queue\Attributes\DeleteWhenMissingModels; #[DeleteWhenMissingModels] class SendOrderConfirmation implements ShouldQueue { public function __construct( protected Order $order, ) {} public function handle(): void { // $order が存在しない場合はこのジョブが削除される } } ``` ### `#[WithoutRelations]` — リレーションを除外する ジョブのシリアライズ時にモデルのリレーションを含めないようにします。キューへの送信データを軽量化できます。 ```php theme={null} use Illuminate\Queue\Attributes\WithoutRelations; #[WithoutRelations] class ExportUser implements ShouldQueue { public function __construct( protected User $user, ) {} } ``` ### `#[FailOnTimeout]` — タイムアウト時に失敗とする タイムアウトが発生した場合にジョブを失敗として記録します(デフォルトではタイムアウトは失敗として記録されません)。 ```php theme={null} use Illuminate\Queue\Attributes\FailOnTimeout; use Illuminate\Queue\Attributes\Timeout; #[Timeout(30)] #[FailOnTimeout] class ProcessLongTask implements ShouldQueue { // ... } ``` ## 複数のキューアトリビュートを組み合わせる これらのアトリビュートを組み合わせて、ジョブの挙動を宣言的に設定できます。 ```php theme={null} use Illuminate\Queue\Attributes\Backoff; use Illuminate\Queue\Attributes\Connection; use Illuminate\Queue\Attributes\DeleteWhenMissingModels; use Illuminate\Queue\Attributes\FailOnTimeout; use Illuminate\Queue\Attributes\MaxExceptions; use Illuminate\Queue\Attributes\Queue; use Illuminate\Queue\Attributes\Timeout; use Illuminate\Queue\Attributes\Tries; #[Queue('payments')] #[Connection('redis')] #[Tries(3)] #[Backoff(30, 60, 120)] #[Timeout(60)] #[MaxExceptions(2)] #[DeleteWhenMissingModels] class ProcessPayment implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function __construct( protected Order $order, ) {} public function handle(PaymentService $payment): void { $payment->charge($this->order); } } ``` ## Eloquent関連アトリビュート Eloquentモデルのアトリビュートは `Illuminate\Database\Eloquent\Attributes` 名前空間にあります。Laravel 13では多数のアトリビュートが追加されています。 ### `#[ScopedBy]` — グローバルスコープを指定する モデルに自動的に適用するグローバルスコープクラスをアトリビュートで指定します。継承をサポートしており、`IS_REPEATABLE` フラグで複数のスコープを指定できます。 ```php theme={null} use Illuminate\Database\Eloquent\Attributes\ScopedBy; #[ScopedBy(ActiveScope::class)] class User extends Model { // booted() でスコープを登録する必要がなくなる } ``` 複数のスコープを付与する場合、アトリビュートを繰り返すか配列で渡します。 ```php theme={null} // 繰り返し指定(IS_REPEATABLE 対応) #[ScopedBy(ActiveScope::class)] #[ScopedBy(VerifiedScope::class)] class User extends Model { } // 配列でまとめて指定 #[ScopedBy([ActiveScope::class, VerifiedScope::class])] class User extends Model { } ``` 従来の `booted()` メソッドとの比較です。 ```php theme={null} // 従来の書き方 class User extends Model { protected static function booted(): void { static::addGlobalScope(new ActiveScope); static::addGlobalScope(new VerifiedScope); } } ``` ### `#[ObservedBy]` — オブザーバーを指定する モデルに関連付けるオブザーバークラスをアトリビュートで指定します。`ScopedBy` と同様に `IS_REPEATABLE` です。 ```php theme={null} use Illuminate\Database\Eloquent\Attributes\ObservedBy; #[ObservedBy(UserObserver::class)] class User extends Model { } ``` 複数のオブザーバーも指定できます。 ```php theme={null} #[ObservedBy(UserObserver::class)] #[ObservedBy(AuditObserver::class)] class User extends Model { } ``` 従来の `AppServiceProvider` での登録が不要になります。 ```php theme={null} // 従来の書き方(AppServiceProvider) public function boot(): void { User::observe(UserObserver::class); } ``` ### `#[UseEloquentBuilder]` — カスタムクエリビルダーを指定する モデルが使用するカスタムEloquentビルダーをアトリビュートで指定します。 ```php theme={null} use Illuminate\Database\Eloquent\Attributes\UseEloquentBuilder; #[UseEloquentBuilder(UserQueryBuilder::class)] class User extends Model { } ``` ```php theme={null} namespace App\Builders; use Illuminate\Database\Eloquent\Builder; class UserQueryBuilder extends Builder { public function active(): static { return $this->where('active', true); } public function verified(): static { return $this->whereNotNull('email_verified_at'); } } ``` ```php theme={null} // 使用例(型安全にカスタムメソッドを呼べる) $users = User::query()->active()->verified()->get(); ``` ### `#[CollectedBy]` — カスタムコレクションを指定する モデルのコレクションとして使用するカスタムコレクションクラスをアトリビュートで指定します。 ```php theme={null} use Illuminate\Database\Eloquent\Attributes\CollectedBy; #[CollectedBy(UserCollection::class)] class User extends Model { } ``` ```php theme={null} namespace App\Collections; use Illuminate\Database\Eloquent\Collection; class UserCollection extends Collection { public function admins(): static { return $this->filter(fn (User $user) => $user->is_admin); } public function active(): static { return $this->filter(fn (User $user) => $user->active); } } ``` ### `#[Table]` — テーブル設定をまとめて指定する テーブル名、主キー、タイムスタンプなど、複数のテーブル関連設定を1つのアトリビュートで指定できます。 ```php theme={null} use Illuminate\Database\Eloquent\Attributes\Table; #[Table(name: 'system_users', key: 'user_id', timestamps: false)] class SystemUser extends Model { } ``` `Table` アトリビュートで設定できるオプションは以下のとおりです。 | パラメーター | 対応するプロパティ | 説明 | | - | - | - | | `name` | `$table` | テーブル名 | | `key` | `$primaryKey` | 主キーのカラム名 | | `keyType` | `$keyType` | 主キーの型(`'int'`、`'string'` など) | | `incrementing` | `$incrementing` | 主キーの自動インクリメント | | `timestamps` | `$timestamps` | タイムスタンプの有効/無効 | | `dateFormat` | `$dateFormat` | 日付フォーマット | ### `#[Scope]` — メソッドをローカルスコープとして定義する `scope` プレフィックスなしのメソッドをEloquentのローカルスコープとして定義できます。 ```php theme={null} use Illuminate\Database\Eloquent\Attributes\Scope; class User extends Model { #[Scope] protected function active(Builder $query): void { $query->where('active', true); } #[Scope] protected function verified(Builder $query): void { $query->whereNotNull('email_verified_at'); } } ``` ```php theme={null} // 従来はメソッド名に「scope」プレフィックスが必要だった // public function scopeActive(Builder $query): void // アトリビュートを使うと、メソッド名がそのままスコープ名になる User::query()->active()->verified()->get(); ``` ### `#[UseFactory]` — ファクトリクラスを指定する モデルが使用するカスタムファクトリクラスをアトリビュートで指定します。 ```php theme={null} use Illuminate\Database\Eloquent\Attributes\UseFactory; #[UseFactory(UserFactory::class)] class User extends Model { } ``` ### その他のEloquentアトリビュート | アトリビュート | 説明 | | - | - | | `#[Fillable(...$attributes)]` | マスアサインメントで許可するカラムを指定 | | `#[Guarded(...$attributes)]` | マスアサインメントで保護するカラムを指定 | | `#[Unguarded]` | マスアサインメントの保護を無効化 | | `#[Hidden(...$attributes)]` | シリアライズ時に除外するカラムを指定 | | `#[Visible(...$attributes)]` | シリアライズ時に含めるカラムを指定 | | `#[Appends(...$attributes)]` | シリアライズ時に追加するアクセサを指定 | | `#[Touches(...$relations)]` | 更新時に `updated_at` を更新するリレーションを指定 | | `#[WithoutTimestamps]` | タイムスタンプを無効化 | | `#[WithoutIncrementing]` | 主キーの自動インクリメントを無効化 | | `#[DateFormat(format: '...')]` | 日付フォーマットを指定 | | `#[UsePolicy(policyClass: '...')]` | 関連するポリシークラスを指定 | | `#[UseResource(resourceClass: '...')]` | 関連するAPIリソースクラスを指定 | | `#[UseResourceCollection(resourceCollectionClass: '...')]` | 関連するリソースコレクションクラスを指定 | ## Enumサポート(v13.2.0で追加) v13.2.0では、`#[Queue]` と `#[Connection]` がenumを受け入れるようになりました。これにより、文字列リテラルの代わりにPHP enumを使って型安全にキューとコネクションを指定できます。 ```php theme={null} // キュー名のenum定義 enum Queue: string { case Default = 'default'; case High = 'high'; case Low = 'low'; case Emails = 'emails'; case Orders = 'orders'; } // コネクションのenum定義 enum Connection: string { case Redis = 'redis'; case Sqs = 'sqs'; case Database = 'database'; case Sync = 'sync'; } ``` ```php theme={null} use Illuminate\Queue\Attributes\Queue; use Illuminate\Queue\Attributes\Connection; // enumを使った型安全な指定 #[Queue(Queue::Orders)] #[Connection(Connection::Redis)] class ProcessOrder implements ShouldQueue { // ... } ``` enumを使うことで、キュー名やコネクション名のタイポを防ぎ、IDEの補完も利用できます。アプリケーション全体でキュー名とコネクション名を一元管理するのに便利です。 ## 従来のクラスプロパティとの比較 ### アトリビュートのメリット * **宣言的** — クラスの冒頭を見ればジョブの挙動が一目でわかる * **型安全** — enumを使えばIDEの補完と型チェックが効く * **継承との親和性** — 親クラスのアトリビュートを子クラスで上書きできる * **コードの削減** — プロパティ宣言やメソッドオーバーライドが不要 ### アトリビュートのデメリット * **動的な値を設定できない** — アトリビュートの引数はコンパイル時定数のみ。変数や設定ファイルの値は使えない * **読み慣れが必要** — チームでPHP 8のアトリビュート構文に慣れが必要な場合もある ### 動的な値が必要な場合 実行時に値を決定したい場合は、従来のメソッドオーバーライドを使います。 ```php theme={null} class ProcessOrder implements ShouldQueue { // 動的なバックオフはメソッドで定義する public function backoff(): array { return [ config('queue.backoff.first'), config('queue.backoff.second'), ]; } } ``` アトリビュートはPHPのコンパイル時に解析されます。`config()` や `env()` のような実行時の値を使うことはできません。動的な設定が必要な場合は、引き続きクラスプロパティやメソッドを使用してください。 ## 実装の仕組み Laravelは内部でReflection APIを使ってアトリビュートを読み取ります。キューワーカーがジョブをディスパッチする際に、`ReadsQueueAttributes` トレイト(`InteractsWithQueue` に含まれる)がリフレクションでアトリビュートを検出し、対応するプロパティに値を設定します。 ```php theme={null} // 内部での読み取りイメージ(簡略化) $reflection = new ReflectionClass($job); $attributes = $reflection->getAttributes(Queue::class); foreach ($attributes as $attribute) { $instance = $attribute->newInstance(); $job->queue = $instance->queue instanceof UnitEnum ? $instance->queue->value : $instance->queue; } ``` Eloquentモデルのアトリビュートも同様に、`Model::booted()` 相当のタイミングでリフレクションにより読み取られます。 ## 次のステップ Laravelのキューシステムの基本的な使い方を学びます。 Laravelがアトリビュートを読み取るために使うReflection APIの仕組みを詳しく解説します。 # PHP Reflection API Source: https://kawax.biz/jp/advanced/php-reflection PHP Reflection APIの仕組みと、LaravelのIoCコンテナやパッケージ開発での活用方法を解説します。 ## PHP Reflection APIとは PHP Reflection APIは、クラス・メソッド・プロパティ・関数・パラメーターなどのメタ情報を実行時に取得・検査できるPHPの組み込み機能です。クラスのコンストラクタが何を引数に取るか、メソッドにどんなアトリビュートが付いているかなどを、ソースコードを直接書き換えることなく調べられます。 Laravelは`Illuminate/Container/Container.php`の内部でReflection APIを多用しており、DIコンテナの自動解決・PHPアトリビュートの読み取り・メソッドインジェクションなどを実現しています。 ## 主要クラス | クラス | 主な用途 | | - | - | | `ReflectionClass` | クラスのメタ情報を取得する起点 | | `ReflectionMethod` | メソッドの引数・アクセス修飾子・アトリビュートを取得 | | `ReflectionProperty` | プロパティの型・デフォルト値・アトリビュートを取得 | | `ReflectionParameter` | メソッド・関数の引数情報(型ヒント・デフォルト値)を取得 | | `ReflectionFunction` | 関数・クロージャのメタ情報を取得 | | `ReflectionAttribute` | アトリビュートのクラス名・引数を取得 | ### ReflectionClass — クラス情報を取得する ```php theme={null} $ref = new ReflectionClass(UserController::class); $ref->getName(); // クラスの完全修飾名 $ref->getShortName(); // クラス名のみ $ref->isInstantiable(); // インスタンス化できるか $ref->getConstructor(); // コンストラクタを ReflectionMethod で返す $ref->getMethods(); // すべてのメソッドを ReflectionMethod[] で返す $ref->getProperties(); // すべてのプロパティを ReflectionProperty[] で返す $ref->getAttributes(); // クラスに付いたアトリビュートを ReflectionAttribute[] で返す ``` ### ReflectionParameter — コンストラクタ引数を検査する ```php theme={null} $ref = new ReflectionClass(UserController::class); $constructor = $ref->getConstructor(); if ($constructor) { foreach ($constructor->getParameters() as $param) { $param->getName(); // 引数名 $param->getType(); // 型ヒント(ReflectionType) $param->isOptional(); // 省略可能かどうか $param->isVariadic(); // 可変長引数かどうか $param->getDefaultValue(); // デフォルト値(存在する場合) } } ``` ## LaravelコンテナとReflection API LaravelのIoCコンテナは、Reflection APIを使ってコンストラクタインジェクション(依存性の自動解決)を実現しています。`app()->make(SomeClass::class)` や依存性注入が動く仕組みを理解しましょう。 ```mermaid theme={null} sequenceDiagram participant App as アプリコード participant Container as IoC コンテナ participant Reflection as ReflectionClass participant Dep as 依存クラス App->>Container: app()->make(UserController::class) Container->>Reflection: new ReflectionClass(UserController::class) Reflection-->>Container: コンストラクタ情報 Container->>Reflection: getConstructor()->getParameters() Reflection-->>Container: [UserRepository, Cache, ...] loop 各引数を解決 Container->>Container: 型ヒントで再帰的に make() Container->>Dep: new Dep(...) Dep-->>Container: インスタンス end Container-->>App: new UserController(repository, cache, ...) ``` ### コンテナの `build()` メソッド(簡略版) 実際の`Container.php`の`build()`メソッドはおよそ次のようなコードです。 ```php theme={null} // Illuminate\Container\Container::build() を簡略化 public function build($concrete) { // 1. ReflectionClass でクラスを検査 $reflector = new ReflectionClass($concrete); // インスタンス化できないクラス(interface, abstract など)はエラー if (! $reflector->isInstantiable()) { throw new BindingResolutionException("[$concrete] is not instantiable."); } // 2. コンストラクタを取得 $constructor = $reflector->getConstructor(); // コンストラクタなし → そのままインスタンス化 if (is_null($constructor)) { return new $concrete; } // 3. コンストラクタのパラメーターをすべて取得 $dependencies = $constructor->getParameters(); // 4. 各パラメーターを再帰的に解決 $instances = $this->resolveDependencies($dependencies); return new $concrete(...$instances); } protected function resolveDependencies(array $dependencies): array { $results = []; foreach ($dependencies as $dependency) { // 型ヒントが取れる場合はコンテナで再帰解決 $className = Util::getParameterClassName($dependency); $results[] = is_null($className) ? $this->resolvePrimitive($dependency) // プリミティブ型 : $this->resolveClass($dependency, $className); // クラス型 } return $results; } ``` `Util::getParameterClassName()` は `$parameter->getType()` の結果から型名の文字列を取り出すユーティリティです。`ReflectionParameter::getType()` が返す `ReflectionNamedType` を扱いやすくラップしています。 ## PHPアトリビュートの読み取り PHP 8.0以降、Reflection APIを使ってクラス・メソッド・プロパティに付いたアトリビュートを取得できます。Laravelはこの仕組みを使ってキューアトリビュートやEloquentアトリビュートを処理しています。 PHPアトリビュートとLaravelへの組み込みについては [PHPアトリビュート](/jp/advanced/php-attributes) も合わせて参照してください。 ### アトリビュートを読み取る基本パターン ```php theme={null} use ReflectionClass; // 1. クラスに付いたアトリビュートを取得 $ref = new ReflectionClass(ProcessOrder::class); $attrs = $ref->getAttributes(Queue::class); // 特定のアトリビュートだけ取得 foreach ($attrs as $attr) { $instance = $attr->newInstance(); // アトリビュートクラスをインスタンス化 echo $instance->queue; // アトリビュートのプロパティを読む } // 2. すべてのアトリビュートを取得(フィルターなし) $allAttrs = $ref->getAttributes(); foreach ($allAttrs as $attr) { echo $attr->getName(); // アトリビュートクラス名(FQCN) print_r($attr->getArguments()); // コンストラクタ引数 } ``` ### LaravelがQueue属性を読み取る仕組み(簡略版) ```php theme={null} // InteractsWithQueue トレイトの ReadsQueueAttributes より簡略化 protected function setJobInstanceForQueue(object $job): void { $reflection = new ReflectionClass($job); foreach ($reflection->getAttributes(Queue::class) as $attribute) { $instance = $attribute->newInstance(); $job->queue = $instance->queue instanceof \UnitEnum ? $instance->queue->value : $instance->queue; } } ``` ### メソッドのアトリビュートを読み取る ```php theme={null} $ref = new ReflectionClass(UserController::class); foreach ($ref->getMethods() as $method) { $attrs = $method->getAttributes(Route::class); foreach ($attrs as $attr) { $route = $attr->newInstance(); echo "{$method->getName()} => {$route->path}"; } } ``` ## パッケージ開発での活用例 ### クラスの実装インターフェースを検査する パッケージでクラスが特定のインターフェースを実装しているかを動的に確認できます。 ```php theme={null} use ReflectionClass; function isQueueable(string $class): bool { $ref = new ReflectionClass($class); return $ref->implementsInterface(\Illuminate\Contracts\Queue\ShouldQueue::class); } ``` ### メタデータ取得 — アトリビュートでルートを自動登録する アトリビュートとReflectionを組み合わせてルートを自動収集するパターンです。 ```php theme={null} // 独自のRouteアトリビュート定義 #[\Attribute(\Attribute::TARGET_METHOD)] class Route { public function __construct( public string $method, public string $path, ) {} } // コントローラーで使う class UserController { #[Route('GET', '/users')] public function index() { /* ... */ } #[Route('POST', '/users')] public function store() { /* ... */ } } // アトリビュートからルートを自動登録するサービスプロバイダー class AttributeRouteServiceProvider extends ServiceProvider { public function boot(Router $router): void { $ref = new ReflectionClass(UserController::class); foreach ($ref->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { foreach ($method->getAttributes(Route::class) as $attr) { $route = $attr->newInstance(); $router->addRoute( $route->method, $route->path, [UserController::class, $method->getName()], ); } } } } ``` ### 動的なメソッド呼び出し — メソッドインジェクション Laravelの`call()`メソッドはReflectionを使って引数を自動解決します。同じ仕組みをパッケージで実装する例です。 ```php theme={null} use ReflectionFunction; use ReflectionMethod; function callWithDependencies(callable $callable, Container $container): mixed { if (is_array($callable)) { [$object, $method] = $callable; $ref = new ReflectionMethod($object, $method); $params = $ref->getParameters(); } else { $ref = new ReflectionFunction($callable); $params = $ref->getParameters(); } $args = []; foreach ($params as $param) { $type = $param->getType()?->getName(); $args[] = $type ? $container->make($type) : null; } return $callable(...$args); } // 使用例 callWithDependencies([new UserController(), 'index'], app()); ``` ### プロパティのデフォルト値を収集する 設定クラスのデフォルト値をReflectionで取得するパターンです。 ```php theme={null} use ReflectionClass; use ReflectionProperty; function getDefaults(string $class): array { $ref = new ReflectionClass($class); $defaults = []; foreach ($ref->getProperties(ReflectionProperty::IS_PUBLIC) as $prop) { if ($prop->hasDefaultValue()) { $defaults[$prop->getName()] = $prop->getDefaultValue(); } } return $defaults; } class DatabaseConfig { public string $driver = 'mysql'; public int $port = 3306; public bool $strict = true; } // ['driver' => 'mysql', 'port' => 3306, 'strict' => true] $defaults = getDefaults(DatabaseConfig::class); ``` ## パフォーマンスへの配慮 Reflection APIはクラス情報を毎回パースするためコストがかかります。プロダクションコードでは結果をキャッシュするのがベストプラクティスです。 ```php theme={null} class ReflectionCache { private static array $cache = []; public static function getClass(string $class): \ReflectionClass { return self::$cache[$class] ??= new \ReflectionClass($class); } } // 使用例 $ref = ReflectionCache::getClass(UserController::class); ``` PHPのOPcacheはReflectionの結果をキャッシュしません。大量のクラスをループで検査する場合は独自キャッシュを検討してください。ただし、Laravelコンテナ自体も同一リクエスト内では`ReflectionClass`インスタンスを再利用しています。 ## 次のステップ ReflectionClass::getAttributes() を使って読み取るPHPアトリビュートの詳細を学びます。 Reflection APIを活用したLaravelパッケージの開発方法を学びます。 # Pipelineパターン Source: https://kawax.biz/jp/advanced/pipeline Illuminate\Pipeline\Pipeline クラスの仕組みと、複数の処理ステップをチェーンで構成するパイプラインパターンを解説します。 ## パイプラインパターンとは パイプラインパターンは、処理対象のオブジェクト(passable)を一連のパイプ(処理ステップ)に順番に通すデザインパターンです。各パイプは前のステップの出力を受け取り、処理を加えて次のステップに渡します。 Laravelでは `Illuminate\Pipeline\Pipeline` クラスがこのパターンを実装しており、ミドルウェアの処理がまさにこの仕組みで動いています。 ``` リクエスト → [ミドルウェア1] → [ミドルウェア2] → [コントローラー] → レスポンス ``` ## Laravelコアでの使用箇所 パイプラインはLaravelの中核で広く使われています。 * **HTTPミドルウェア** — `Illuminate\Foundation\Http\Kernel` でリクエストをミドルウェアのパイプラインに通す * **コンソールコマンド** — Artisanコマンドの前後処理 * **Routerのミドルウェア** — ルートグループのミドルウェア適用 HTTPカーネルがパイプラインを使う部分を見てみましょう。 ```php theme={null} // Illuminate\Foundation\Http\Kernel より protected function sendRequestThroughRouter($request) { return (new Pipeline($this->app)) ->send($request) ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) ->then($this->dispatchToRouter()); } ``` ## 基本的な使い方 ### send / through / thenReturn 最もシンプルなパイプラインの構成です。 ```php theme={null} use Illuminate\Pipeline\Pipeline; $result = app(Pipeline::class) ->send('hello') ->through([ function (string $passable, Closure $next): string { return $next(strtoupper($passable)); }, function (string $passable, Closure $next): string { return $next($passable . '!'); }, ]) ->thenReturn(); // $result === 'HELLO!' ``` * `send($passable)` — パイプラインに通すオブジェクトを指定する * `through($pipes)` — パイプの配列を指定する * `thenReturn()` — パイプラインを実行して結果を返す ### then 最終的な処理をクロージャで指定したい場合は `then()` を使います。 ```php theme={null} $result = app(Pipeline::class) ->send($request) ->through([AuthPipe::class, LogPipe::class]) ->then(function ($request) { return 'processed: ' . $request; }); ``` ### pipe 後からパイプを追加するには `pipe()` を使います。 ```php theme={null} $pipeline = app(Pipeline::class) ->send($data) ->through([FirstPipe::class]); // 条件によって追加のパイプを挿入 if ($needsExtra) { $pipeline->pipe(ExtraPipe::class); } $result = $pipeline->thenReturn(); ``` ## クラスベースのパイプ クロージャの代わりに専用クラスを使うことで、再利用性が高まります。各パイプクラスは `handle` メソッドを実装します。 ```php theme={null} namespace App\Pipes; use Closure; class TrimPipe { public function handle(string $passable, Closure $next): string { return $next(trim($passable)); } } ``` ```php theme={null} namespace App\Pipes; use Closure; class SanitizePipe { public function handle(string $passable, Closure $next): string { $sanitized = htmlspecialchars($passable, ENT_QUOTES, 'UTF-8'); return $next($sanitized); } } ``` ```php theme={null} use App\Pipes\TrimPipe; use App\Pipes\SanitizePipe; use Illuminate\Pipeline\Pipeline; $result = app(Pipeline::class) ->send(' ') ->through([TrimPipe::class, SanitizePipe::class]) ->thenReturn(); // $result === '<script>alert("xss")</script>' ``` ## via — 呼び出すメソッドを変更する デフォルトではパイプの `handle` メソッドが呼ばれますが、`via()` で別のメソッド名を指定できます。 ```php theme={null} class ValidatePipe { public function process(array $data, Closure $next): array { // バリデーション処理 return $next($data); } } $result = app(Pipeline::class) ->send($data) ->through([ValidatePipe::class]) ->via('process') ->thenReturn(); ``` ## パイプへのパラメーター渡し クラス名に `:` と `,` を使ってパイプにパラメーターを渡せます。これはミドルウェアの `throttle:60,1` のような構文と同じ仕組みです。 ```php theme={null} namespace App\Pipes; use Closure; class LimitLengthPipe { public function handle(string $passable, Closure $next, int $max = 100): string { return $next(substr($passable, 0, $max)); } } ``` ```php theme={null} $result = app(Pipeline::class) ->send($longText) ->through(['App\Pipes\LimitLengthPipe:50']) ->thenReturn(); ``` ## finally — パイプライン終了後の処理 `finally()` メソッドを使うと、パイプライン終了後に必ず実行されるコールバックを登録できます。成功・失敗に関わらず実行されます。 ```php theme={null} $result = app(Pipeline::class) ->send($request) ->through([LogPipe::class, AuthPipe::class]) ->finally(function ($passable) { // ログ記録やリソース解放など logger()->info('Pipeline completed', ['request' => $passable]); }) ->thenReturn(); ``` ## withinTransaction — トランザクション内での実行 Laravel 11以降、パイプライン全体をデータベーストランザクション内で実行できます。 ```php theme={null} $result = app(Pipeline::class) ->send($order) ->through([ CreateOrderPipe::class, ChargePaymentPipe::class, SendConfirmationPipe::class, ]) ->withinTransaction() ->thenReturn(); ``` 特定のデータベース接続を指定することもできます。 ```php theme={null} ->withinTransaction('mysql') ``` いずれかのパイプで例外が発生した場合、トランザクション全体がロールバックされます。 ## 実践例:注文処理パイプライン ECサイトの注文処理を複数のパイプで構成する例です。 ```php theme={null} namespace App\Pipes\Order; use App\Models\Order; use Closure; class ValidateInventoryPipe { public function handle(Order $order, Closure $next): Order { foreach ($order->items as $item) { if ($item->product->stock < $item->quantity) { throw new \RuntimeException("在庫不足: {$item->product->name}"); } } return $next($order); } } ``` ```php theme={null} namespace App\Pipes\Order; use App\Models\Order; use Closure; class ReserveInventoryPipe { public function handle(Order $order, Closure $next): Order { foreach ($order->items as $item) { $item->product->decrement('stock', $item->quantity); } return $next($order); } } ``` ```php theme={null} namespace App\Pipes\Order; use App\Models\Order; use App\Services\PaymentService; use Closure; class ProcessPaymentPipe { public function __construct( protected PaymentService $payment, ) {} public function handle(Order $order, Closure $next): Order { $this->payment->charge($order->total, $order->payment_token); $order->update(['status' => 'paid']); return $next($order); } } ``` ```php theme={null} use App\Models\Order; use App\Pipes\Order\ValidateInventoryPipe; use App\Pipes\Order\ReserveInventoryPipe; use App\Pipes\Order\ProcessPaymentPipe; use Illuminate\Pipeline\Pipeline; class OrderService { public function process(Order $order): Order { return app(Pipeline::class) ->send($order) ->through([ ValidateInventoryPipe::class, ReserveInventoryPipe::class, ProcessPaymentPipe::class, ]) ->withinTransaction() ->thenReturn(); } } ``` ## Pipeline の内部実装 `carry()` メソッドがパイプラインの核心部分です。`array_reduce` を使ってパイプを右から左に畳み込み、クロージャのチェーンを構築します。 ```php theme={null} // Pipeline::then() の内部 protected function carry() { return function ($stack, $pipe) { return function ($passable) use ($stack, $pipe) { if (is_callable($pipe)) { return $pipe($passable, $stack); } elseif (! is_object($pipe)) { [$name, $parameters] = $this->parsePipeString($pipe); $pipe = $this->getContainer()->make($name); $parameters = array_merge([$passable, $stack], $parameters); } else { $parameters = [$passable, $stack]; } return $pipe->{$this->method}(...$parameters); }; }; } ``` `array_reverse` でパイプを逆順にして `array_reduce` に渡すことで、呼び出し順を正しく保ちます。最初のパイプが最初に実行されるよう、クロージャのスタックを内側から積み上げていきます。 パイプラインは「タマネギ型」アーキテクチャとも呼ばれます。リクエストは外側の層(最初のパイプ)から内側に向かって通過し、レスポンスは内側から外側に戻ってきます。各ミドルウェアが `$next()` を呼ぶ前後に処理を加えられるのはこのためです。 ## Macroableトレイトの併用 `Pipeline` クラスは `Macroable` トレイトを使っているため、独自のメソッドを追加できます。 ```php theme={null} use Illuminate\Pipeline\Pipeline; Pipeline::macro('sendThroughLogging', function (array $pipes) { /** @var Pipeline $this */ return $this->through(array_map(function ($pipe) { return function ($passable, $next) use ($pipe) { logger()->debug("Entering pipe: {$pipe}"); $result = $next($passable); logger()->debug("Exiting pipe: {$pipe}"); return $result; }; }, $pipes)); }); $result = app(Pipeline::class) ->send($data) ->sendThroughLogging([TrimPipe::class, SanitizePipe::class]) ->thenReturn(); ``` ## 次のステップ 既存クラスに独自メソッドを追加するMacroableトレイトの使い方を学びます。 # キュージョブの実行制御 Source: https://kawax.biz/jp/advanced/queue-job-control ShouldBeUnique・ShouldBeUniqueUntilProcessing・DebounceFor を使って、キュージョブの重複実行・連続ディスパッチを制御する方法を解説します。 ## 概要 Laravel のキュー機能には、ジョブの **重複排除(Unique)** と **デバウンス(Debounce)** という2種類の実行制御が用意されています。どちらも「同じジョブを何度もディスパッチしたとき、無駄な実行を省く」ための仕組みですが、動作が異なります。 | 機能 | インターフェース / アトリビュート | 目的 | | - | - | - | | Unique Jobs | `ShouldBeUnique` | キューに同一ジョブが1つだけ存在する状態を保つ | | Unique Until Processing | `ShouldBeUniqueUntilProcessing` | 処理開始までのみユニーク制約を保持する | | Debounced Jobs | `#[DebounceFor]` | 短時間に連続ディスパッチされた場合、最新1件だけ実行する | Unique Jobs と Debounced Jobs は**排他的**です。`DebounceFor` アトリビュートを使用するジョブに `ShouldBeUnique` を実装しないでください。 *** ## Unique Jobs — `ShouldBeUnique` 同一ジョブがキューに存在する間は、追加のディスパッチを無視します。 ```php theme={null} productId; } } ``` * `uniqueId()` が返す値がキャッシュロックのキーになります。 * `#[UniqueFor(秒数)]` を指定すると、その秒数が経過した後にロックが自動解放されます(ジョブが処理されなかった場合のフェイルセーフ)。 ### キャッシュドライバーを指定する — `uniqueVia()` デフォルトのキャッシュドライバー以外を使用したい場合は `uniqueVia()` を実装します。 ```php theme={null} use Illuminate\Contracts\Cache\Repository; use Illuminate\Support\Facades\Cache; public function uniqueVia(): Repository { return Cache::driver('redis'); } ``` Unique Jobs はアトミックロックをサポートするキャッシュドライバー(`redis`、`database`、`memcached`、`dynamodb`、`file`、`array`)が必要です。 *** ## `ShouldBeUnique` vs `ShouldBeUniqueUntilProcessing` `ShouldBeUnique` のロックは**ジョブが完了またはリトライ上限に達するまで**保持されます。これが問題になるケースがあります。 **例:** キューに `UpdateSearchIndex(product_id: 42)` が1件あり、ワーカーが処理を開始した直後に同じジョブを再ディスパッチしたい場合。`ShouldBeUnique` では処理完了まで2件目がキューに入りません。 この場合は `ShouldBeUniqueUntilProcessing` を使います。ロックが**処理開始直前に解放**されるため、ワーカーがジョブを取り出した瞬間に次のディスパッチが可能になります。 ```php theme={null} >Q: dispatch() — ロック取得 D->>Q: dispatch() — ロックあり → 無視 note over Q,W: ShouldBeUnique の場合 W->>Q: ジョブ取り出し W->>W: 処理中(ロック保持) D->>Q: dispatch() — ロックあり → 無視 W->>W: 処理完了 — ロック解放 D->>Q: dispatch() — ここでやっとキュー可能 note over Q,W: ShouldBeUniqueUntilProcessing の場合 W->>Q: ジョブ取り出し — ロック解放 D->>Q: dispatch() — ロックなし → キュー可能 W->>W: 処理中 ``` ### 比較まとめ | | `ShouldBeUnique` | `ShouldBeUniqueUntilProcessing` | | - | - | - | | ロック解放タイミング | 処理完了 / 失敗後 | 処理開始直前 | | 処理中の重複ディスパッチ | 無視 | キュー可能 | | ユースケース | 同時実行を完全に防ぎたい | 処理後にすぐ次のジョブを入れたい | *** ## Debounced Jobs — `#[DebounceFor]` `DebounceFor` アトリビュートは Laravel 13 で追加された機能です。 短時間に同じジョブが大量にディスパッチされた場合に、**最後にディスパッチされた1件だけ**を実行します。Web フロントエンドのデバウンスと同じ発想です。 ```php theme={null} productId; } } ``` * `debounceId()` が返す値でジョブを識別します(商品 ID ごとに独立したデバウンスが適用)。 * 30秒の間に同じ `productId` で10回ディスパッチされても、最後の1件だけが実行されます。 ### `maxWait` — 最大待機時間の上限 頻繁に更新されるデータでは、デバウンスが延々と続いてジョブが永遠に実行されない可能性があります。`maxWait` で最大遅延時間を設定できます。 ```php theme={null} #[DebounceFor(30, maxWait: 120)] class UpdateSearchIndex implements ShouldQueue { use Queueable; // ... } ``` この例では、初回ディスパッチから最大 120 秒後には必ず実行されます(30秒のデバウンスが続いても 120 秒でタイムアウト)。 ### キャッシュドライバーを指定する — `debounceVia()` ```php theme={null} use Illuminate\Contracts\Cache\Repository; use Illuminate\Support\Facades\Cache; public function debounceVia(): Repository { return Cache::driver('redis'); } ``` ### `JobDebounced` イベント 後続のディスパッチによって上書きされたジョブは、`Illuminate\Queue\Events\JobDebounced` イベントを発行してキューから削除されます。このイベントをリッスンすることで、デバウンスされたジョブの追跡や監視が可能です。 *** ## どれを使うべきか ```mermaid theme={null} flowchart TD A["同じジョブを複数回ディスパッチする可能性がある"] --> B{"短時間に連続ディスパッチ
    最新1件だけ実行したい?"} B -->|Yes| C["#[DebounceFor]"] B -->|No| D{"キューに1件だけ
    存在させたい?"} D -->|Yes| E{"処理中も重複を防ぎたい?"} E -->|Yes| F["ShouldBeUnique"] E -->|No| G["ShouldBeUniqueUntilProcessing"] D -->|No| H["通常のジョブ"] ``` | ユースケース | 推奨 | | - | - | | 同じ処理がキューに2件以上あっても意味がない | `ShouldBeUnique` | | 処理中も含めて並列実行を防ぎたい | `ShouldBeUnique` | | ワーカーが取り出したらすぐ次を入れたい | `ShouldBeUniqueUntilProcessing` | | ユーザーが保存ボタンを連打しても1回だけ実行したい | `#[DebounceFor]` | | モデル更新のたびに検索インデックスを再構築(大量更新時) | `#[DebounceFor]` + `maxWait` | *** ## 内部実装 ### Unique Jobs のロック機構 `ShouldBeUnique` ジョブがディスパッチされると、Laravel は内部でキャッシュの[アトミックロック](/jp/cache#アトミック操作(ロック))を取得します。ロックキーは以下のフォーマットです: ``` laravel_unique_job:{ジョブクラス名}:{uniqueId()} ``` ロックが取得できなかった場合(既に別のジョブが保持中)、ジョブはキューに追加されません。 ### Debounced Jobs の実装 `DebounceFor` は内部的に「デバウンスウィンドウ」を管理するキャッシュエントリを使います。新しいディスパッチが来るたびに: 1. 既存のジョブをキューから削除(`JobDebounced` イベント発行) 2. 新しいジョブをキューに追加(デバウンス秒数の遅延付き) 3. キャッシュのタイマーをリセット `maxWait` が指定されている場合、初回ディスパッチのタイムスタンプも記録し、その時刻から `maxWait` 秒を超えるデバウンスを防ぎます。 *** ## 関連ページ RateLimited・WithoutOverlapping・ThrottlesExceptions ミドルウェアの解説 ## 参考リンク * [Laravel 公式ドキュメント — Unique Jobs](https://laravel.com/docs/queues#unique-jobs) * [Laravel 公式ドキュメント — Debounced Jobs](https://laravel.com/docs/queues#debounced-jobs) * [`Illuminate\Contracts\Queue\ShouldBeUnique`](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Contracts/Queue/ShouldBeUnique.php) * [`Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing`](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Contracts/Queue/ShouldBeUniqueUntilProcessing.php) * [`Illuminate\Queue\Attributes\DebounceFor`](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Queue/Attributes/DebounceFor.php) * [`Illuminate\Queue\Attributes\UniqueFor`](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Queue/Attributes/UniqueFor.php) # レート制限のカスタマイズ Source: https://kawax.biz/jp/advanced/rate-limiting LaravelのRateLimiterファサードを深掘りし、ユーザー別・プラン別・IPアドレス別のカスタムレート制限の実装から、Redisを使った高可用な設定まで解説します。 ## `RateLimiter` ファサードの仕組み Laravelのレート制限は `Illuminate\Cache\RateLimiting\Limit` クラスと `RateLimiter` ファサードで構成されています。内部的にはキャッシュドライバー(デフォルトはファイルまたはRedis)にカウンターを保存し、リクエスト数を追跡します。 `throttle` ミドルウェアが受け取ったリクエストに対して `RateLimiter::for()` で定義したクロージャを実行し、制限に達していれば `429 Too Many Requests` を返します。 ## `AppServiceProvider` でのカスタムリミッター定義 レート制限の設定は `App\Providers\AppServiceProvider` の `boot()` メソッドで行います。 ```php theme={null} by($request->user()?->id ?: $request->ip()); }); } } ``` `RateLimiter::for()` の第1引数はリミッター名で、`throttle` ミドルウェアから参照する際に使います。第2引数のクロージャは `Illuminate\Cache\RateLimiting\Limit` インスタンスを返す必要があります。 ## ユーザー別・IPアドレス別・プランごとのレート制限 ### 認証済みユーザーとゲストで制限を変える ```php theme={null} RateLimiter::for('uploads', function (Request $request) { return $request->user() ? Limit::perHour(100)->by($request->user()->id) : Limit::perHour(10)->by($request->ip()); }); ``` ### ユーザーのプランに応じた制限 ```php theme={null} RateLimiter::for('api', function (Request $request) { $user = $request->user(); if (! $user) { return Limit::perMinute(30)->by($request->ip()); } return match ($user->plan) { 'enterprise' => Limit::none(), 'pro' => Limit::perMinute(500)->by($user->id), default => Limit::perMinute(60)->by($user->id), }; }); ``` ### IPアドレスによるグローバル制限 特定のエンドポイントに関係なく、IPアドレス単位でスロットリングします。 ```php theme={null} RateLimiter::for('global', function (Request $request) { return Limit::perMinute(1000)->by($request->ip()); }); ``` ### 複数の制限を組み合わせる 配列で返すと、すべての制限が評価されます。いずれかに達した時点で `429` を返します。 ```php theme={null} RateLimiter::for('login', function (Request $request) { return [ Limit::perMinute(10)->by($request->ip()), Limit::perMinute(5)->by($request->input('email')), ]; }); ``` 同じ `by` 値を持つ複数の制限を定義する場合は、キーが衝突しないようにプレフィックスを付けてください。 ```php theme={null} RateLimiter::for('uploads', function (Request $request) { return [ Limit::perMinute(10)->by('minute:' . $request->user()->id), Limit::perDay(1000)->by('day:' . $request->user()->id), ]; }); ``` ## `throttle` ミドルウェアとカスタムリミッター名の指定 `throttle` ミドルウェアに定義したリミッター名を渡します。 ```php theme={null} use Illuminate\Support\Facades\Route; Route::middleware(['throttle:api'])->group(function () { Route::get('/user', function () { /* ... */ }); Route::post('/posts', function () { /* ... */ }); }); ``` ### `bootstrap/app.php` での登録 Laravel 11以降、ミドルウェアは `bootstrap/app.php` で管理します。 ```php theme={null} use Illuminate\Foundation\Application; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__ . '/../routes/web.php', api: __DIR__ . '/../routes/api.php', apiPrefix: 'api', ) ->withMiddleware(function (\Illuminate\Foundation\Configuration\Middleware $middleware): void { $middleware->throttleApi('api'); }) ->create(); ``` ## APIルートへの適用例 `AppServiceProvider` に複数のリミッターを定義します。 ```php theme={null} public function boot(): void { // 一般APIアクセス RateLimiter::for('api', function (Request $request) { return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); }); // ファイルアップロード RateLimiter::for('uploads', function (Request $request) { return $request->user()?->isPro() ? Limit::perHour(500)->by($request->user()->id) : Limit::perHour(50)->by($request->user()?->id ?: $request->ip()); }); // ログイン試行 RateLimiter::for('login', function (Request $request) { return [ Limit::perMinute(10)->by($request->ip()), Limit::perMinute(5)->by($request->input('email')), ]; }); } ``` ```php theme={null} // routes/api.php use Illuminate\Support\Facades\Route; Route::middleware(['auth:sanctum', 'throttle:api'])->group(function () { Route::get('/user', [\App\Http\Controllers\UserController::class, 'show']); Route::get('/posts', [\App\Http\Controllers\PostController::class, 'index']); }); Route::middleware(['auth:sanctum', 'throttle:uploads'])->group(function () { Route::post('/uploads', [\App\Http\Controllers\UploadController::class, 'store']); }); Route::middleware(['throttle:login'])->group(function () { Route::post('/login', [\App\Http\Controllers\AuthController::class, 'login']); }); ``` ## レスポンスヘッダー(`X-RateLimit-*`)の仕組み `throttle` ミドルウェアは制限情報をレスポンスヘッダーに自動付与します。 | ヘッダー | 説明 | | - | - | | `X-RateLimit-Limit` | 許可されているリクエスト数 | | `X-RateLimit-Remaining` | 残りのリクエスト数 | | `Retry-After` | 次のリクエストが可能になるまでの秒数(429時のみ) | | `X-RateLimit-Reset` | 制限がリセットされるUNIXタイムスタンプ | ```http theme={null} HTTP/1.1 429 Too Many Requests X-RateLimit-Limit: 60 X-RateLimit-Remaining: 0 Retry-After: 45 X-RateLimit-Reset: 1717000000 Content-Type: application/json { "message": "Too Many Requests." } ``` ### カスタムレスポンスを返す ```php theme={null} RateLimiter::for('api', function (Request $request) { return Limit::perMinute(60) ->by($request->user()?->id ?: $request->ip()) ->response(function (Request $request, array $headers) { return response()->json([ 'message' => 'リクエスト制限を超えました。しばらくしてから再試行してください。', 'retry_after' => $headers['Retry-After'], ], 429, $headers); }); }); ``` ## `RateLimiter::attempt()` を使った手動チェック `throttle` ミドルウェアを使わず、コードの中で任意のタイミングでレート制限を確認したい場合は `RateLimiter::attempt()` を使います。 ```php theme={null} use Illuminate\Support\Facades\RateLimiter; class SmsController extends Controller { public function send(Request $request): \Illuminate\Http\JsonResponse { $key = 'sms:' . $request->user()->id; $executed = RateLimiter::attempt( key: $key, maxAttempts: 5, callback: function () use ($request) { app(SmsService::class)->send( $request->user()->phone, $request->input('message') ); }, decaySeconds: 3600, // 1時間 ); if (! $executed) { $seconds = RateLimiter::availableIn($key); return response()->json([ 'message' => "SMS送信の制限を超えました。{$seconds}秒後に再試行してください。", ], 429); } return response()->json(['message' => 'SMSを送信しました。']); } } ``` ### 試行回数の確認とリセット ```php theme={null} // 現在の試行回数を取得 $hits = RateLimiter::attempts($key); // 次のリセットまでの秒数 $seconds = RateLimiter::availableIn($key); // 制限に達しているか確認 $tooMany = RateLimiter::tooManyAttempts($key, $maxAttempts = 5); // カウンターを手動でリセット(ログアウト後など) RateLimiter::clear($key); ``` ### ログインスロットリングの例 ```php theme={null} public function login(Request $request): mixed { $key = 'login:' . $request->input('email'); if (RateLimiter::tooManyAttempts($key, 5)) { $seconds = RateLimiter::availableIn($key); throw ValidationException::withMessages([ 'email' => "ログイン試行回数が多すぎます。{$seconds}秒後に再試行してください。", ]); } if (! Auth::attempt($request->only('email', 'password'))) { RateLimiter::hit($key, 300); // 5分間カウント throw ValidationException::withMessages([ 'email' => 'メールアドレスまたはパスワードが正しくありません。', ]); } RateLimiter::clear($key); return redirect()->intended('/dashboard'); } ``` ## レスポンスベースのレート制限 特定のレスポンスのみカウントしたい場合は `after()` を使います。404レスポンスのみカウントすることでリソース列挙攻撃を防ぐ例: ```php theme={null} use Symfony\Component\HttpFoundation\Response; RateLimiter::for('resource-lookup', function (Request $request) { return Limit::perMinute(10) ->by($request->user()?->id ?: $request->ip()) ->after(function (Response $response) { return $response->getStatusCode() === 404; }); }); ``` ## Redisを使ったレート制限 デフォルトのキャッシュドライバーをRedisに変更するだけで、`throttle` ミドルウェアも自動的にRedisを使います。 ### Redisドライバーの設定 ```php theme={null} // config/cache.php 'default' => env('CACHE_DRIVER', 'redis'), ``` ```ini theme={null} # .env CACHE_DRIVER=redis REDIS_HOST=127.0.0.1 REDIS_PORT=6379 ``` ### `throttleWithRedis` を使う Redis専用の最適化されたスロットリングミドルウェアを使うには `bootstrap/app.php` で `throttleWithRedis()` を呼び出します。 ```php theme={null} use Illuminate\Foundation\Application; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__ . '/../routes/web.php', api: __DIR__ . '/../routes/api.php', apiPrefix: 'api', ) ->withMiddleware(function (\Illuminate\Foundation\Configuration\Middleware $middleware): void { $middleware->throttleWithRedis(); }) ->create(); ``` これにより `throttle` ミドルウェアが `ThrottleRequestsWithRedis` クラスにマッピングされ、Redisのアトミック操作を使って正確なカウントが行われます。 `throttleWithRedis()` を使う場合は必ずRedisが利用可能な状態にしてください。Redisへの接続が失敗すると、リクエストがすべて拒否される可能性があります。 ### Redisを使う利点 * **水平スケーリング対応** — 複数のサーバーインスタンス間でカウンターを共有できる * **高精度** — アトミック操作でレースコンディションを防ぐ * **TTL管理** — Redisのネイティブな有効期限機能でカウンターを自動削除 ## 関連ページ Redisを含むLaravelのキャッシュドライバーの設定と使い方を確認します。 # Laravelスターターキットの作り方 Source: https://kawax.biz/jp/advanced/starter-kit-creation Laravelスターターキットを作成してPackagistに公開し、Laravelとフロントエンドのバージョンアップに継続対応する実践フローを解説します。 ## はじめに スターターキットは、`laravel new` でアプリを作るときに `create-project` で展開されるテンプレートプロジェクトです。Laravel 13 の公式スターターキットは React / Vue / Svelte / Livewire の4種類があり、認証や初期UIを含みます。 あなたが独自スターターキットを作るときも、やることは同じです。Laravelアプリをベースに整備して、Composerパッケージとして公開します。 * 公式ドキュメント: [Starter Kits](https://laravel.com/docs/starter-kits) * 公式実装例: [laravel/react-starter-kit](https://github.com/laravel/react-starter-kit) * 参考ページ: [Laravel Console Starter](/jp/packages/laravel-console-starter/index) ## スターターキットの作成 ### Laravelプロジェクトを初期化する まずテンプレートになるLaravelプロジェクトを作り、不要なサンプルコードを整理します。次に `composer.json` の `name` に一意なパッケージ名を付けます。 ```json theme={null} { "name": "example/starter-kit", "type": "project", "require": { "php": "^8.3", "laravel/framework": "^13.0" } } ``` この `name` が、後で `laravel new my-app --using=example/starter-kit` で使われる識別子です。 ### composer.json の scripts を設定する `laravel new` 実行後は、スターターキット側のComposer scriptsが初期化フローに使われます。`post-root-package-install` と `post-create-project-cmd` は最低限そろえてください。 ```json theme={null} "scripts": { "post-root-package-install": [ "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" ], "post-create-project-cmd": [ "@php artisan key:generate --ansi", "@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"", "@php artisan migrate --graceful --ansi" ] } ``` この例は [laravel/react-starter-kit の composer.json](https://github.com/laravel/react-starter-kit/blob/main/composer.json) をベースにした構成です。データベースを使うならそのまま流用できます。 ### .gitattributes で配布対象を制御する スターターキットのリポジトリには置きたいが、ユーザーの生成済みプロジェクトには含めたくないファイルを `export-ignore` で除外します。 ```gitattributes theme={null} LICENSE export-ignore composer.lock export-ignore README.md export-ignore ``` 詳細は [.gitattributes の公式例](https://github.com/laravel/react-starter-kit/blob/main/.gitattributes) を確認してください。 ## Packagist への登録 ユーザーが `--using` で利用できるようにするには、Packagist登録が必要です。 `composer.json` の `name` とリポジトリURLを整合させて公開します。 [Packagist](https://packagist.org/) でリポジトリを登録します。公開後、`example/starter-kit` が解決可能になります。 ユーザー向けに次の導入コマンドを記載します。 ```bash theme={null} laravel new my-app --using=example/starter-kit ``` ## laravel new コマンドのフロー `laravel/installer` の `NewCommand` は、スターターキット指定時に `create-project` 先を切り替えて初期化コマンドを順番に実行します。 ```mermaid theme={null} flowchart TD A["laravel new my-app --using=example/starter-kit"] --> B["getStarterKit() でパッケージ名を決定"] B --> C["composer create-project example/starter-kit my-app"] C --> D["composer run post-root-package-install -d my-app"] D --> E["artisan key:generate など初期化処理"] E --> F["必要に応じて migrate / npm install を実行"] ``` [NewCommand.php](https://github.com/laravel/installer/blob/master/src/NewCommand.php) で確認したい箇所: * [--using オプション定義](https://github.com/laravel/installer/blob/958a4e7c1386199a63d3c5e49a18b2e5c2ad1600/src/NewCommand.php#L119) * [`create-project` と `post-root-package-install` の実行](https://github.com/laravel/installer/blob/958a4e7c1386199a63d3c5e49a18b2e5c2ad1600/src/NewCommand.php#L521-L569) * [`getStarterKit()` の実装](https://github.com/laravel/installer/blob/958a4e7c1386199a63d3c5e49a18b2e5c2ad1600/src/NewCommand.php#L1237-L1255) ## フロントエンドフレームワークの選択 Laravel 13 の公式スターターキットは React / Vue / Svelte / Livewire を提供していますが、独自スターターキットはこの構成に合わせる必要はありません。目的に合わせて CSS や component library、認証方式を自由に決めて問題ありません。 | 選択肢 | 主な構成 | 向いているケース | | - | - | - | | 公式 React / Vue / Svelte / Livewire | Inertia 3 や Livewire 4 を使う公式構成 | 公式に近い開発体験を提供したい | | 独自フロントエンド | 任意のCSSやcomponent library | 既存デザインシステムや別CSS基盤を使いたい | | シンプル Blade キット | Blade中心、最小限のフロント依存 | 軽量な構成で素早く導入したい | | 独自認証キット | Fortifyに限定しない(例: Socialite中心) | 特定の認証方式に絞りたい | コミュニティのスターターキットは、公式とは異なるCSS基盤を採用する例が一般的です。認証もFortify前提にせず、要件に合わせて設計してください。 ## バージョンアップメンテナンス スターターキットは「作って終わり」ではありません。LaravelやPHPの更新に合わせて継続的に追従します。 ### Laravel・PHPバージョンアップ対応 まず `composer.json` の要件を更新し、CIで互換性を確認します。 ```json theme={null} "require": { "php": "^8.3", "laravel/framework": "^13.0" } ``` Laravelメジャー更新時は、`laravel/framework` と周辺依存を同時に見直してください。 ### 定期的な依存パッケージ更新 フロントエンド依存は変化が速いため、月次で更新タスクを回すのが安全です。 * Tailwind * component library (shadcn/ui, shadcn-vue, shadcn-svelte, Flux UI) * Inertia / Livewire 関連パッケージ ### 互換性テスト 少なくとも次の確認をCIに入れてください。 * `composer install` が成功する * `php artisan test` が通る * `npm install && npm run build` が通る ## ベストプラクティス * **`name` は早い段階で固定する**: Packagist公開後の変更コストが高いです。 * **scripts は公式例を基準にする**: 初期化フローを崩すと導入失敗が増えます。 * **`.gitattributes` を最初に整備する**: テンプレート配布時のノイズを減らせます。 * **アップグレードを定期化する**: Laravelリリース後すぐに追従できる運用を作ってください。 * **関連ガイドを併読する**: [パッケージ開発の基礎](/jp/advanced/package-development) と [パッケージのバージョン互換性管理](/jp/advanced/package-versioning) を合わせて読むと設計が安定します。 # Support Contracts (Arrayable / Jsonable / Htmlable / Responsable) Source: https://kawax.biz/jp/advanced/support-contracts Illuminate\Contracts\Support の主要コントラクト4種を、Laravel 13 の公式ソースコードに基づいて解説します。 ## Support Contractsとは `Illuminate\Contracts\Support` には、Laravel全体で使われる小さく重要なインターフェースが集まっています。\ とくに `Arrayable` / `Jsonable` / `Htmlable` / `Responsable` は、Value Object・DTO・レスポンスオブジェクトをLaravelの既存フローに自然に乗せるための基本パターンです。 ```mermaid theme={null} flowchart TD A["Value Object / DTO"] --> B["Arrayable
    toArray()"] A --> C["Jsonable
    toJson($options = 0)"] A --> D["Htmlable
    toHtml()"] A --> E["Responsable
    toResponse($request)"] B --> F["JsonResponse / Eloquent / Collection"] C --> F D --> G["e() helper / Blade output"] E --> H["Controller return / Router::toResponse()"] ``` ここで扱うシグネチャは Laravel 13.x の公式ソース(`laravel/framework`)を参照しています。 ## 1) Arrayable ### インターフェース定義 ```php theme={null} interface Arrayable { public function toArray(); } ``` ### 実装例 ```php theme={null} use Illuminate\Contracts\Support\Arrayable; final class Money implements Arrayable { public function __construct( public readonly int $amount, public readonly string $currency, ) {} public function toArray(): array { return [ 'amount' => $this->amount, 'currency' => $this->currency, ]; } } ``` ### Laravelコアでの使用例 * `Illuminate\Database\Eloquent\Model` は `Arrayable` を実装し、`toArray()` を提供 * `Illuminate\Http\JsonResponse::setData()` は `Arrayable` を検出すると `json_encode($data->toArray(), ...)` でシリアライズ ### パッケージ開発での活用ポイント * DTOやValue Objectをコントローラー・リソース・ログ出力で共通フォーマット化できる * `array` への変換責務をオブジェクト側に閉じ込められる ## 2) Jsonable ### インターフェース定義 ```php theme={null} interface Jsonable { public function toJson($options = 0); } ``` ### 実装例 ```php theme={null} use Illuminate\Contracts\Support\Jsonable; final class ApiPayload implements Jsonable { public function __construct( private array $data, ) {} public function toJson($options = 0): string { return json_encode([ 'data' => $this->data, 'generated_at' => now()->toIso8601String(), ], $options | JSON_THROW_ON_ERROR); } } ``` ### Laravelコアでの使用例 * `Model` は `Jsonable` も実装し、`toJson($options = 0)` を提供 * `JsonResponse::setData()` は `Jsonable` を最優先で判定して `toJson()` を使う ### パッケージ開発での活用ポイント * 監査ログ・Webhook送信などでJSON構造を厳密に固定できる * `json_encode()` 側に任せず、ドメイン都合のJSON表現を明示できる ## 3) Htmlable ### インターフェース定義 ```php theme={null} interface Htmlable { public function toHtml(); } ``` ### 実装例 ```php theme={null} use Illuminate\Contracts\Support\Htmlable; final class BadgeHtml implements Htmlable { public function __construct( private string $label, ) {} public function toHtml(): string { $escaped = e($this->label); return "{$escaped}"; } } ``` ### Laravelコアでの使用例 * `Illuminate\Support\HtmlString` は `Htmlable` を実装 * `e()` ヘルパーは引数が `Htmlable` の場合 `toHtml()` を返す(再エスケープしない) ### パッケージ開発での活用ポイント * Blade内で安全にHTML断片を渡す責務境界を明確にできる * `{!! $obj !!}` を使う場面でも、出力をオブジェクトで管理しやすい `Htmlable` を実装するクラス内では、ユーザー入力をそのまま連結せず、必要な値は `e()` でエスケープしてから埋め込んでください。 ## 4) Responsable ### インターフェース定義 ```php theme={null} interface Responsable { public function toResponse($request); } ``` ### 実装例 ```php theme={null} use Illuminate\Contracts\Support\Responsable; final class ExportCsvResponse implements Responsable { public function __construct( private array $rows, private string $filename = 'export.csv', ) {} /** * @param \Illuminate\Http\Request $request */ public function toResponse($request) { return response()->streamDownload(function () { $stream = fopen('php://output', 'w'); foreach ($this->rows as $row) { fputcsv($stream, $row); } fclose($stream); }, $this->filename, [ 'Content-Type' => 'text/csv', ]); } } ``` ### Laravelコアでの使用例 * `Illuminate\Routing\Router::toResponse()` は最初に `Responsable` を判定し、`$response->toResponse($request)` を呼び出す * `Illuminate\Http\Resources\Json\JsonResource` は `Responsable` を実装しており、コントローラーから直接 `return UserResource::make($user);` が可能 ### パッケージ開発での活用ポイント * コントローラーで配列組み立てをせず、レスポンス生成をオブジェクトへ移譲できる * 「DTOをそのままreturn」する設計にしやすく、HTTP表現とドメイン表現を分離しやすい ## 実装の使い分けガイド `Arrayable` を実装し、`toArray()` に正規化ロジックを集約します。 `Jsonable` を実装し、`toJson($options)` で出力形式を明示します。 `Htmlable` を実装し、`toHtml()` でレンダリング文字列を返します。 `Responsable` を実装し、`toResponse($request)` へ責務を集約します。 ## 次に読むページ 既存クラスに独自メソッドを追加する拡張パターンを学びます。 `when()` / `unless()` で条件分岐を組み込む設計を学びます。 副作用を挟みつつ値を返すチェーン設計を学びます。 `dump()` / `dd()` をオブジェクトへ組み込むデバッグ手法を学びます。 # tap() ヘルパーと Tappable トレイト Source: https://kawax.biz/jp/advanced/tap Laravelのtap()ヘルパーとIlluminate\Support\Traits\Tappableトレイトの使い方と実装パターンを解説します。 ## tap() とは `tap()` は「値を使いながら、その値をそのまま返す」ためのヘルパーです。副作用を挟みたいときに使います。 `Illuminate\Support\helpers.php` の実装はシンプルです。 ```php theme={null} function tap($value, $callback = null) { if (is_null($callback)) { return new HigherOrderTapProxy($value); } $callback($value); return $value; } ``` ```mermaid theme={null} flowchart TD A["tap(value, callback) を呼ぶ"] --> B{"callback は null ?"} B -- はい --> C["HigherOrderTapProxy を返す"] B -- いいえ --> D["callback(value) を実行"] D --> E["元の value を返す"] ``` `tap()` はコールバックの戻り値を無視します。戻り値は常に元の値です。 ## 基本的な使い方 もっとも基本的な使い方は、値を受け取って処理したあと、そのまま返す形です。 ```php theme={null} use App\Models\User; $user = tap(User::query()->latest()->firstOrFail(), function (User $model) { $model->update(['last_seen_at' => now()]); }); // update() の戻り値に関係なく $user が返る ``` コールバックを省略すると `HigherOrderTapProxy` が返るので、メソッド呼び出しをそのままつなげられます。 ```php theme={null} $updatedUser = tap($user)->update([ 'name' => $name, 'email' => $email, ]); // update() の戻り値ではなく元の User インスタンスが返る ``` ## よくある使い所 ### デバッグ出力を挟む ```php theme={null} $result = tap($query->get(), function ($users) { logger()->debug('Fetched users', ['count' => $users->count()]); }); ``` ### チェーン中にログやイベント送信を挿入する ```php theme={null} $order = tap(Order::create($payload), function (Order $order) { event(new OrderCreated($order)); logger()->info('Order created', ['id' => $order->id]); }); ``` ### 返り値を変えずに副作用だけ実行する ```php theme={null} $response = tap($service->handle($request), function ($response) { \Illuminate\Support\Facades\Cache::increment('service_handle_success_total'); }); ``` 返り値を加工したいなら `with()` や通常の変数代入を使います。`tap()` は副作用に限定すると読みやすくなります。 ## Tappable トレイトとは `Illuminate\Support\Traits\Tappable` を `use` すると、クラスに `tap()` メソッドを追加できます。 ```php theme={null} trait Tappable { public function tap($callback = null) { return tap($this, $callback); } } ``` つまりインスタンスメソッド版の `tap()` を提供するだけです。 ```php theme={null} use Illuminate\Support\Traits\Tappable; class ReportBuilder { use Tappable; } $builder = new ReportBuilder(); $builder = $builder->tap(function (ReportBuilder $instance) { logger()->debug('builder initialized'); }); ``` ## パッケージ開発での活用 `Tappable` は Fluent API の途中で副作用を差し込みたいときに便利です。`Macroable` と `Conditionable` を組み合わせると、Laravelらしい拡張可能なビルダーを作れます。 ```php theme={null} use Illuminate\Support\Traits\Conditionable; use Illuminate\Support\Traits\Macroable; use Illuminate\Support\Traits\Tappable; class QueryPresetBuilder { use Macroable; use Conditionable; use Tappable; protected array $filters = []; public function where(string $key, mixed $value): static { $this->filters[$key] = $value; return $this; } public function toArray(): array { return $this->filters; } } ``` ```php theme={null} QueryPresetBuilder::macro('forActiveUsers', function () { /** @var QueryPresetBuilder $this */ return $this->where('active', true); }); $filters = (new QueryPresetBuilder) ->forActiveUsers() ->when($request->filled('role'), fn ($builder) => $builder->where('role', $request->role)) ->tap(fn ($builder) => logger()->debug('current filters', $builder->toArray())) ->toArray(); ``` 関連ページ: * [Macroableトレイト](/jp/advanced/macroable) * [Conditionableトレイト](/jp/advanced/conditionable) * [VOICEVOX for Laravel](/jp/packages/laravel-voicevox) — `TalkAudioQuery` や `SongAudioQuery` を `tap()` で調整してから `generate()` へ流す実用例 ## Laravelコア内での使用例 Laravel本体でも `tap()` / `Tappable` は実際に使われています。 * `Illuminate\Routing\Router` は `Macroable` と `Tappable` を併用しています * `Router::respondWithRoute()` ではコールバックなしの `tap($route)->bind(...)` を使い、`bind()` 実行後も元の `$route` を返しています * `Router::prepareResponse()` ではレスポンス変換後に `tap(..., fn (...) => event(...))` でイベントを発火しています * `Illuminate\Testing\Fluent\Concerns\Has` では `->tap(...)->first(...)->etc()` というチェーンでアサーション補助を実装しています ```php theme={null} // Illuminate\Routing\Router より $route = tap($this->routes->getByName($name))->bind($this->currentRequest); return tap(static::toResponse($request, $response), function ($response) use ($request) { $this->events->dispatch(new ResponsePrepared($request, $response)); }); ``` `tap()` は単体だと小さなヘルパーですが、`Macroable`・`Conditionable` と合わせると Laravel でよく見る読みやすいメソッドチェーンを作りやすくなります。 ## 次のステップ 既存クラスに独自メソッドを追加する設計を学びます。 `when()` / `unless()` による条件分岐チェーンを学びます。 WeakMapを使ったキャッシュの仕組みと循環参照防止への応用を学びます。 # Pestを使った高度なテスト Source: https://kawax.biz/jp/advanced/testing-pest Laravel 11以降でデフォルトになったPestの高度な使い方を、Expectation API・データセット・フェイク・Mockeryまで体系的に解説します。 ## Pestとは [Pest](https://pestphp.com) はPHPUnit上に構築されたテストフレームワークで、Laravel 11以降の新規プロジェクトにデフォルトで採用されています。PHPUnitの豊富なアサーション群をそのまま利用しながら、クロージャベースの簡潔な構文でテストを記述できます。 PHPUnitとの主な違い: | 観点 | Pest | PHPUnit | | - | - | - | | テスト定義 | `test()` / `it()` クロージャ | メソッドで定義するクラス | | アサーション | Expectation API (`expect()->toBe()`) | `$this->assert*()` | | データセット | `dataset()` / `with()` | `@dataProvider` | | フック | `beforeEach()` / `afterEach()` | `setUp()` / `tearDown()` | | ネスト | `describe()` でグループ化 | クラスで分離 | PestのテストはPHPUnitで実行されるため、既存のPHPUnitテストと共存できます。`php artisan test` または `vendor/bin/pest` で実行します。 ## `describe` / `it` / `test` の使い分け ### `test()` 最もシンプルな定義。テスト名をそのまま説明文として使います。 ```php theme={null} test('ユーザーはメールアドレスでログインできる', function () { $user = User::factory()->create(); $response = $this->post('/login', [ 'email' => $user->email, 'password' => 'password', ]); $response->assertRedirect('/dashboard'); }); ``` ### `it()` 「it should...」のような英語的な文体で自然言語に近い記述ができます。 ```php theme={null} it('認証されていないユーザーをログインページにリダイレクトする', function () { $response = $this->get('/dashboard'); $response->assertRedirect('/login'); }); ``` ### `describe()` 関連するテストをグループ化します。`beforeEach()` の共有やファイル内での論理的な整理に使います。 ```php theme={null} describe('注文管理', function () { beforeEach(function () { $this->user = User::factory()->create(); $this->actingAs($this->user); }); it('注文一覧を取得できる', function () { Order::factory(3)->for($this->user)->create(); $response = $this->get('/orders'); $response->assertOk()->assertJsonCount(3, 'data'); }); it('他のユーザーの注文は取得できない', function () { $other = User::factory()->create(); Order::factory()->for($other)->create(); $response = $this->get('/orders'); $response->assertOk()->assertJsonCount(0, 'data'); }); }); ``` `describe()` はネストできます。ただし深すぎるネストはテストを読みにくくするため、2階層までを目安にしましょう。 ## Expectation API `expect()` はPest独自のアサーション構文です。メソッドチェーンで条件を積み重ねられます。 ### 基本的なアサーション ```php theme={null} expect($value)->toBe(42); // 厳密な等価(===) expect($value)->toEqual(['a' => 1]); // 緩やかな等価(==) expect($value)->toBeTrue(); expect($value)->toBeFalse(); expect($value)->toBeNull(); expect($value)->not->toBeNull(); expect($string)->toContain('Laravel'); expect($array)->toHaveCount(3); expect($array)->toHaveKey('email'); expect($array)->toContain('admin'); expect($number)->toBeGreaterThan(0); expect($number)->toBeLessThanOrEqual(100); ``` ### モデルに対するアサーション ```php theme={null} expect($user)->toBeInstanceOf(User::class); expect($user->email)->toMatchRegex('/^.+@.+\..+$/'); // データベースへの保存を確認 expect(User::where('email', 'test@example.com')->exists())->toBeTrue(); ``` ### `and()` チェーン ```php theme={null} expect($response->status())->toBe(200) ->and($response->json('name'))->toBe('Laravel') ->and($response->json('version'))->toBeGreaterThan(12); ``` ### `each()` で配列の各要素を検証 ```php theme={null} $users = User::factory(3)->create(); expect($users)->each(function ($user) { $user->toBeInstanceOf(User::class) ->email->not->toBeNull(); }); ``` ## データセットを使ったパラメータ化テスト 同じロジックを複数の入力値でテストするには `dataset()` またはインライン `with()` を使います。 ### インラインデータセット ```php theme={null} it('無効なメールアドレスはバリデーションエラーになる', function (string $email) { $response = $this->post('/register', ['email' => $email]); $response->assertInvalid('email'); })->with([ 'プレーンテキスト' => ['not-an-email'], 'ドメインなし' => ['user@'], '空文字' => [''], ]); ``` ### 名前付きデータセット `tests/Datasets` ディレクトリにデータセットを定義します。 ```php theme={null} // tests/Datasets/InvalidEmails.php dataset('invalid_emails', [ 'plain' => ['not-an-email'], 'no-domain' => ['user@'], 'empty' => [''], 'spaces' => ['user @example.com'], ]); ``` ```php theme={null} it('無効なメールアドレスはバリデーションエラーになる', function (string $email) { $response = $this->post('/register', ['email' => $email]); $response->assertInvalid('email'); })->with('invalid_emails'); ``` ### クロージャを使った動的データセット ```php theme={null} it('ユーザープランごとに異なる制限がある', function (string $plan, int $limit) { $user = User::factory()->create(['plan' => $plan]); expect($user->requestLimit())->toBe($limit); })->with([ ['free', 100], ['pro', 1000], ['enterprise', 10000], ]); ``` ## Mockeryを使ったユニットテスト ### サービスのモック ```php theme={null} use App\Services\PaymentGateway; use Mockery\MockInterface; test('決済サービスが呼び出される', function () { $mock = $this->mock(PaymentGateway::class, function (MockInterface $mock) { $mock->expects('charge') ->with(1000, 'jpy') ->andReturn(['status' => 'succeeded']); }); $result = app(PaymentGateway::class)->charge(1000, 'jpy'); expect($result['status'])->toBe('succeeded'); }); ``` ### スパイ スパイはモックと違い、実装を呼び出しながら呼び出し記録を残します。 ```php theme={null} use App\Services\NotificationService; test('通知サービスが呼び出されたか確認する', function () { $spy = $this->spy(NotificationService::class); $this->post('/orders', ['amount' => 1000]); $spy->shouldHaveReceived('send')->once()->with('order.created'); }); ``` ### 部分モック 一部のメソッドだけモックし、他は実際の実装を使います。 ```php theme={null} use App\Services\ReportService; use Mockery\MockInterface; test('レポートのファイル書き込みをスキップする', function () { $mock = $this->partialMock(ReportService::class, function (MockInterface $mock) { $mock->expects('writeFile')->andReturnNull(); }); $result = $mock->generate(); expect($result)->not->toBeNull(); }); ``` ## HTTPフェイクによる外部API呼び出しのテスト `Http::fake()` を使うと実際のHTTPリクエストを送らずにレスポンスをスタブできます。 ### 基本的なフェイク ```php theme={null} use Illuminate\Support\Facades\Http; test('外部APIからユーザー情報を取得する', function () { Http::fake([ 'https://api.example.com/users/*' => Http::response([ 'id' => 1, 'name' => 'Laravel User', ], 200), ]); $result = app(UserApiClient::class)->find(1); expect($result['name'])->toBe('Laravel User'); Http::assertSent(fn ($request) => $request->url() === 'https://api.example.com/users/1'); }); ``` ### エラーレスポンスのテスト ```php theme={null} test('APIがエラーを返したときに例外が発生する', function () { Http::fake([ 'api.example.com/*' => Http::response([], 503), ]); expect(fn () => app(UserApiClient::class)->find(1)) ->toThrow(\App\Exceptions\ApiUnavailableException::class); }); ``` ### ネットワーク障害のシミュレーション ```php theme={null} use Illuminate\Http\Client\ConnectionException; test('接続エラーをハンドリングする', function () { Http::fake(fn () => throw new ConnectionException('Connection refused')); $result = app(UserApiClient::class)->findWithFallback(1); expect($result)->toBeNull(); }); ``` ## イベント・メール・通知のフェイク ### `Event::fake()` ```php theme={null} use Illuminate\Support\Facades\Event; use App\Events\OrderCreated; test('注文作成時にイベントが発行される', function () { Event::fake(); $this->post('/orders', ['product_id' => 1, 'amount' => 1000]); Event::assertDispatched(OrderCreated::class, function ($event) { return $event->order->amount === 1000; }); }); ``` 特定のイベントだけフェイクし、他は実際に処理させることもできます。 ```php theme={null} Event::fake([OrderCreated::class]); ``` ### `Mail::fake()` ```php theme={null} use Illuminate\Support\Facades\Mail; use App\Mail\WelcomeEmail; test('ユーザー登録時にウェルカムメールが送信される', function () { Mail::fake(); $this->post('/register', [ 'name' => 'Test User', 'email' => 'test@example.com', 'password' => 'password', 'password_confirmation' => 'password', ]); Mail::assertSent(WelcomeEmail::class, function ($mail) { return $mail->hasTo('test@example.com'); }); }); ``` ### `Notification::fake()` ```php theme={null} use Illuminate\Support\Facades\Notification; use App\Notifications\OrderShipped; test('出荷時に通知が送られる', function () { Notification::fake(); $user = User::factory()->create(); $order = Order::factory()->for($user)->create(); $this->post("/orders/{$order->id}/ship"); Notification::assertSentTo($user, OrderShipped::class, function ($notification) use ($order) { return $notification->order->id === $order->id; }); }); ``` Laravel 13では `Mail::assertSentOnce()` / `Mail::assertQueuedOnce()` / `Notification::assertSentToOnce()` / `Notification::assertSentOnDemandOnce()` が追加され、「ちょうど1回だけ送信された」ことを簡潔にアサートできます。詳細は[メール送信](/jp/mail#メールのテスト)と[Notification](/jp/notifications#通知のテスト)を参照してください。 ## Artisanコマンドのテスト ```php theme={null} use Illuminate\Support\Facades\Artisan; test('不要なデータを削除するコマンドが動作する', function () { $expired = Order::factory()->create(['expires_at' => now()->subDay()]); $valid = Order::factory()->create(['expires_at' => now()->addDay()]); $this->artisan('orders:cleanup') ->assertSuccessful() ->expectsOutput('Cleaned up 1 expired order(s).'); expect(Order::find($expired->id))->toBeNull(); expect(Order::find($valid->id))->not->toBeNull(); }); ``` ### 対話型コマンドのテスト ```php theme={null} test('対話型コマンドで確認後に処理が実行される', function () { $this->artisan('reports:generate') ->expectsQuestion('実行しますか?', 'yes') ->expectsOutput('レポートを生成しました。') ->assertExitCode(0); }); ``` ## `RefreshDatabase` と `LazilyRefreshDatabase` ### `RefreshDatabase` 各テスト後にデータベースをロールバックし、クリーンな状態を保ちます。テストスイート開始時にマイグレーションを実行します。 ```php theme={null} // tests/Pest.php uses(RefreshDatabase::class)->in('Feature'); ``` ```php theme={null} use Illuminate\Foundation\Testing\RefreshDatabase; test('ユーザーを作成できる', function () { $user = User::factory()->create(['name' => 'Laravel']); expect(User::count())->toBe(1); expect($user->name)->toBe('Laravel'); }); ``` ### `LazilyRefreshDatabase` `RefreshDatabase` はすべてのテストでマイグレーションを確認しますが、`LazilyRefreshDatabase` はデータベースを実際に変更するテストが実行されるまでマイグレーションを遅延させます。データベースに触れないテストが多い場合に高速化できます。 ```php theme={null} // tests/Pest.php uses(LazilyRefreshDatabase::class)->in('Feature'); ``` ほとんどのプロジェクトでは `RefreshDatabase` が安全な選択肢です。`LazilyRefreshDatabase` はテストスイートが大きくなりデータベース操作のないテストが多い場合に検討しましょう。 | 観点 | `RefreshDatabase` | `LazilyRefreshDatabase` | | - | - | - | | マイグレーション実行タイミング | テストスイート開始時 | 最初のDB操作時 | | DBに触れないテストのオーバーヘッド | あり | なし | | 推奨場面 | 一般的なケース | テスト数が多くDBを使わないテストが多い場合 | ## コードカバレッジの計測 XdebugまたはPCOVが必要です。 ```bash theme={null} # カバレッジをターミナルに表示 php artisan test --coverage # 最低カバレッジを強制(下回るとCIが失敗) php artisan test --coverage --min=80 # HTMLレポートを出力 vendor/bin/pest --coverage-html=coverage/ # 低速なテストを確認 php artisan test --profile ``` コードカバレッジの計測はテスト実行時間を大幅に増加させます。CI環境では専用ジョブに分けるか、プルリクエスト時のみ実行するように設定することをおすすめします。 ## 関連ページ Laravelでのテストの基本的な書き方と `php artisan test` の使い方を確認します。 # Laravel AI SDK Source: https://kawax.biz/jp/ai-sdk OpenAI・Anthropic・Geminiなど複数のAIプロバイダーを統一インターフェースで扱えるLaravel AI SDKの完全ガイド。エージェント、画像生成、音声、埋め込み、テストまで網羅します。 ## はじめに [Laravel AI SDK](https://github.com/laravel/ai) は、OpenAI・Anthropic・Gemini などの AI プロバイダーと対話するための統一された表現力豊かな API を提供します。AI SDK を使うと、ツールや構造化出力を備えたインテリジェントなエージェントの構築、画像生成、音声合成・文字起こし、ベクター埋め込みの作成など、多彩な AI 機能を一貫した Laravel らしいインターフェースで実現できます。 Laravel AI SDK は Laravel 13 で追加された公式パッケージです。`laravel/ai` として提供されており、複数の AI プロバイダーを統一 API で扱えます。 ## プロバイダーサポート一覧 | 機能 | 対応プロバイダー | | - | - | | テキスト生成 | OpenAI, OpenAI Compatible, Anthropic, Gemini, Azure, Bedrock, Groq, xAI, DeepSeek, Mistral, Ollama, OpenRouter | | 画像生成 | OpenAI, Gemini, xAI, Azure, Bedrock, OpenRouter | | 音声合成(TTS) | OpenAI, ElevenLabs, Gemini, Mistral | | 音声認識(STT) | OpenAI, OpenAI Compatible, ElevenLabs, Groq, Mistral, Gemini | | 埋め込み | OpenAI, OpenAI Compatible, Gemini, Azure, Bedrock, Cohere, Mistral, Jina, VoyageAI, Ollama, OpenRouter | | リランキング | Cohere, Jina, VoyageAI, Bedrock | | ファイル | OpenAI, Anthropic, Gemini, Azure | ## インストール Composer で Laravel AI SDK をインストールします。 ```shell theme={null} composer require laravel/ai ``` `vendor:publish` Artisan コマンドで設定ファイルとマイグレーションを公開します。 ```shell theme={null} php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider" ``` データベースマイグレーションを実行します。`agent_conversations` と `agent_conversation_messages` テーブルが作成され、会話履歴の保存に使われます。 ```shell theme={null} php artisan migrate ``` ## 設定 ### 環境変数 使用する AI プロバイダーの API キーを `.env` ファイルに設定します。 ```ini theme={null} ANTHROPIC_API_KEY= AZURE_OPENAI_API_KEY= COHERE_API_KEY= DEEPSEEK_API_KEY= ELEVENLABS_API_KEY= GEMINI_API_KEY= GROQ_API_KEY= MISTRAL_API_KEY= OLLAMA_API_KEY= OPENAI_API_KEY= OPENROUTER_API_KEY= JINA_API_KEY= VOYAGEAI_API_KEY= XAI_API_KEY= ``` テキスト・画像・音声・文字起こし・埋め込みに使うデフォルトモデルは `config/ai.php` でも設定できます。 ### カスタムベース URL プロキシサービスを経由させる場合は、プロバイダーごとにカスタム URL を設定できます。 ```php theme={null} 'providers' => [ 'openai' => [ 'driver' => 'openai', 'key' => env('OPENAI_API_KEY'), 'url' => env('OPENAI_URL'), ], 'anthropic' => [ 'driver' => 'anthropic', 'key' => env('ANTHROPIC_API_KEY'), 'url' => env('ANTHROPIC_BASE_URL'), ], ], ``` カスタムベース URL は OpenAI・Anthropic・Gemini・Groq・Cohere・DeepSeek・xAI・OpenRouter で利用できます。 ### OpenAI-Compatible プロバイダー LM Studio・vLLM・Together・Fireworks・ローカルゲートウェイなど、OpenAI 互換の API を使用する場合は `openai-compatible` ドライバーでプロバイダーを設定できます。`url` は必須で、`key` を指定した場合は Bearer トークンとして送信されます。 ```php theme={null} 'providers' => [ 'local' => [ 'driver' => 'openai-compatible', 'url' => env('LOCAL_AI_URL'), 'key' => env('LOCAL_AI_API_KEY'), ], ], ``` 設定後は、他のプロバイダーと同様にプロバイダー名で指定できます。 ```php theme={null} agent()->prompt('What is Laravel?', provider: 'local', model: 'local-model'); ``` デフォルトテキストモデルを設定しておくと、毎回モデルを指定せずに使えます。 ```php theme={null} 'local' => [ 'driver' => 'openai-compatible', 'url' => env('LOCAL_AI_URL'), 'key' => env('LOCAL_AI_API_KEY'), 'models' => [ 'text' => [ 'default' => env('LOCAL_AI_MODEL'), ], ], ], ``` すべての送信リクエストにカスタムHTTPヘッダーを追加するには、設定に `headers` 配列を定義します。Bearerトークン以外の認証ヘッダーや識別ヘッダーが必要なエンドポイントで利用できます。 ```php theme={null} 'local' => [ 'driver' => 'openai-compatible', 'url' => env('LOCAL_AI_URL'), 'key' => env('LOCAL_AI_API_KEY'), 'headers' => [ 'X-Tenant-Id' => env('LOCAL_AI_TENANT_ID'), ], ], ``` OpenAI-Compatible プロバイダーはテキスト生成・ストリーミング・ツール・構造化出力・画像添付・埋め込み・文字起こしに対応しています。エンドポイントで追加のリクエストボディフィールドが必要な場合は、[プロバイダーオプション](#プロバイダーオプション)を使用してください。 #### OpenAI-Compatibleの埋め込み 任意のエンドポイントには既知のモデルがないため、OpenAI-Compatibleプロバイダーで `embeddings()` を使うにはデフォルトの埋め込みモデルを設定する必要があります。`dimensions` を省略するとリクエストにパラメーターが含まれず、モデル固有の次元数が使われます。 ```php theme={null} 'local' => [ 'driver' => 'openai-compatible', 'url' => env('LOCAL_AI_URL'), 'key' => env('LOCAL_AI_API_KEY'), 'models' => [ 'embeddings' => [ 'default' => 'text-embedding-qwen3-embedding-0.6b', 'dimensions' => 1024, // 任意 ], ], ], ``` #### OpenAI-Compatibleの文字起こし OpenAI-Compatibleプロバイダーで `Transcription` を使うには、デフォルトの文字起こしモデルを設定します。音声は標準的な multipart リクエストとして、エンドポイントの `/audio/transcriptions` へアップロードされます。 ```php theme={null} 'local' => [ 'driver' => 'openai-compatible', 'url' => env('LOCAL_AI_URL'), 'key' => env('LOCAL_AI_API_KEY'), 'models' => [ 'transcription' => [ 'default' => 'whisper-1', ], ], ], ``` OpenAI-Compatible と Groq プロバイダーは話者分離に対応していません。これらのプロバイダーで `diarize` メソッドを呼び出すと例外がスローされます。 ### オンデマンドプロバイダー マルチテナントアプリでテナントごとのAPIキーをDBに保存している場合など、設定ファイルに定義していない認証情報を使いたいことがあります。`Ai::build` メソッドに `config/ai.php` のプロバイダー定義と同じ構造の配列を渡すと、その場でプロバイダーを作成できます。 ```php theme={null} use App\Ai\Agents\SalesCoach; use Laravel\Ai\Ai; $response = (new SalesCoach)->prompt('Analyze this sales transcript...', provider: [ Ai::build([ 'driver' => 'anthropic', 'key' => $user->anthropic_key, ]), ]); ``` オンデマンドプロバイダーは、[フェイルオーバー](#フェイルオーバー)のリストや、画像・音声・文字起こし・埋め込みの生成を含め、プロバイダーを受け取る場所ならどこでも使えます。モデルを指定するには、設定に `models` 配列を含めます。 ```php theme={null} $response = (new SalesCoach)->prompt('Analyze this sales transcript...', provider: [ Ai::build(['driver' => 'anthropic', 'key' => $user->anthropic_key]), Ai::build([ 'driver' => 'openai', 'key' => $user->openai_key, 'models' => ['text' => ['default' => 'gpt-6']], ]), ]); ``` エージェントが常にオンデマンドプロバイダーを使う場合は、エージェントの `provider` メソッドからプロバイダーを返せます。エージェントが使われるたびにプロバイダーが再構築されるため、キュー実行するエージェントでも問題なく動作します。 ```php theme={null} use Laravel\Ai\Ai; use Laravel\Ai\Providers\Provider; public function provider(): Provider { return Ai::build($this->user->aiConfiguration()); } ``` オンデマンドプロバイダーは、上の例のように配列で渡してください。設定配列に `name` を指定する場合、その名前を組み込みプロバイダーや `config/ai.php` で定義したプロバイダーと重複させることはできません。 ### Lab enum コード内でプロバイダーを参照するには `Lab` enum を使います。 ```php theme={null} use Laravel\Ai\Enums\Lab; Lab::Anthropic; Lab::OpenAI; Lab::Gemini; ``` *** ## エージェント エージェントは Laravel AI SDK の基本的な構成要素です。`make:agent` コマンドでエージェントクラスを生成できます。 ```shell theme={null} php artisan make:agent SalesCoach # 構造化出力付きエージェントの生成 php artisan make:agent SalesCoach --structured ``` 生成されたエージェントは `app/Ai/Agents/` ディレクトリに配置されます。以下は主要なインターフェースをすべて実装したエージェントの例です。 ```php theme={null} user->id) ->latest() ->limit(50) ->get() ->reverse() ->map(function ($message) { return new Message($message->role, $message->content); })->all(); } public function tools(): iterable { return [new RetrievePreviousTranscripts]; } public function schema(JsonSchema $schema): array { return [ 'feedback' => $schema->string()->required(), 'score' => $schema->integer()->min(1)->max(10)->required(), ]; } } ``` ```mermaid theme={null} flowchart TD A["ユーザーのプロンプト"] --> B["Agent::prompt()"] B --> C["オプション付与"] C -->|"Conversational の場合"| D["会話履歴を付与"] C -->|"HasTools の場合"| E["ツール一覧を付与"] C -->|"HasStructuredOutput の場合"| F["JSONスキーマを付与"] D --> G["AI プロバイダーへ送信"] E --> G F --> G B --> G G --> H["レスポンス"] H -->|"ツール呼び出し"| I["ツール実行"] I --> G H -->|"完了"| J["AgentResponse"] ``` ### プロンプト `prompt()` メソッドでエージェントにメッセージを送ります。 ```php theme={null} $response = (new SalesCoach)->prompt('Analyze this sales transcript...'); return (string) $response; ``` `make()` 静的メソッドを使うと、コンテナから依存関係を解決してインスタンスを生成できます。 ```php theme={null} $agent = SalesCoach::make(user: $user); ``` プロバイダー・モデル・タイムアウトは `prompt()` の引数で上書きできます。 ```php theme={null} $response = (new SalesCoach)->prompt( 'Analyze this sales transcript...', provider: Lab::Anthropic, model: 'claude-haiku-4-5-20251001', timeout: 120, ); ``` #### 生のHTTPレスポンス テキスト生成エージェントのレスポンスには、プロバイダーAPI呼び出しの生のHTTPレスポンスを返す `raw` プロパティがあります。レート制限ヘッダーやリクエストIDなど、AI SDKの共通レスポンスに含まれないプロバイダー固有の情報を取得できます。 ```php theme={null} $response = (new SalesCoach)->prompt('Analyze this sales transcript...'); $response->raw; // Illuminate\Http\Client\Response|null $response->raw->header('X-RateLimit-Remaining-Requests'); $response->raw->json('id'); ``` ツール呼び出しループでは、各ステップがそれぞれのリクエストの生レスポンスを保持します。 ```php theme={null} foreach ($response->steps as $step) { $step->raw?->header('X-RateLimit-Remaining-Requests'); } ``` ストリーミング、Bedrock(AWS SDK経由でAPIを呼び出すため)、または明示的な `withRawResponse` を指定していないFakeレスポンスでは `raw` は `null` です。 ### 会話コンテキスト `Conversational` インターフェースを実装して `messages()` メソッドを定義すると、過去の会話履歴を AI に渡せます。 `RemembersConversations` トレイトを使うと、会話履歴をデータベースに自動保存・取得できます。 ```php theme={null} use Laravel\Ai\Concerns\RemembersConversations; class SalesCoach implements Agent, Conversational { use Promptable, RemembersConversations; public function instructions(): string { return 'You are a sales coach...'; } } ``` `forUser()` で会話を開始し、返ってきた `conversationId` を使って `continue()` で続きの会話ができます。 ```php theme={null} $response = (new SalesCoach)->forUser($user)->prompt('Hello!'); $conversationId = $response->conversationId; $response = (new SalesCoach)->continue($conversationId, as: $user)->prompt('Tell me more about that.'); ``` ### 構造化出力 `HasStructuredOutput` インターフェースを実装し、`schema()` メソッドで JSON スキーマを定義すると、AI のレスポンスを構造化されたデータとして受け取れます。 ```php theme={null} public function schema(JsonSchema $schema): array { return ['score' => $schema->integer()->required()]; } $response = (new SalesCoach)->prompt('Analyze this...'); return $response['score']; ``` #### ネストされたオブジェクト ```php theme={null} public function schema(JsonSchema $schema): array { return [ 'score' => $schema->integer()->required(), 'metadata' => $schema->object(fn ($schema) => [ 'confidence' => $schema->string()->enum(['low', 'medium', 'high'])->required(), 'language' => $schema->string()->required(), ])->required(), ]; } ``` #### オブジェクトの配列 ```php theme={null} public function schema(JsonSchema $schema): array { return [ 'feedback' => $schema->array()->items( $schema->object(fn ($schema) => [ 'comment' => $schema->string()->required(), 'score' => $schema->integer()->required(), ]) )->required(), ]; } ``` #### anyOf(複数スキーマの選択) 値が複数のスキーマのいずれかにマッチする場合は、`anyOf` メソッドを使います。 ```php theme={null} public function schema(JsonSchema $schema): array { return [ 'content' => $schema->anyOf([ $schema->object(fn ($schema) => [ 'type' => $schema->string()->enum(['article'])->required(), 'title' => $schema->string()->required(), ]), $schema->object(fn ($schema) => [ 'type' => $schema->string()->enum(['image'])->required(), 'url' => $schema->string()->required(), ]), ])->required(), ]; } ``` ### 添付ファイル `attachments` 引数でドキュメントや画像をエージェントに渡せます。 ```php theme={null} use Laravel\Ai\Files; $response = (new SalesCoach)->prompt( 'Analyze the attached sales transcript...', attachments: [ Files\Document::fromStorage('transcript.pdf'), Files\Document::fromPath('/home/laravel/transcript.md'), $request->file('transcript'), ] ); ``` 画像の添付も同様に行えます。 ```php theme={null} $response = (new ImageAnalyzer)->prompt('What is in this image?', attachments: [ Files\Image::fromStorage('photo.jpg'), Files\Image::fromPath('/home/laravel/photo.jpg'), $request->file('photo'), ]); ``` ### ストリーミング `stream()` メソッドを使うと、レスポンスをチャンク単位で返せます。長いレスポンスをリアルタイムにフロントエンドへ送るのに適しています。 ```php theme={null} Route::get('/coach', function () { return (new SalesCoach)->stream('Analyze this sales transcript...'); }); ``` `then()` コールバックで、ストリーミング完了後の処理を記述できます。 ```php theme={null} use Laravel\Ai\Responses\StreamedAgentResponse; Route::get('/coach', function () { return (new SalesCoach) ->stream('Analyze this sales transcript...') ->then(function (StreamedAgentResponse $response) { // $response->text, $response->events, $response->usage... }); }); ``` ストリームを手動でイテレートすることもできます。 ```php theme={null} $stream = (new SalesCoach)->stream('Analyze this sales transcript...'); foreach ($stream as $event) { // ... } ``` #### Vercel AI SDK プロトコル フロントエンドで Vercel AI SDK を使う場合は `usingVercelDataProtocol()` を呼びます。 ```php theme={null} Route::get('/coach', function () { return (new SalesCoach)->stream('Analyze...')->usingVercelDataProtocol(); }); ``` ### ブロードキャスト ストリームのイベントを Laravel Echo などのブロードキャストチャンネルに送信できます。 ```php theme={null} use Illuminate\Broadcasting\Channel; $stream = (new SalesCoach)->stream('Analyze this sales transcript...'); foreach ($stream as $event) { $event->broadcast(new Channel('channel-name')); } ``` `broadcastOnQueue()` を使うと、キューを経由してブロードキャストできます。 ```php theme={null} (new SalesCoach)->broadcastOnQueue( 'Analyze this sales transcript...', new Channel('channel-name'), ); ``` #### 巨大なイベントのスキップ ブロードキャストプラットフォームによっては WebSocket メッセージを約 10KB に制限しているものがあります。大きなツール結果など、データ量の多いストリームイベントがこの上限を超えてブロードキャストに失敗することがあります。`WithoutBroadcasting` Attribute を使用して、特定のイベントタイプをブロードキャストから除外できます。 ```php theme={null} queue($request->input('transcript')) ->then(function (AgentResponse $response) { /* ... */ }) ->catch(function (Throwable $e) { /* ... */ }); return back(); }); ``` ### ツール ツールを使うと、AI がコード内の関数を呼び出せるようになります。`make:tool` コマンドでツールクラスを生成できます。 ```shell theme={null} php artisan make:tool RandomNumberGenerator ``` ```php theme={null} $schema->integer()->min(0)->required(), 'max' => $schema->integer()->required(), ]; } } ``` エージェントの `tools()` メソッドでツールを登録します。 ```php theme={null} public function tools(): iterable { return [new RandomNumberGenerator]; } ``` #### ツール引数のバリデーション ツールの `schema` メソッドで受け取る引数の型は制約できますが、モデルが渡す値そのものが正しいとは限りません。リクエストの `validate` メソッドを使ってツールの引数を検証できます。 ```php theme={null} public function handle(Request $request): Stringable|string { $validated = $request->validate([ 'city' => 'required|string', 'days' => 'required|integer|max:7', ]); return $this->forecast($validated['city'], $validated['days']); } ``` バリデーションが失敗すると、エラーメッセージがツールの結果としてモデルに返されるため、モデルは引数を修正して再度ツールを呼び出せます。 #### 類似検索ツール ベクター埋め込みを使った類似検索ツールを簡単に追加できます。 ```php theme={null} use App\Models\Document; use Laravel\Ai\Tools\SimilaritySearch; public function tools(): iterable { return [ SimilaritySearch::usingModel(Document::class, 'embedding'), ]; } ``` オプションを指定することもできます。 ```php theme={null} SimilaritySearch::usingModel( model: Document::class, column: 'embedding', minSimilarity: 0.7, limit: 10, query: fn ($query) => $query->where('published', true), ), ``` クロージャで独自の検索ロジックを定義することもできます。 ```php theme={null} new SimilaritySearch(using: function (string $query) { return Document::query() ->where('user_id', $this->user->id) ->whereVectorSimilarTo('embedding', $query) ->limit(10) ->get(); }), ``` `withDescription()` でツールの説明をカスタマイズできます。 ```php theme={null} SimilaritySearch::usingModel(Document::class, 'embedding') ->withDescription('Search the knowledge base for relevant articles.'), ``` ### ファイルストレージツール `FileStorage` ツールファクトリーを使うと、エージェントに Laravel [ファイルシステムディスク](/jp/filesystem)へのアクセスを与えられます。`all` メソッドは、指定ディスク上のファイルを一覧・読み取り・URL 生成・書き込み・削除・コピーするツール一式を返します。 ```php theme={null} use Laravel\Ai\Tools\FileStorage; public function tools(): iterable { return FileStorage::all('local'); } ``` 読み取り専用のアクセスだけを許可する場合は `readOnly` メソッドを使います。 ```php theme={null} return FileStorage::readOnly('local'); ``` これらのメソッドは `Illuminate\Support\Collection` を返すため、提供するツールをさらに絞り込めます。 ```php theme={null} use Laravel\Ai\Tools\Filesystem\DeleteFile; return FileStorage::all('s3') ->reject(fn ($tool) => $tool instanceof DeleteFile); ``` ### MCP ツール アプリケーションで [Laravel MCP](/jp/mcp) を使っている場合、[Model Context Protocol](https://modelcontextprotocol.io) サーバーが公開するツールをエージェントに提供できます。[Laravel MCP クライアント](/jp/mcp#mcp-クライアント)を使って、リモートまたはローカルの MCP サーバーに接続し、そのツールをエージェントに直接渡すことができます。 MCP ツールを使用するには、アプリケーションに [Laravel MCP](/jp/mcp) パッケージがインストールされている必要があります。 MCP クライアントの `tools` メソッドはコレクションを返すため、`...` スプレッド演算子を使ってエージェントの `tools` 配列に展開します。 ```php theme={null} use App\Ai\Tools\RandomNumberGenerator; use Laravel\Mcp\Client; /** * Get the tools available to the agent. * * @return Tool[] */ public function tools(): iterable { return [ ...Client::web('https://mcp.example.com') ->withToken($token) ->tools(), new RandomNumberGenerator, ]; } ``` AI SDK は各 MCP ツールを自動的にラップし、エージェントが他のツールと同様に呼び出せるようにします。[名前付き MCP クライアント](/jp/mcp#名前付きクライアント) を使うこともできます。 ```php theme={null} use Laravel\Mcp\Facades\Mcp; public function tools(): iterable { return [ ...Mcp::client('github')->tools(), ]; } ``` または、[ローカル MCP サーバー](/jp/mcp#ローカルサーバー) に接続することも可能です。 ```php theme={null} use Laravel\Mcp\Client; public function tools(): iterable { return [ ...Client::local('php', ['artisan', 'mcp:start'])->tools(), ]; } ``` MCP クライアントの作成と認証(Bearer トークンや OAuth など)については、[MCP クライアントのドキュメント](/jp/mcp#mcp-クライアント) を参照してください。 ### プロバイダーツール AI プロバイダーがネイティブで実装している特別なツールです。 #### Web 検索 ウェブ検索をエージェントに追加します。Anthropic・OpenAI・Azure・Gemini・xAI・OpenRouter に対応しています。 ```php theme={null} use Laravel\Ai\Providers\Tools\WebSearch; public function tools(): iterable { return [new WebSearch]; } ``` オプションで検索件数やドメインの絞り込み、位置情報の指定ができます。 ```php theme={null} (new WebSearch)->max(5)->allow(['laravel.com', 'php.net']), (new WebSearch)->location(city: 'New York', region: 'NY', country: 'US'); ``` #### Web フェッチ 指定した URL のコンテンツを取得するツールです。Anthropic・Gemini・OpenRouter に対応しています。 ```php theme={null} use Laravel\Ai\Providers\Tools\WebFetch; public function tools(): iterable { return [new WebFetch]; } (new WebFetch)->max(3)->allow(['docs.laravel.com']), ``` #### ファイル検索 ベクターストアからドキュメントを検索するツールです。OpenAI・Gemini・xAI に対応しています。 ```php theme={null} use Laravel\Ai\Providers\Tools\FileSearch; public function tools(): iterable { return [new FileSearch(stores: ['store_id'])]; } // 複数のストアを指定 new FileSearch(stores: ['store_1', 'store_2']); // メタデータでフィルタリング new FileSearch(stores: ['store_id'], where: ['author' => 'Taylor Otwell', 'year' => 2026]); ``` `FileSearchQuery` を使った複雑なフィルターも指定できます。 ```php theme={null} use Laravel\Ai\Providers\Tools\FileSearchQuery; new FileSearch(stores: ['store_id'], where: fn (FileSearchQuery $query) => $query->where('author', 'Taylor Otwell') ->whereNot('status', 'draft') ->whereIn('category', ['news', 'updates']) ); ``` ### 遅延ツール読み込み エージェントが公開するすべてのツールは、デフォルトではリクエストごとにプロバイダーへ送信されます。ツールが多いエージェントではトークンを消費し、モデルのツール選択精度が下がることがあります。OpenAI または Anthropic では `ToolSearch` プロバイダーツールを使い、必要なときだけツール定義を読み込むよう遅延できます。 ```php theme={null} use App\Ai\Tools\RefundOrder; use App\Ai\Tools\SearchInvoices; use App\Ai\Tools\Weather; use Laravel\Ai\Providers\Tools\ToolSearch; public function tools(): iterable { return [ new Weather, new ToolSearch(tools: [ new SearchInvoices, new RefundOrder, ]), ]; } ``` ラップしたツール自体を変更する必要はありません。プロバイダーがプロンプトに関連するツールを検索して読み込み、通常のツールと同じように呼び出します。 Anthropic では `strategy` 引数で検索方法を指定できます。利用できる戦略はデフォルトの `regex` と `bm25` です。 ```php theme={null} new ToolSearch(tools: [new SearchInvoices], strategy: 'bm25'), ``` Anthropic 固有のオプションは `withProviderOptions` メソッドで渡せます。 ```php theme={null} (new ToolSearch(tools: [new SearchInvoices])) ->withProviderOptions(['cache_control' => ['type' => 'ephemeral']]), ``` ツール検索に対応していないプロバイダーは、遅延ツールを黙って破棄せず例外をスローします。Anthropic では `ToolSearch` の外側にも少なくとも1つのツールが必要です。 ### ツール呼び出しの修復 モデルが存在しないローカルツールを呼び出したときに、エージェントがその呼び出しを修復できるようにするには `RepairToolCalls` アトリビュートを使います。Laravelは失敗した呼び出しと利用可能なローカルツールの名前をモデルへ返すため、モデルは呼び出しを修正できます。 ```php theme={null} use Laravel\Ai\Attributes\RepairToolCalls; use Laravel\Ai\Contracts\Agent; use Laravel\Ai\Contracts\HasTools; use Laravel\Ai\Promptable; #[RepairToolCalls] class SupportAgent implements Agent, HasTools { use Promptable; // ... } ``` Laravelが最大ステップ数を自動的に決定する場合、このアトリビュートによる修復呼び出しのために1ステップが追加されます。`MaxSteps` で明示的に上限を指定した場合、その上限は変わりません。 ### サブエージェント エージェントは別のエージェントの `tools()` メソッドから返すこともできます。エージェントをツールとして登録すると、親エージェントが特定のタスクをサブエージェントに委譲し、その結果を元の応答に組み込めます。汎用エージェントが専門的な指示・ツール・モデル設定・プロバイダー設定を持つ特化型エージェントにアクセスするときに便利です。 たとえば、カスタマーサポートエージェントが返金ポリシーの質問を返金専門エージェントに委譲する例です。 ```php theme={null} $prompt->prompt]); return $next($prompt); } } ``` `then()` を使うと、レスポンス後の処理も追加できます。 ```php theme={null} public function handle(AgentPrompt $prompt, Closure $next) { return $next($prompt)->then(function (AgentResponse $response) { Log::info('Agent responded', ['text' => $response->text]); }); } ``` ### 匿名エージェント クラスを定義せずに `agent()` ヘルパーで匿名エージェントを使えます。 ```php theme={null} use function Laravel\Ai\{agent}; $response = agent( instructions: 'You are an expert at software development.', messages: [], tools: [], )->prompt('Tell me about Laravel'); ``` 構造化出力付きの匿名エージェントも作成できます。 ```php theme={null} use Illuminate\Contracts\JsonSchema\JsonSchema; $response = agent( schema: fn (JsonSchema $schema) => ['number' => $schema->integer()->required()], )->prompt('Generate a random number less than 100'); ``` ### エージェント設定(PHP Attributes) PHP Attribute を使ってエージェントのデフォルト設定を宣言的に記述できます。 | Attribute | 説明 | | - | - | | `#[Provider]` | 使用するプロバイダーを指定 | | `#[Model]` | 使用するモデルを指定 | | `#[MaxSteps]` | ツール呼び出しの最大ステップ数 | | `#[MaxTokens]` | 最大トークン数 | | `#[Temperature]` | 温度パラメーター(0.0〜1.0) | | `#[TopP]` | 核サンプリングの確率(0.0〜1.0) | | `#[Timeout]` | タイムアウト(秒) | | `#[UseCheapestModel]` | 最安値モデルを自動選択 | | `#[UseSmartestModel]` | 最高性能モデルを自動選択 | ```php theme={null} [ 'reasoning' => ['effort' => 'low'], 'frequency_penalty' => 0.5, 'presence_penalty' => 0.3, ], Lab::Anthropic => [ 'thinking' => ['budget_tokens' => 1024], ], default => [], }; } } ``` `providerOptions` メソッドには現在使用中のプロバイダー(`Lab` enumまたは文字列)が渡されるため、プロバイダーごとに異なるオプションを返せます。各フォールバックプロバイダーに個別の設定を渡せるため、[フェイルオーバー](#フェイルオーバー)使用時に特に便利です。 上記の Anthropic の例では `cache_control` によって[プロンプトキャッシュ](#プロンプトキャッシュ)も有効化しています。 ### プロンプトキャッシュ 多くのプロバイダーは繰り返されるプロンプトの接頭辞を自動的にキャッシュし、キャッシュされた部分を割引価格で課金します。OpenAI、Gemini、Groq、DeepSeek、xAI は設定不要で、レスポンスの usage から節約分を確認できます。 ```php theme={null} $response->usage->cacheReadInputTokens; $response->usage->cacheWriteInputTokens; ``` `anthropic` と `bedrock` プロバイダーは明示的に指示した場合のみキャッシュします。`CacheInstructions` と `CacheToolDefinitions` 属性を使うと、エージェントの指示とツール定義の末尾にキャッシュのブレークポイントを配置できます。これにより、すべての会話がその接頭辞をキャッシュから読み込むようになり、毎回書き込む必要がなくなります。 ```php theme={null} use Laravel\Ai\Attributes\CacheInstructions; use Laravel\Ai\Attributes\CacheToolDefinitions; #[CacheInstructions] #[CacheToolDefinitions] class SalesCoach implements Agent { use Promptable; // ... } ``` 現在の日付を埋め込むなど、リクエストごとに指示の内容が変わる場合は `CacheToolDefinitions` のみを使用してください。リクエストごとに変わる接頭辞をキャッシュすると、毎回新しいキャッシュエントリが作られてしまい、再利用されないまま書き込みコストだけがかかります。 これらの属性をサポートしないプロバイダーは単に無視するため、[フェイルオーバー](#フェイルオーバー)使用時でも安全に宣言できます。 キャッシュされた接頭辞はデフォルトで5分間保持されます。Anthropic では属性にTTLを渡すことで1時間まで保持できます。 ```php theme={null} #[CacheInstructions('1h')] #[CacheToolDefinitions('1h')] ``` また、トップレベルの `cache_control` [プロバイダーオプション](#プロバイダーオプション)によって Anthropic の自動キャッシュを有効化することもできます。これはリクエストの最後のブロックの後に単一のブレークポイントを配置する方式で、会話が進むにつれてブレークポイントも進み、各ターンが前のターンをキャッシュから読み込みます。両方の仕組みを組み合わせることも可能です。 プロバイダーはツール、指示、メッセージの順にプロンプトを構築するため、指示を1時間キャッシュする場合はツール定義も1時間キャッシュする必要があります。この2つを混在させると `InvalidArgumentException` がスローされます。 *** ## 人間による承認(Human Tool Approval) ツール承認を利用するには、会話履歴が永続化される`Conversational`エージェントが必要です。一時停止した呼び出しを再開するために、`RemembersConversations`トレイトが必要な永続化機能を提供します。 ファイルの削除や送金など、機密性の高い操作や取り消せない操作を行うツールには、実行前に人間の承認を要求できます。ツールを承認対象にするには、`Approvable`コントラクトを実装し`InteractsWithApprovals`トレイトを使用します。承認対象のツールはデフォルトで承認が必須になります。 ```php theme={null} $schema->string()->required(), ]; } } ``` ツール呼び出しの引数に応じて承認の要否を判定したい場合は、ツールに`needsApproval`メソッドを定義します。このメソッドは真偽値か、承認理由を含む`Approval`インスタンスを返せます。 ```php theme={null} use Laravel\Ai\Approvals\Approval; /** * 与えられたリクエストに対してツールが承認を必要とするか判定 */ protected function needsApproval(Request $request): Approval|bool { return str_starts_with($request['path'], 'temporary/') ? false : Approval::required('このファイルは完全に削除されます。'); } ``` エージェントの`tools`メソッドからツールを返す際に、承認要件を上書きすることもできます。 ```php theme={null} public function tools(): iterable { return [ (new SendNotification)->withoutApproval(), (new DeleteFile)->requireApproval('削除には確認が必要です。'), ]; } ``` 承認対象のツールが呼び出されると、エージェントは実行前に一時停止します。レスポンスの`pendingApprovals`を調べることで、各ツール呼び出しのID・ツール名・引数・承認理由を確認できます。 ```php theme={null} $response = (new FileAssistant) ->forUser($user) ->prompt('古い請求書を削除して。'); if ($response->hasPendingApprovals()) { foreach ($response->pendingApprovals as $approval) { // $approval->id // $approval->tool // $approval->arguments // $approval->reason } } ``` エージェントを再開するには、会話を継続し、保留中の各ツール呼び出しに対する決定を含む`Decisions`インスタンスを渡します。決定では呼び出しの承認・却下・実行前の引数の編集が行えます。 ```php theme={null} use Laravel\Ai\Approvals\Decision; use Laravel\Ai\Approvals\Decisions; $response = (new FileAssistant) ->continue($conversationId, as: $user) ->prompt(Decisions::from([ 'call_abc' => Decision::approve(), 'call_ghi' => Decision::reject('この請求書は保管が必要です。'), ])); ``` 真偽値の`true`・`false`は、それぞれ承認・却下の省略記法として使えます。保留中のすべてのツール呼び出しには決定が必要です。不明・欠落・すでに解決済みのツール呼び出しIDを指定すると`ApprovalMismatchException`がスローされます。明示的な決定がない呼び出しに対しては、`approveRemaining`または`rejectRemaining`メソッドでデフォルトの決定を指定できます。 ```php theme={null} $decisions = Decisions::from([ 'call_abc' => true, ])->rejectRemaining('承認されませんでした。'); $response = (new FileAssistant) ->continue($conversationId, as: $user) ->prompt($decisions); ``` `Decision::reject('承認されませんでした。')`のように結果付きで却下すると、モデルに返されて応答が継続されます。結果なしで却下すると、却下が記録された時点で生成ループが停止します。 ツール承認は`prompt`・`stream`・`queue`・`broadcast`・`broadcastNow`・`broadcastOnQueue`メソッドでサポートされています。 ストリーミングおよびブロードキャスト中は、一時停止が`tool_approval_request`イベントとして表現されます。[Vercel AI SDK ストリームプロトコル](#vercel-ai-sdk-プロトコル)を使用している場合、承認リクエストと結果はプロトコルのネイティブなツール承認パートとして送出されます。 キューイングされたエージェントの場合、結果のレスポンスは`then`コールバックに渡され、Laravel は`ToolApprovalRequested`イベントもディスパッチします。 Laravel は、モデルに続行を求める前に、承認済みツールの実行結果を保存します。その後に生成が失敗した場合、承認はすでに解決済みです。同じ承認決定を再送するのではなく、通常のテキストプロンプトで会話を継続してください。 ### 完全な承認フロー 以下のルートは、完全な承認フローを示します。`GET`ルートはチャット画面を返し、`POST`ルートはチャット画面からの新しいテキストプロンプトまたは承認決定のいずれかを受け取ります。この例では、アプリケーションの`User`モデルが`HasConversations`トレイトを使用していることを前提としています。 ```php theme={null} use App\Ai\Agents\FileAssistant; use Illuminate\Http\Request; use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Route; use Illuminate\Validation\Rule; use Laravel\Ai\Approvals\Decision; use Laravel\Ai\Approvals\Decisions; use Laravel\Ai\Models\Conversation; Route::get('/chat/{conversation}', function (Request $request, Conversation $conversation) { Gate::authorize('view', $conversation); return view('chat', [ 'conversation' => $conversation, ]); })->middleware('auth'); Route::post('/chat/{conversation}', function (Request $request, Conversation $conversation) { Gate::authorize('view', $conversation); $validated = $request->validate([ 'message' => ['nullable', 'string', 'required_without:decisions', 'prohibits:decisions'], 'decisions' => ['nullable', 'array', 'required_without:message', 'prohibits:message'], 'decisions.*.action' => ['required_with:decisions', Rule::in(['approve', 'reject'])], 'decisions.*.result' => ['nullable', 'string'], ]); $prompt = isset($validated['decisions']) ? Decisions::from(collect($validated['decisions'])->map( fn (array $decision) => match ($decision['action']) { 'approve' => Decision::approve(), 'reject' => Decision::reject($decision['result'] ?? null), } )->all()) : $validated['message']; $response = (new FileAssistant) ->continue($conversation->id, as: $request->user()) ->prompt($prompt); return [ 'conversation_id' => $response->conversationId, 'status' => $response->hasPendingApprovals() ? 'awaiting_approval' : 'complete', 'message' => $response->text, 'approvals' => $response->pendingApprovals, ]; })->middleware('auth'); ``` レスポンスのステータスが`awaiting_approval`の場合、チャット画面は保留中の承認を表示し、ツール呼び出しIDをキーとしてユーザーの選択を同じエンドポイントに送信する必要があります。 ```json theme={null} { "decisions": { "call_abc": { "action": "approve" }, "call_def": { "action": "reject", "result": "この請求書は保管が必要です。" } } } ``` 通常のチャットメッセージの場合は、代わりに`message`の値を送信します。 ```json theme={null} { "message": "古い請求書を削除して。" } ``` 承認フローはAIエージェントに強力な操作権限を与えつつ、実行前に人間のチェックを挟める仕組みです。ファイル削除・決済処理・外部APIへの書き込みなど、取り消しの効かない操作を伴うツールでは積極的に活用しましょう。 *** ## 画像生成 `Image` クラスで画像を生成できます。OpenAI・Gemini・xAI プロバイダーが対応しています。 ```php theme={null} use Laravel\Ai\Image; $image = Image::of('A donut sitting on the kitchen counter')->generate(); $rawContent = (string) $image; ``` 品質やアスペクト比・タイムアウトを指定できます。 ```php theme={null} $image = Image::of('A donut sitting on the kitchen counter') ->quality('high') ->landscape() ->timeout(120) ->generate(); ``` 参照画像を添付して加工することもできます。 ```php theme={null} use Laravel\Ai\Files; $image = Image::of('Update this photo to be in the style of an impressionist painting.') ->attachments([Files\Image::fromStorage('photo.jpg')]) ->landscape() ->generate(); ``` ### 画像の保存 ```php theme={null} $path = $image->store(); $path = $image->storeAs('image.jpg'); $path = $image->storePublicly(); $path = $image->storePubliclyAs('image.jpg'); ``` ### キューで画像生成 ```php theme={null} use Laravel\Ai\Responses\ImageResponse; Image::of('A donut sitting on the kitchen counter') ->portrait() ->queue() ->then(function (ImageResponse $image) { $path = $image->store(); }); ``` *** ## 音声合成(TTS) `Audio` クラスでテキストを音声に変換できます。OpenAI・ElevenLabs プロバイダーが対応しています。 ```php theme={null} use Laravel\Ai\Audio; $audio = Audio::of('I love coding with Laravel.')->generate(); $rawContent = (string) $audio; ``` 声の性別や具体的なボイス ID、話し方の指示も指定できます。 ```php theme={null} $audio = Audio::of('I love coding with Laravel.')->female()->generate(); $audio = Audio::of('I love coding with Laravel.')->voice('voice-id-or-name')->generate(); $audio = Audio::of('I love coding with Laravel.')->female()->instructions('Said like a pirate')->generate(); ``` ### 音声の保存 ```php theme={null} $path = $audio->store(); $path = $audio->storeAs('audio.mp3'); $path = $audio->storePublicly(); $path = $audio->storePubliclyAs('audio.mp3'); ``` ### キューで音声生成 ```php theme={null} use Laravel\Ai\Responses\AudioResponse; Audio::of('I love coding with Laravel.') ->queue() ->then(function (AudioResponse $audio) { $path = $audio->store(); }); ``` *** ## 文字起こし(STT) `Transcription` クラスで音声ファイルをテキストに変換できます。OpenAI・OpenAI Compatible・ElevenLabs・Groq・Mistral・Gemini プロバイダーが対応しています。 ```php theme={null} use Laravel\Ai\Transcription; $transcript = Transcription::fromPath('/home/laravel/audio.mp3')->generate(); $transcript = Transcription::fromStorage('audio.mp3')->generate(); $transcript = Transcription::fromUpload($request->file('audio'))->generate(); return (string) $transcript; ``` ### 話者分離(ダイアリゼーション) `diarize()` を使うと、話者ごとに分離した文字起こしが得られます。 ```php theme={null} $transcript = Transcription::fromStorage('audio.mp3')->diarize()->generate(); ``` ### キューで文字起こし ```php theme={null} use Laravel\Ai\Responses\TranscriptionResponse; Transcription::fromStorage('audio.mp3') ->queue() ->then(function (TranscriptionResponse $transcript) { /* ... */ }); ``` *** ## テキスト要約(Text Summarization) Laravel の `Stringable` クラスが提供する `summarize` メソッドで、テキストを要約できます。デフォルトでは3文以内に要約され、設定されたプロバイダーの最も安価なテキストモデルが使われます。 ```php theme={null} use Illuminate\Support\Str; $summary = Str::of($article)->summarize(); ``` 要約に使う文の最大数・プロバイダー・モデル・タイムアウトを指定することもできます。`Str` クラスには静的メソッド版も用意されています。 ```php theme={null} use Laravel\Ai\Enums\Lab; $summary = Str::of($article)->summarize( sentences: 4, provider: Lab::Anthropic, model: 'claude-sonnet-5', timeout: 30, ); $summary = Str::summarize($article, sentences: 4); ``` *** ## 埋め込み(Embeddings) テキストをベクター表現に変換して、類似検索などに活用できます。 ```php theme={null} use Illuminate\Support\Str; // Stringable を使う方法 $embeddings = Str::of('Napa Valley has great wine.')->toEmbeddings(); ``` ```php theme={null} use Laravel\Ai\Embeddings; // 複数のテキストを一度に処理 $response = Embeddings::for(['Napa Valley has great wine.', 'Laravel is a PHP framework.'])->generate(); $response->embeddings; // [[0.123, 0.456, ...], [0.789, 0.012, ...]] ``` プロバイダー・モデル・次元数を指定することもできます。 ```php theme={null} $response = Embeddings::for(['Napa Valley has great wine.']) ->dimensions(1536) ->generate(Lab::OpenAI, 'text-embedding-3-small'); ``` ### マルチモーダル埋め込み(Multimodal Embeddings) `Embeddings::for` メソッドは文字列だけでなく、画像・音声・ドキュメント・動画の入力も受け付けるため、テキスト以外のコンテンツに対しても埋め込みを生成できます。Gemini は画像・音声・ドキュメント・動画の埋め込みに対応し、VoyageAI は画像・動画の埋め込みに対応しています。 ```php theme={null} use Laravel\Ai\Embeddings; use Laravel\Ai\Enums\Lab; use Laravel\Ai\Files\Image; use Laravel\Ai\Files\Video; $response = Embeddings::for([ 'A vineyard at sunset.', Image::fromStorage('vineyard.jpg'), Video::fromPath('/home/laravel/tour.mp4'), ])->generate(Lab::Gemini); ``` マルチモーダル入力には[添付ファイルで使われるものと同じファイルクラス](#添付ファイル)を使います。これらのファイルはローカルパス、ファイルシステムディスク、リモート URL、Base64 エンコードされたコンテンツから作成できます。画像・ドキュメント・動画はアップロードされたファイルからも作成でき、ドキュメントは生の文字列コンテンツからも作成できます。 ```php theme={null} use Laravel\Ai\Files\Audio; use Laravel\Ai\Files\Document; use Laravel\Ai\Files\Image; use Laravel\Ai\Files\Video; Image::fromPath('/home/laravel/photo.jpg'); Image::fromStorage('photo.jpg'); Image::fromUpload($request->file('photo')); Audio::fromPath('/home/laravel/clip.mp3'); Audio::fromStorage('clip.mp3'); Audio::fromUpload($request->file('clip.mp3')); Video::fromPath('/home/laravel/video.mp4'); Video::fromStorage('video.mp4'); Video::fromUpload($request->file('video')); Document::fromUrl('https://example.com/report.pdf'); Document::fromString('Laravel is a PHP framework.', 'text/plain'); Document::fromUpload($request->file('report')); ``` VoyageAI はリモート URL のメディアと Base64 エンコードされたメディアを同一リクエスト内で混在させることを許可していません。ローカル・ストレージ・アップロードされたファイルは Base64 エンコードされたコンテンツとして送信され、テキスト入力はどちらのメディアソースとも組み合わせられます。利用可能なマルチモーダルモデルと入力については、各プロバイダーのドキュメントを確認してください。 ### ベクター検索(pgvector) PostgreSQL と pgvector 拡張を使ったベクター検索の設定例です。 ```php theme={null} Schema::ensureVectorExtensionExists(); Schema::create('documents', function (Blueprint $table) { $table->id(); $table->string('title'); $table->text('content'); $table->vector('embedding', dimensions: 1536); $table->timestamps(); }); // HNSW インデックスを追加 $table->vector('embedding', dimensions: 1536)->index(); ``` ```php theme={null} use Illuminate\Database\Eloquent\Casts\AsVector; protected function casts(): array { return ['embedding' => AsVector::class]; } ``` ```php theme={null} // 埋め込みベクターで検索 $documents = Document::query() ->whereVectorSimilarTo('embedding', $queryEmbedding, minSimilarity: 0.4) ->limit(10) ->get(); // 文字列を渡すと自動で埋め込みを生成 $documents = Document::query() ->whereVectorSimilarTo('embedding', 'best wineries in Napa Valley') ->limit(10) ->get(); ``` 低レベルのメソッドも利用できます。 ```php theme={null} $documents = Document::query() ->select('*') ->selectVectorDistance('embedding', $queryEmbedding, as: 'distance') ->whereVectorDistanceLessThan('embedding', $queryEmbedding, maxDistance: 0.3) ->orderByVectorDistance('embedding', $queryEmbedding) ->limit(10) ->get(); ``` ### 埋め込みのキャッシュ 同じテキストの埋め込み生成を繰り返さないようにキャッシュできます。 `config/ai.php` でデフォルトのキャッシュ設定を行います。 ```php theme={null} 'caching' => [ 'embeddings' => [ 'cache' => true, 'store' => env('CACHE_STORE', 'database'), 'individually' => true, // ... ], ], ``` キャッシュが有効な場合、埋め込みは30日間キャッシュされます。キャッシュキーはプロバイダー、モデル、次元数、入力コンテンツを元に生成されるため、同一のリクエストはキャッシュされた結果を返し、異なる設定では新規に生成されます。 デフォルトでは各入力の埋め込みが個別のキーでキャッシュされるため、入力の集合や順序が変わっても、以前に見た入力に対してはキャッシュがヒットします。入力全体をひとつのキーでキャッシュしたい場合は、`ai.caching.embeddings.individually` 設定を `false` にしてください。 リクエストごとにキャッシュを制御することもできます。 ```php theme={null} $response = Embeddings::for(['Napa Valley has great wine.'])->cache()->generate(); $response = Embeddings::for(['Napa Valley has great wine.'])->cache(seconds: 3600)->generate(); // Stringable でも同様 $embeddings = Str::of('Napa Valley has great wine.')->toEmbeddings(cache: true); $embeddings = Str::of('Napa Valley has great wine.')->toEmbeddings(cache: 3600); ``` *** ## リランキング 検索結果をクエリへの関連度でリランク(並び替え)できます。Cohere・Jina プロバイダーが対応しています。 ```php theme={null} use Laravel\Ai\Reranking; $response = Reranking::of([ 'Django is a Python web framework.', 'Laravel is a PHP web application framework.', 'React is a JavaScript library for building user interfaces.', ])->rerank('PHP frameworks'); $response->first()->document; // "Laravel is a PHP web application framework." $response->first()->score; // 0.95 $response->first()->index; // 1 ``` `limit()` で返す件数を絞り込めます。 ```php theme={null} $response = Reranking::of($documents)->limit(5)->rerank('search query'); ``` ### コレクションのリランキング Eloquent コレクションを直接リランクできます。 ```php theme={null} // 単一フィールド $posts = Post::all()->rerank('body', 'Laravel tutorials'); // 複数フィールド(JSON として送信) $reranked = $posts->rerank(['title', 'body'], 'Laravel tutorials'); // クロージャで独自のテキスト生成 $reranked = $posts->rerank(fn ($post) => $post->title.': '.$post->body, 'Laravel tutorials'); // オプション付き $reranked = $posts->rerank( by: 'content', query: 'Laravel tutorials', limit: 10, provider: Lab::Cohere, ); ``` *** ## 分類(Classification) ### Yes / No の判定 単純な yes / no の判定には、Laravelの `Stringable` クラスの `decide` メソッドが使えます。完全なレスポンスではなく boolean を返します。「yes」と「no」の意味を記述したり、回答が満たすべき確率(デフォルトは `0.5`)を `threshold` で指定したりできます。 ```php theme={null} use Illuminate\Support\Str; $spam = Str::of($message)->decide('Is this spam?'); ``` ### コレクションから選ぶ 選択肢のリストから1つを選ぶには、`Collection` クラスの `decide` メソッドが使えます。質問と分類するテキストを渡すと、コレクション内から選ばれた要素を返します。 文字列やenumのコレクションはそのまま使えます。それ以外の要素は `by` 引数で名前を指定します。`describe` 引数にフィールド名・フィールド名の配列・クロージャを渡すと、各選択肢の詳細をモデルに伝えられます。 ```php theme={null} $department = collect(['billing', 'technical', 'sales']) ->decide('Which team should handle this request?', $ticket->body); $priority = collect(Priority::cases()) ->decide('How urgent is this request?', $ticket->body); $department = Department::all()->decide( 'Which department should handle this request?', $ticket->body, by: 'name', describe: 'description', ); ``` `describe` で定義した説明は、テキストがその選択肢に該当するかをモデルが判断する基準になります。名前だけでは曖昧な選択肢に有効です。クロージャを渡す場合は各要素を受け取り、説明の文字列か配列を返します。 ```php theme={null} $priority = collect(Priority::cases())->decide( 'How urgent is this request?', $ticket->body, describe: fn (Priority $priority) => $priority->description(), ); ``` キーが文字列の文字列コレクションでは、キーが選択肢、値がその説明として使われ、選ばれたキーが返されます。`threshold` を指定して、選ばれた選択肢の確率がそれを下回った場合は `null` が返ります。 ```php theme={null} $department = collect([ 'billing' => 'Payments, invoices, and refunds', 'technical' => 'Bugs, outages, and integrations', ])->decide('Which team should handle this request?', $ticket->body, threshold: 0.6) ?? 'triage'; ``` *** ## ファイル管理 AI プロバイダーにファイルをアップロードして、後から参照できます。 ```php theme={null} use Laravel\Ai\Files\Document; use Laravel\Ai\Files\Image; // パスから $response = Document::fromPath('/home/laravel/document.pdf')->put(); $response = Image::fromPath('/home/laravel/photo.jpg')->put(); // ストレージから $response = Document::fromStorage('document.pdf', disk: 'local')->put(); $response = Image::fromStorage('photo.jpg', disk: 'local')->put(); // URL から $response = Document::fromUrl('https://example.com/document.pdf')->put(); $response = Image::fromUrl('https://example.com/photo.jpg')->put(); return $response->id; ``` 文字列やフォームアップロードからも扱えます。 ```php theme={null} $stored = Document::fromString('Hello, World!', 'text/plain')->put(); $stored = Document::fromUpload($request->file('document'))->put(); ``` ### 保存済みファイルの参照 アップロード済みのファイル ID でエージェントに添付できます。 ```php theme={null} use Laravel\Ai\Files; $response = (new SalesCoach)->prompt( 'Analyze the attached sales transcript...', attachments: [Files\Document::fromId('file-id')] ); ``` ### ファイルの取得・削除 ```php theme={null} // 取得 $file = Document::fromId('file-id')->get(); $file->id; $file->mimeType(); // 削除 Document::fromId('file-id')->delete(); ``` ### プロバイダーの指定 ```php theme={null} $response = Document::fromPath('/home/laravel/document.pdf')->put(provider: Lab::Anthropic); ``` ### プロバイダー固有オプションの指定 `withProviderOptions` メソッドでプロバイダー固有のアップロードオプションを渡せます。たとえば OpenAI のファイル `purpose` を設定できます。 ```php theme={null} use Laravel\Ai\Files\Document; $response = Document::fromPath('/home/laravel/knowledge.txt') ->withProviderOptions(['purpose' => 'assistants']) ->put(); ``` プロバイダーごとに異なるオプションを指定する場合は、クロージャを渡します。 ```php theme={null} use Laravel\Ai\Enums\Lab; use Laravel\Ai\Files\Document; $response = Document::fromPath('/home/laravel/training.jsonl') ->withProviderOptions(fn (Lab|string $provider) => match ($provider) { Lab::OpenAI => ['purpose' => 'fine-tune'], default => [], }) ->put(); ``` *** ## ベクターストア ベクターストアを使うと、ドキュメントをプロバイダー側で管理できます。 ```php theme={null} use Laravel\Ai\Stores; // 作成 $store = Stores::create('Knowledge Base'); $store = Stores::create( name: 'Knowledge Base', description: 'Documentation.', expiresWhenIdleFor: days(30), ); return $store->id; // 取得 $store = Stores::get('store_id'); $store->id; $store->name; $store->fileCounts; $store->ready; // 削除 Stores::delete('store_id'); $store->delete(); ``` ### ストアへのファイル追加 ```php theme={null} $store = Stores::get('store_id'); // さまざまな方法でファイルを追加 $document = $store->add('file_id'); $document = $store->add(Document::fromId('file_id')); $document = $store->add(Document::fromPath('/path/to/document.pdf')); $document = $store->add(Document::fromStorage('manual.pdf')); $document = $store->add($request->file('document')); $document->id; $document->fileId; ``` メタデータを付与することもできます。 ```php theme={null} $store->add( Document::fromPath('/path/to/document.pdf'), metadata: [ 'author' => 'Taylor Otwell', 'department' => 'Engineering', 'year' => 2026, ] ); ``` ### ストアからのファイル削除 ```php theme={null} $store->remove('file_id'); // ファイル自体も削除する場合 $store->remove('file_abc123', deleteFile: true); ``` *** ## フェイルオーバー 複数のプロバイダーを配列で指定すると、最初のプロバイダーが失敗した場合に次のプロバイダーへ自動フォールバックします。 ```php theme={null} use App\Ai\Agents\SalesCoach; use Laravel\Ai\Image; // エージェントのフェイルオーバー $response = (new SalesCoach)->prompt( 'Analyze this sales transcript...', provider: [Lab::OpenAI, Lab::Anthropic], ); // 画像生成のフェイルオーバー $image = Image::of('A donut sitting on the kitchen counter') ->generate(provider: [Lab::Gemini, Lab::xAI]); ``` *** ## テスト Laravel AI SDK はテスト用のフェイク機能を提供しており、実際の API を呼び出さずにテストできます。 キューに入れた画像・音声・文字起こし・埋め込み生成をフェイクすると、生成に登録した `then` コールバックはフェイクレスポンスを受け取って呼び出されます。コールバックも実行したくない場合は、`Queue::fake()` も使用してください。 ### エージェントのテスト ```php theme={null} use App\Ai\Agents\SalesCoach; use Laravel\Ai\Prompts\AgentPrompt; // 固定レスポンスのフェイク SalesCoach::fake(); SalesCoach::fake(['First response', 'Second response']); // クロージャで動的レスポンス SalesCoach::fake(function (AgentPrompt $prompt) { return 'Response for: '.$prompt->prompt; }); // アサーション SalesCoach::assertPrompted('Analyze this...'); SalesCoach::assertPrompted(function (AgentPrompt $prompt) { return $prompt->contains('Analyze'); }); SalesCoach::assertPromptedTimes(3); SalesCoach::assertNotPrompted('Missing prompt'); SalesCoach::assertNeverPrompted(); ``` キューイングのアサーションも用意されています。 ```php theme={null} use Laravel\Ai\QueuedAgentPrompt; SalesCoach::assertQueued('Analyze this...'); SalesCoach::assertQueued(function (QueuedAgentPrompt $prompt) { return $prompt->contains('Analyze'); }); SalesCoach::assertNotQueued('Missing prompt'); SalesCoach::assertNeverQueued(); ``` `preventStrayPrompts()` を使うと、フェイクで定義していないプロンプトが呼ばれた場合に例外を投げます。 ```php theme={null} SalesCoach::fake()->preventStrayPrompts(); ``` 構造化出力を返すエージェントをフェイクする場合、配列でレスポンスを指定できます。エージェントは指定したデータを含む構造化レスポンスを返します。 ```php theme={null} SalesCoach::fake([ ['score' => 87], ]); ``` 構造化出力エージェントに対して `fake()` がフェイクデータを明示的に渡さずに呼ばれた場合、Laravel はエージェントの定義したスキーマに合致するフェイクデータを自動生成します。 匿名エージェントのテストには `AnonymousAgent::fake()` を使います。 ```php theme={null} use Laravel\Ai\AnonymousAgent; AnonymousAgent::fake(['Test response']); ``` ### 画像生成のテスト ```php theme={null} use Laravel\Ai\Image; use Laravel\Ai\Prompts\ImagePrompt; Image::fake(); Image::fake([base64_encode($firstImage), base64_encode($secondImage)]); Image::fake(function (ImagePrompt $prompt) { return base64_encode('...'); }); Image::assertGenerated(function (ImagePrompt $prompt) { return $prompt->contains('sunset') && $prompt->isLandscape(); }); Image::assertNotGenerated('Missing prompt'); Image::assertNothingGenerated(); Image::assertQueued(fn (QueuedImagePrompt $prompt) => $prompt->contains('sunset')); Image::assertNotQueued('Missing prompt'); Image::assertNothingQueued(); Image::fake()->preventStrayImages(); ``` ### 音声合成のテスト ```php theme={null} use Laravel\Ai\Audio; use Laravel\Ai\Prompts\AudioPrompt; Audio::fake(); Audio::fake([base64_encode($firstAudio), base64_encode($secondAudio)]); Audio::fake(function (AudioPrompt $prompt) { return base64_encode('...'); }); Audio::assertGenerated(function (AudioPrompt $prompt) { return $prompt->contains('Hello') && $prompt->isFemale(); }); Audio::assertNotGenerated('Missing prompt'); Audio::assertNothingGenerated(); Audio::assertQueued(fn (QueuedAudioPrompt $prompt) => $prompt->contains('Hello')); Audio::assertNotQueued('Missing prompt'); Audio::assertNothingQueued(); Audio::fake()->preventStrayAudio(); ``` ### 文字起こしのテスト ```php theme={null} use Laravel\Ai\Transcription; use Laravel\Ai\Prompts\TranscriptionPrompt; Transcription::fake(); Transcription::fake(['First transcription text.', 'Second transcription text.']); Transcription::fake(function (TranscriptionPrompt $prompt) { return 'Transcribed text...'; }); Transcription::assertGenerated(function (TranscriptionPrompt $prompt) { return $prompt->language === 'en' && $prompt->isDiarized(); }); Transcription::assertNotGenerated(fn (TranscriptionPrompt $prompt) => $prompt->language === 'fr'); Transcription::assertNothingGenerated(); Transcription::assertQueued(fn (QueuedTranscriptionPrompt $prompt) => $prompt->isDiarized()); Transcription::assertNotQueued(fn (QueuedTranscriptionPrompt $prompt) => $prompt->language === 'fr'); Transcription::assertNothingQueued(); Transcription::fake()->preventStrayTranscriptions(); ``` ### 埋め込みのテスト ```php theme={null} use Laravel\Ai\Embeddings; use Laravel\Ai\Prompts\EmbeddingsPrompt; Embeddings::fake(); Embeddings::fake([[$firstEmbeddingVector], [$secondEmbeddingVector]]); Embeddings::fake(function (EmbeddingsPrompt $prompt) { return array_map( fn () => Embeddings::fakeEmbedding($prompt->dimensions), $prompt->inputs ); }); Embeddings::assertGenerated(function (EmbeddingsPrompt $prompt) { return $prompt->contains('Laravel') && $prompt->dimensions === 1536; }); Embeddings::assertNotGenerated(fn (EmbeddingsPrompt $prompt) => $prompt->contains('Other')); Embeddings::assertNothingGenerated(); Embeddings::assertQueued(fn (QueuedEmbeddingsPrompt $prompt) => $prompt->contains('Laravel')); Embeddings::assertNotQueued(fn (QueuedEmbeddingsPrompt $prompt) => $prompt->contains('Other')); Embeddings::assertNothingQueued(); Embeddings::fake()->preventStrayEmbeddings(); ``` ### リランキングのテスト ```php theme={null} use Laravel\Ai\Reranking; use Laravel\Ai\Prompts\RerankingPrompt; use Laravel\Ai\Responses\Data\RankedDocument; Reranking::fake(); Reranking::fake([[ new RankedDocument(index: 0, document: 'First', score: 0.95), new RankedDocument(index: 1, document: 'Second', score: 0.80), ]]); Reranking::assertReranked(function (RerankingPrompt $prompt) { return $prompt->contains('Laravel') && $prompt->limit === 5; }); Reranking::assertNotReranked(fn (RerankingPrompt $prompt) => $prompt->contains('Django')); Reranking::assertNothingReranked(); ``` ### ファイルのテスト ```php theme={null} use Laravel\Ai\Files; use Laravel\Ai\Contracts\Files\StorableFile; use Laravel\Ai\Files\Document; Files::fake(); Document::fromString('Hello, Laravel!', mimeType: 'text/plain')->as('hello.txt')->put(); Files::assertStored(fn (StorableFile $file) => (string) $file === 'Hello, Laravel!' && $file->mimeType() === 'text/plain' ); Files::assertNotStored(fn (StorableFile $file) => (string) $file === 'Hello, World!'); Files::assertNothingStored(); Files::assertDeleted('file-id'); Files::assertNotDeleted('file-id'); Files::assertNothingDeleted(); ``` ### ベクターストアのテスト ```php theme={null} use Laravel\Ai\Stores; Stores::fake(); // ファイル操作もフェイクになる $store = Stores::create('Knowledge Base'); Stores::assertCreated('Knowledge Base'); Stores::assertCreated(fn (string $name, ?string $description) => $name === 'Knowledge Base'); Stores::assertNotCreated('Other Store'); Stores::assertNothingCreated(); Stores::assertDeleted('store_id'); Stores::assertNotDeleted('other_store_id'); Stores::assertNothingDeleted(); ``` ストアに対するファイル操作のアサーションも行えます。 ```php theme={null} $store = Stores::get('store_id'); $store->add('added_id'); $store->remove('removed_id'); $store->assertAdded('added_id'); $store->assertRemoved('removed_id'); $store->assertNotAdded('other_file_id'); $store->assertNotRemoved('other_file_id'); // クロージャでコンテンツを検証 $store->add(Document::fromString('Hello, World!', 'text/plain')->as('hello.txt')); $store->assertAdded(fn (StorableFile $file) => $file->name() === 'hello.txt'); $store->assertAdded(fn (StorableFile $file) => $file->content() === 'Hello, World!'); ``` *** ## イベント Laravel AI SDK は以下のイベントをディスパッチします。これらのイベントをリスニングすることで、ログの記録や監視などに活用できます。 * `AgentFailed` — エージェント処理の失敗時 * `AgentFailedOver` — エージェントのフェイルオーバー時 * `PromptingAgent` — プロンプト送信前 * `AgentPrompted` — プロンプト送信後 * `StreamingAgent` — ストリーミング開始時 * `AgentStreamed` — ストリーミング完了後 * `InvokingTool` — ツール呼び出し前 * `ToolInvoked` — ツール呼び出し後 * `ToolApprovalRequested` — ツール承認要求時 * `ToolApprovalResolved` — ツール承認解決後 * `ProviderFailedOver` — プロバイダーのフェイルオーバー時 * `StartingStep` — ステップ開始時 * `StepCompleted` — ステップ完了時 * `StepFailed` — ステップ失敗時 * `ToolFailed` — ツール実行失敗時 * `GeneratingImage` — 画像生成前 * `ImageGenerated` — 画像生成後 * `GeneratingAudio` — 音声生成前 * `AudioGenerated` — 音声生成後 * `GeneratingTranscription` — 文字起こし前 * `TranscriptionGenerated` — 文字起こし後 * `GeneratingEmbeddings` — 埋め込み生成前 * `EmbeddingsGenerated` — 埋め込み生成後 * `Reranking` — リランキング前 * `Reranked` — リランキング後 * `StoringFile` — ファイル保存前 * `FileStored` — ファイル保存後 * `FileDeleted` — ファイル削除後 * `CreatingStore` — ストア作成前 * `StoreCreated` — ストア作成後 * `StoreDeleted` — ストア削除後 * `AddingFileToStore` — ストアへのファイル追加前 * `FileAddedToStore` — ストアへのファイル追加後 * `RemovingFileFromStore` — ストアからのファイル削除前 * `FileRemovedFromStore` — ストアからのファイル削除後 # Artisanコンソール Source: https://kawax.biz/jp/artisan LaravelのArtisanコンソールを使ってカスタムCLIコマンドを作成する方法を解説します。 ## Artisanとは ArtisanはLaravelに同梱されているコマンドラインインターフェース(CLI)ツールです。 プロジェクトルートの `artisan` スクリプトとして存在し、開発・運用を効率化する多数のコマンドを提供しています。 利用可能なコマンド一覧を確認するには `list` コマンドを使います。 ```shell theme={null} php artisan list ``` コマンドの使い方を確認するには `help` を先頭に付けます。 ```shell theme={null} php artisan help migrate ``` Laravel Sailを使っている場合は、`php artisan` の代わりに `sail artisan` を使ってください。 コマンドはDockerコンテナ内で実行されます。 ```shell theme={null} ./vendor/bin/sail artisan list ``` ## Tinker [Laravel Tinker](https://github.com/laravel/tinker) は、EloquentモデルやJob、イベントなど、アプリケーション全体をコマンドライン上でインタラクティブに操作できるREPL環境です。 ```shell theme={null} php artisan tinker ``` 起動後はPHPコードをそのまま実行できます。 ```php theme={null} // ユーザーを取得して確認 >>> App\Models\User::find(1) // ファクトリでレコードを作成 >>> App\Models\User::factory()->create() // サービスクラスを直接呼び出す >>> app(App\Services\OrderService::class)->process(1) ``` Tinkerはデータを実際に変更します。本番環境での実行は十分注意してください。 ローカルやステージング環境での動作確認・デバッグ用途に適しています。 ## カスタムコマンドの作成 ### コマンドを生成する `make:command` でコマンドクラスのひな型を生成します。 ```shell theme={null} php artisan make:command ImportProducts ``` `app/Console/Commands/ImportProducts.php` が生成されます。 ### コマンドの基本構造 生成されたコマンドクラスには、`$signature`、`$description`、`handle()` という3つの主要要素があります。 ```php theme={null} info('インポートを開始します...'); // 処理を記述する $this->info('完了しました。'); } } ``` Laravel 13ではPHPアトリビュートを使った書き方も利用できます。 ```php theme={null} use Illuminate\Console\Attributes\Description; use Illuminate\Console\Attributes\Signature; #[Signature('import:products')] #[Description('商品データをCSVからインポートする')] class ImportProducts extends Command { public function handle(): void { // ... } } ``` ### 引数とオプションの定義 `$signature` にコマンド名・引数・オプションをまとめて定義します。 ```php theme={null} protected $signature = 'import:products {file : インポートするCSVファイルのパス} {--limit= : インポートする最大件数} {--dry-run : 実際にはDBに保存しない(確認用)}'; ``` | 記法 | 種別 | 説明 | | - | - | - | | `{file}` | 必須引数 | 省略するとエラーになる | | `{file?}` | 省略可能な引数 | 省略した場合は `null` | | `{file=products.csv}` | デフォルト付き引数 | 省略した場合はデフォルト値を使う | | `{--queue}` | フラグオプション | 指定で `true`、省略で `false` | | `{--limit=}` | 値付きオプション | `--limit=100` のように値を渡す | | `{--limit=50}` | デフォルト付きオプション | 省略時は `50` | | `{--Q\|queue}` | ショートカット付き | `-Q` でも指定可能 | ### 引数とオプションの取得 `handle()` メソッド内で `argument()` と `option()` を使って値を取得します。 ```php theme={null} public function handle(): void { $file = $this->argument('file'); // 引数の取得 $limit = $this->option('limit'); // オプションの取得 $dryRun = $this->option('dry-run'); // フラグオプション(bool) $this->info("ファイル: {$file}"); if ($dryRun) { $this->warn('ドライランモード: DBへの保存はスキップします'); } } ``` ### `input()` — 型付きアクセサ `input()` メソッドを使うと、HTTPリクエストと同じ型付きアクセサでコマンドの引数やオプションを取得できます。 ```php theme={null} use App\Enums\ReportType; public function handle(): void { // 引数・オプションを CommandInput インスタンスとして取得 $from = $this->input()->date('from'); $type = $this->input()->enum('type', ReportType::class); $limit = $this->input()->integer('limit'); } ``` 特定の名前だけ取得したい場合は名前を直接渡します(引数とオプションの両方から検索されます)。 ```php theme={null} $queue = $this->input('queue', 'default'); ``` `argument()` や `option()` が文字列を返すのに対し、`input()` の型付きアクセサは適切な型に変換します。日付・数値・Enum など型安全に扱いたい場合に便利です。 ## ユーザーとのインタラクション ### メッセージの出力 コンソールに色付きのメッセージを出力するメソッドが用意されています。 ```php theme={null} $this->info('処理が正常に完了しました'); // 緑 $this->warn('この操作は元に戻せません'); // 黄 $this->error('エラーが発生しました'); // 赤 $this->line('通常のテキスト'); // 色なし $this->comment('補足情報'); // 薄いグレー ``` ### ユーザーへの問い合わせ 対話的にユーザーから入力を受け付けることができます。 ```php theme={null} // テキスト入力 $name = $this->ask('担当者名を入力してください'); // デフォルト値付き $env = $this->ask('実行環境を選択', 'production'); // パスワードなど秘匿情報(入力が画面に表示されない) $password = $this->secret('APIキーを入力してください'); // Yes/No確認 if (! $this->confirm('本番DBにインポートしますか?')) { $this->info('キャンセルしました。'); return; } // 選択肢から選ぶ $format = $this->choice('出力形式を選んでください', ['csv', 'json', 'xml'], 0); ``` ### プログレスバー 大量データの処理中に進捗をわかりやすく表示できます。 ```php theme={null} use App\Models\Product; // コレクションを渡すだけで自動的にプログレスバーを表示する $this->withProgressBar(Product::cursor(), function (Product $product) { $this->processProduct($product); }); ``` 手動で細かく制御したい場合は次の方法を使います。 ```php theme={null} $total = Product::count(); $bar = $this->output->createProgressBar($total); $bar->start(); Product::cursor()->each(function (Product $product) use ($bar) { $this->processProduct($product); $bar->advance(); }); $bar->finish(); $this->newLine(); ``` ## 実用的なユースケース ### データインポートコマンド CSVファイルから商品データをインポートする実践的なコマンドの例です。 ```shell theme={null} php artisan make:command ImportProducts ``` ```php theme={null} argument('file'); $limit = $this->option('limit') ? (int) $this->option('limit') : null; $dryRun = $this->option('dry-run'); if (! file_exists($filePath)) { $this->error("ファイルが見つかりません: {$filePath}"); return; } // PHPの組み込み関数でCSVを読み込む $handle = fopen($filePath, 'r'); $headers = fgetcsv($handle); // 1行目をヘッダーとして取得 $rows = []; while (($row = fgetcsv($handle)) !== false) { $rows[] = array_combine($headers, $row); if ($limit !== null && count($rows) >= $limit) { break; } } fclose($handle); $count = 0; $this->withProgressBar($rows, function (array $row) use ($dryRun, &$count) { if (! $dryRun) { Product::updateOrCreate( ['sku' => $row['sku']], [ 'name' => $row['name'], 'price' => $row['price'], ] ); } $count++; }); $this->newLine(); if ($dryRun) { $this->warn("{$count} 件が対象です(ドライラン: 保存はスキップしました)"); } else { $this->info("{$count} 件のインポートが完了しました。"); } } } ``` ```shell theme={null} # 通常のインポート php artisan import:products storage/products.csv # 件数を制限する php artisan import:products storage/products.csv --limit=100 # ドライランで確認 php artisan import:products storage/products.csv --dry-run ``` ### 定期メンテナンスコマンド 古いデータの削除など、定期的に実行するメンテナンスコマンドの例です。 ```php theme={null} option('days'); if (! $this->confirm("{$days}日以上前の完了済み注文を削除しますか?")) { $this->info('キャンセルしました。'); return; } $deleted = Order::where('status', 'completed') ->where('created_at', '<', now()->subDays($days)) ->delete(); $this->info("{$deleted} 件の注文データを削除しました。"); } } ``` ### スケジュール実行との組み合わせ 作成したコマンドは `routes/console.php` でスケジュール実行できます。 詳細は[タスクスケジューリング](/jp/scheduling)を参照してください。 ```php theme={null} use Illuminate\Support\Facades\Schedule; // 毎日深夜2時に実行 Schedule::command('orders:prune --days=90')->dailyAt('2:00'); // 毎週月曜9時に実行 Schedule::command('import:products storage/weekly.csv')->weeklyOn(1, '9:00'); ``` ## コマンドのテスト Laravelのテスト機能を使って、Artisanコマンドの動作を検証できます。 ```php theme={null} create([ 'status' => 'completed', 'created_at' => now()->subDays(100), ]); // 直近の完了済み注文(削除されないはず) Order::factory()->create([ 'status' => 'completed', 'created_at' => now()->subDays(10), ]); $this->artisan('orders:prune', ['--days' => 90]) ->expectsConfirmation('90日以上前の完了済み注文を削除しますか?', 'yes') ->expectsOutput('1 件の注文データを削除しました。') ->assertExitCode(0); $this->assertDatabaseCount('orders', 1); } public function test_it_cancels_when_user_declines(): void { Order::factory()->create(['status' => 'completed', 'created_at' => now()->subDays(100)]); $this->artisan('orders:prune', ['--days' => 90]) ->expectsConfirmation('90日以上前の完了済み注文を削除しますか?', 'no') ->expectsOutput('キャンセルしました。') ->assertExitCode(0); $this->assertDatabaseCount('orders', 1); } } ``` `artisan()` メソッドで使えるアサーションメソッドの例: | メソッド | 説明 | | - | - | | `expectsOutput('...')` | 指定したテキストが出力されることを検証 | | `expectsQuestion('?', '回答')` | `ask()` の入力をシミュレート | | `expectsConfirmation('?', 'yes')` | `confirm()` の応答をシミュレート | | `expectsChoice('?', '選択肢')` | `choice()` の選択をシミュレート | | `assertExitCode(0)` | 終了コードを検証(0 = 成功) | | `assertFailed()` | 終了コードが 0 以外であることを検証 | ## `dev`コマンド `dev` Artisanコマンドは、ローカル開発に必要な複数のプロセスを1つのターミナルウィンドウでまとめて起動します。デフォルトでは、PHP開発サーバー・キューワーカー・[Pail](/jp/logging)によるログ監視・Viteのアセットコンパイルが並行して実行されます。 ```shell theme={null} php artisan dev ``` 内部では[`@laravel/multiplex`](https://github.com/laravel/multiplex) npmパッケージを使ってプロセスを管理しています。各プロセスにタブが割り当てられ、出力を検索・スクロールできます。プロセスがクラッシュすると自動的に再起動され、終了時には出力がターミナルのスクロールバックへ書き戻されるためログを失いません。 `dev`コマンドにはNode.js 22.13以降が必要です。Windowsでは`concurrently` npmパッケージにフォールバックするため、タブ形式のUIは利用できません。 デフォルトで実行されるプロセスは以下の通りです。 | 名前 | コマンド | | - | - | | `server` | `php artisan serve --host=localhost` | | `queue` | `php artisan queue:listen --tries=1 --timeout=0` | | `logs` | `php artisan pail --timeout=0` | | `vite` | `npm run dev` | `vite`プロセスは、使用しているNodeパッケージマネージャー(npm、pnpm、Yarn、Bun)を自動検出して適切な実行コマンドを使用します。 プロセスは最大5回まで再起動されます。起動直後(1秒以内)に終了したプロセスは、コマンドの誤りやポートの競合である可能性が高いため再起動されません。1回の実行だけ再起動を無効にするには`--no-restart`を指定します。 ```shell theme={null} php artisan dev --no-restart ``` ### `dev`プロセスのカスタマイズ `dev`コマンドが実行するプロセスは、`DevCommands`クラスを使ってカスタマイズできます。通常はアプリケーションの`AppServiceProvider`の`boot`メソッド内で登録します。`register`メソッドはコマンド文字列と任意のプロセス名を受け取ります。 ```php theme={null} use Illuminate\Foundation\DevCommands; /** * アプリケーションサービスの初期起動処理 */ public function boot(): void { DevCommands::register('some-command --flag', 'my-process'); } ``` Artisanコマンドを登録する場合は、自動的に`php artisan`を前置してくれる`artisan`メソッドが使えます。 ```php theme={null} DevCommands::artisan('horizon', 'horizon'); ``` 同様に、`node`メソッドは検出したパッケージマネージャーの実行コマンド(例: `npm run`)を前置し、`nodeExec`メソッドはパッケージマネージャーのexecコマンド(例: `npx`)を前置します。 ```php theme={null} DevCommands::node('storybook', 'storybook'); DevCommands::nodeExec('tailwindcss -i resources/css/app.css -o public/css/app.css --watch', 'tailwind'); ``` デフォルトのプロセスと同じ名前でプロセスを登録すると、そのデフォルトプロセスを置き換えます。たとえば、サーバープロセスを別のポートで使うようにカスタマイズできます。 ```php theme={null} DevCommands::artisan('serve --host=localhost --port=9000', 'server'); ``` ターミナルでのプロセスラベルの色もカスタマイズできます。利用可能な色メソッドは`blue`、`purple`、`pink`、`orange`、`green`、`yellow`です。`color`メソッドにカスタムのhexカラーを渡すこともできます。 ```php theme={null} DevCommands::register('my-command', 'my-process')->green(); DevCommands::register('my-command', 'my-process')->color('#ff6347'); ``` 登録済みの`dev`プロセスを実際に起動せず一覧表示したい場合は、`dev:list`コマンドを使います。 ```shell theme={null} php artisan dev:list ``` ### `dev`プロセスのフィルタリング `only`メソッドを使うと、`dev`コマンド実行時に特定のプロセスだけを起動するよう指定できます。同様に、`except`メソッドで特定のプロセスを除外することもできます。 ```php theme={null} // serverとviteのプロセスのみ起動する... DevCommands::only('server', 'vite'); // キューワーカー以外のすべてのプロセスを起動する... DevCommands::except('queue'); ``` パッケージが登録した`dev`コマンドや、Laravelのデフォルトコマンドを除外したい場合は、`withoutVendorCommands`メソッドと`withoutDefaultCommands`メソッドを使います。 ```php theme={null} DevCommands::withoutVendorCommands(); DevCommands::withoutDefaultCommands(); ``` ローカル開発でHorizonやReverbなどを併用する場合、`DevCommands::artisan()`で登録しておけば`php artisan dev`一つですべて起動できて便利です。 ## よく使うコマンドまとめ ```shell theme={null} # コマンドを新規作成 php artisan make:command CommandName # 利用可能なコマンド一覧 php artisan list # コマンドの使い方を確認 php artisan help command:name # Tinkerを起動 php artisan tinker ``` # Laravel Boost Source: https://kawax.biz/jp/boost Laravel BoostはAI支援開発を加速するパッケージです。MCPサーバー、AIガイドライン、Agent Skillsを通じて、AIコーディングエージェントがあなたのLaravelアプリケーションを深く理解できるようにします。 ## はじめに Laravel Boostは、AIエージェントが高品質なLaravelアプリケーションをLaravelのベストプラクティスに沿って書けるよう、必要なガイドラインとAgent Skillsを提供することで、AI支援開発を加速させるパッケージです。 Boostはまた、1万7,000件以上のLaravel固有情報を含む知識ベースと、埋め込みを使った意味的検索を組み合わせた強力なLaravelエコシステム ドキュメントAPIを提供します。Boostは、Claude CodeやCursorなどのAIエージェントに対して、このAPIを活用して最新のLaravel機能やベストプラクティスを学ぶよう指示します。 ## インストール Laravel BoostはComposerでインストールします。 ```shell theme={null} composer require laravel/boost --dev ``` 次に、MCPサーバーとコーディングガイドラインをインストールします。 ```shell theme={null} php artisan boost:install ``` `boost:install` コマンドは、インストール時に選択したコーディングエージェント向けの関連ガイドラインとスキルファイルを生成します。 インストール完了後は、Cursor、Claude Code、またはお好みのAIエージェントを使ってコーディングを始められます。 生成されたMCP設定ファイル(`.mcp.json`)、ガイドラインファイル(`CLAUDE.md`、`AGENTS.md`、`junie/` など)、および `boost.json` 設定ファイルはアプリケーションの `.gitignore` に追加しても構いません。これらのファイルは `boost:install` や `boost:update` を実行する際に自動的に再生成されます。 ### エージェントの設定 1. コマンドパレットを開きます(`Cmd+Shift+P` または `Ctrl+Shift+P`) 2. "/open MCP Settings" を選択して `Enter` を押します 3. `laravel-boost` のトグルをオンにします Claude Codeのサポートは通常、自動的に有効になります。有効になっていない場合は、プロジェクトディレクトリでシェルを開き、次のコマンドを実行してください。 ```shell theme={null} claude mcp add -s local -t stdio laravel-boost php artisan boost:mcp ``` Codexのサポートは通常、自動的に有効になります。有効になっていない場合は、プロジェクトディレクトリでシェルを開き、次のコマンドを実行してください。 ```shell theme={null} codex mcp add laravel-boost -- php "artisan" "boost:mcp" ``` Gemini CLIのサポートは通常、自動的に有効になります。有効になっていない場合は、プロジェクトディレクトリでシェルを開き、次のコマンドを実行してください。 ```shell theme={null} gemini mcp add -s project -t stdio laravel-boost php artisan boost:mcp ``` 1. コマンドパレットを開きます(`Cmd+Shift+P` または `Ctrl+Shift+P`) 2. "MCP: List Servers" を選択して `Enter` を押します 3. `laravel-boost` を選択して `Enter` を押します 4. "Start server" を選択します 1. `Shift` を2回押してコマンドパレットを開きます 2. "MCP Settings" を検索して `Enter` を押します 3. `laravel-boost` の横のチェックボックスにチェックを入れます 4. 右下の "Apply" をクリックします ### Boostリソースの更新 インストールされているLaravelエコシステムパッケージの最新バージョンを反映させるため、ローカルのBoostリソース(AIガイドラインとスキル)を定期的に更新することをお勧めします。`boost:update` Artisanコマンドを使用してください。 ```shell theme={null} php artisan boost:update ``` Composerの `post-update-cmd` スクリプトに追加して自動化することもできます。 ```json theme={null} { "scripts": { "post-update-cmd": [ "@php artisan boost:update --ansi" ] } } ``` デフォルトでは、`boost:update` コマンドはアプリケーション内にすでに公開されているBoostリソースのみを更新します。新しくインストールされたパッケージを検出してそのガイドラインやスキルを公開するよう促したい場合は、`--discover` オプションを使用します。 ```shell theme={null} php artisan boost:update --discover ``` ## MCPサーバー Laravel BoostはMCP(Model Context Protocol)サーバーを提供し、AIエージェントがLaravelアプリケーションと対話するためのツールを公開します。これらのツールにより、エージェントはアプリケーションの構造を調査したり、データベースに問い合わせたり、コードを実行したりすることができます。 ### 利用可能なMCPツール | ツール名 | 説明 | | - | - | | Application Info | PHPとLaravelのバージョン、データベースエンジン、バージョン付きエコシステムパッケージ一覧、Eloquentモデルを読み取る | | Browser Logs | ブラウザからのログとエラーを読み取る | | Database Connections | デフォルト接続を含む利用可能なデータベース接続を確認する | | Database Query | データベースに対してクエリを実行する | | Database Schema | データベーススキーマを読み取る | | Get Absolute URL | 相対パスのURIを絶対URLに変換し、エージェントが有効なURLを生成できるようにする | | Last Error | アプリケーションのログファイルから最後のエラーを読み取る | | Read Log Entries | 最後のN件のログエントリを読み取る | | Record Rule | 将来のエージェントが引き継ぐ永続的な[プロジェクトルール](#プロジェクトルール)を `.ai/rules` に記録する | | Search Docs | インストール済みパッケージに基づいてLaravelのホスト型ドキュメントAPIサービスに問い合わせる | ### 手動でのMCPサーバー登録 エディタによっては、Laravel Boost MCPサーバーを手動で登録する必要がある場合があります。以下の情報でMCPサーバーを登録してください。 | 項目 | 値 | | - | - | | **Command** | `php` | | **Args** | `artisan boost:mcp` | JSON形式での記述例: ```json theme={null} { "mcpServers": { "laravel-boost": { "command": "php", "args": ["artisan", "boost:mcp"] } } } ``` ## AIガイドライン AIガイドラインは、AIエージェントにLaravelエコシステムパッケージに関する重要なコンテキストを提供するために、あらかじめ読み込まれる命令ファイルです。これらのガイドラインにはコアの規約、ベストプラクティス、フレームワーク固有のパターンが含まれており、エージェントが一貫した高品質なコードを生成するのに役立ちます。 ### 利用可能なガイドライン Laravel Boostには以下のパッケージとフレームワーク向けのAIガイドラインが含まれています。`core` ガイドラインは、バージョンに関係なく適用される汎用的なアドバイスを提供します。 | パッケージ | 対応バージョン | | - | - | | Core & Boost | core | | Laravel Framework | core, 10.x, 11.x, 12.x | | Livewire | core, 2.x, 3.x, 4.x | | Flux UI | core, free, pro | | Folio | core | | Herd | core | | Inertia Laravel | core, 1.x, 2.x, 3.x | | Inertia React | core, 1.x, 2.x, 3.x | | Inertia Vue | core, 1.x, 2.x, 3.x | | Inertia Svelte | core, 1.x, 2.x, 3.x | | MCP | core | | Pennant | core | | Pest | core, 3.x, 4.x | | PHPUnit | core | | Pint | core | | Sail | core | | Tailwind CSS | core, 3.x, 4.x | | Livewire Volt | core | | Wayfinder | core | | Enforce Tests | conditional | AIガイドラインを最新の状態に保つには、[Boostリソースの更新](#boostリソースの更新)を参照してください。 ### カスタムガイドラインの追加 独自のカスタムAIガイドラインをLaravel Boostに追加するには、`.blade.php` または `.md` ファイルをアプリケーションの `.ai/guidelines/*` ディレクトリに追加します。これらのファイルは `boost:install` を実行する際に、Laravel Boostのガイドラインと自動的に組み合わされます。 ### Boostのガイドラインをオーバーライドする ファイルパスが一致するカスタムガイドラインを作成することで、Boostの組み込みガイドラインをオーバーライドできます。既存のBoostガイドラインと一致するパスにカスタムガイドラインを作成すると、Boostは組み込みのガイドラインの代わりにカスタムバージョンを使用します。 たとえば、Boostの "Inertia React v2 Form Guidance" ガイドラインをオーバーライドするには、`.ai/guidelines/inertia-react/2/forms.blade.php` にファイルを作成します。`boost:install` を実行すると、Boostはデフォルトのガイドラインの代わりにカスタムガイドラインを使用します。 ### サードパーティパッケージのガイドライン サードパーティパッケージを管理しており、Boostにそのパッケージ向けのAIガイドラインを含めたい場合は、パッケージに `resources/boost/guidelines/core.blade.php` ファイルを追加します。パッケージのユーザーが `php artisan boost:install` を実行すると、Boostが自動的にそのガイドラインを読み込みます。 AIガイドラインには、パッケージの概要、必要なファイル構造や規約の説明、主な機能の作成方法や使用方法(コマンドやコードスニペットの例を含む)を記述してください。簡潔でアクション重視の内容にし、ベストプラクティスに焦点を当てることで、AIがユーザーのために正しいコードを生成できるようになります。 ```php theme={null} ## Package Name This package provides [brief description of functionality]. ### Features - Feature 1: [clear & short description]. - Feature 2: [clear & short description]. Example usage: @verbatim $result = PackageName::featureTwo($param1, $param2); @endverbatim ``` ## Agent Skills [Agent Skills](https://agentskills.io/home) は、エージェントが特定のドメインで作業する際にオンデマンドで有効化できる、軽量で的を絞った知識モジュールです。ガイドラインがあらかじめ読み込まれるのとは異なり、スキルは関連する場合にのみ詳細なパターンとベストプラクティスを読み込むため、コンテキストの肥大化を抑えてAI生成コードの品質を高めます。 `boost:install` を実行してスキルを機能として選択すると、`composer.json` で検出されたパッケージに基づいてスキルが自動的にインストールされます。たとえば、プロジェクトに `livewire/livewire` が含まれている場合、`livewire-development` スキルが自動的にインストールされます。`infer-conventions` などBoostに含まれるスキルは、パッケージの種類に関係なくインストールされます。 ### 利用可能なスキル | スキル | パッケージ | | - | - | | fluxui-development | Flux UI | | folio-routing | Folio | | infer-conventions | Boost | | inertia-react-development | Inertia React | | inertia-svelte-development | Inertia Svelte | | inertia-vue-development | Inertia Vue | | livewire-development | Livewire | | mcp-development | MCP | | pennant-development | Pennant | | pest-testing | Pest | | tailwindcss-development | Tailwind CSS | | volt-development | Volt | | wayfinder-development | Wayfinder | スキルを最新の状態に保つには、[Boostリソースの更新](#boostリソースの更新)を参照してください。 ### カスタムスキルの作成 独自のカスタムスキルを作成するには、`SKILL.md` ファイルをアプリケーションの `.ai/skills/{skill-name}/` ディレクトリに追加します。`boost:update` を実行すると、カスタムスキルがBoostの組み込みスキルと一緒にインストールされます。 たとえば、アプリケーション固有のドメインロジック向けのカスタムスキルを作成するには: ``` .ai/skills/creating-invoices/SKILL.md ``` ### スキルのオーバーライド 一致する名前のカスタムスキルを作成することで、Boostの組み込みスキルをオーバーライドできます。既存のBoostスキルと一致する名前でカスタムスキルを作成すると、Boostは組み込みスキルの代わりにカスタムバージョンを使用します。 たとえば、Boostの `livewire-development` スキルをオーバーライドするには、`.ai/skills/livewire-development/SKILL.md` にファイルを作成します。`boost:update` を実行すると、Boostはデフォルトのスキルの代わりにカスタムスキルを使用します。 ### サードパーティパッケージのスキル サードパーティパッケージを管理しており、Boostにそのパッケージ向けのスキルを含めたい場合は、パッケージに `resources/boost/skills/{skill-name}/SKILL.md` ファイルを追加します。パッケージのユーザーが `php artisan boost:install` を実行すると、Boostはユーザーの希望に応じてスキルを自動的にインストールします。 BoostのスキルはAgent Skillsフォーマットをサポートしており、YAMLフロントマターとMarkdown指示を含む `SKILL.md` ファイルを格納したフォルダとして構成する必要があります。`SKILL.md` ファイルには必須フロントマター(`name` と `description`)を含め、スクリプト、テンプレート、参照資料をオプションで含めることができます。 ```markdown theme={null} --- name: package-name-development description: Build and work with PackageName features, including components and workflows. --- # Package Name Development ## When to use this skill Use this skill when working with PackageName features... ## Features - Feature 1: [clear & short description]. - Feature 2: [clear & short description]. Example usage: $result = PackageName::featureTwo($param1, $param2); ``` ## ガイドラインとスキルの比較 Laravel Boostは、AIエージェントにアプリケーションに関するコンテキストを提供するための2つの異なる方法を提供します: **ガイドライン**と**スキル**です。 **ガイドライン**はAIエージェントの起動時にあらかじめ読み込まれ、コードベース全体に広く適用されるLaravelの規約とベストプラクティスに関する重要なコンテキストを提供します。 **スキル**は特定のタスクで作業する際にオンデマンドで有効化され、特定のドメイン(LivewireコンポーネントやPestテストなど)の詳細なパターンを含みます。必要な場合にのみスキルを読み込むことで、コンテキストの肥大化を抑えてコード品質を向上させます。 | 観点 | ガイドライン | スキル | | - | - | - | | **読み込みタイミング** | あらかじめ、常に存在 | オンデマンド、関連する場合のみ | | **スコープ** | 広範囲、基盤的 | 焦点を絞った、タスク固有 | | **目的** | コアの規約とベストプラクティス | 詳細な実装パターン | ガイドラインとスキルはLaravelエコシステムについての知識を提供します。アプリケーション固有の規約をAIエージェントに伝えるには、[プロジェクトルール](#プロジェクトルール)を使用します。 ## プロジェクトルール ガイドラインとスキルがLaravelのコードの書き方を教えるのに対し、プロジェクトルールはあなたのアプリケーションのコードの書き方を教えます。ルールは、新しいセッションのたびに説明し直す必要がある次のような内容です。 * あなた、エージェント、チームメンバーが決めた設計上の判断 * エージェントに守らせることが難しいスタイルガイドや好み * 周囲のコードから推測できない制約や注意点 ルールはアプリケーションの `.ai/rules` ディレクトリにMarkdownファイルとして保存し、ソース管理にコミットします。個人単位・セッション単位のエージェントの記憶とは異なり、ルールはチームとアプリケーションに関わるすべてのエージェントで共有されます。 各ルールファイルのfrontmatterには、適用対象のファイルglobを定義します。 ```markdown theme={null} --- paths: - app/Http/Controllers/** --- # Http Controllers ## テナントのスコープにはBaseControllerを継承する すべてのコントローラーは `App\Http\Controllers\BaseController` を継承します。 このクラスが現在のテナントのクエリスコープを適用します。 Laravelのベースコントローラーを直接継承すると、テナント間でデータが漏洩します。 ``` Boostは `.ai/rules/index.md` ファイルも管理し、globとルールファイルの対応を記録します。エージェントはファイルの計画や編集の前にこのインデックスを参照するよう指示されるため、関連するルールだけが読み込まれます。 ```markdown theme={null} # Project Rules Index 計画や編集の前に、ファイルパスに一致するglobの行を探し、そのルールファイルを読み込みます。 | 適用対象 | ルールファイル | | --- | --- | | app/Http/Controllers/** | .ai/rules/controllers.md | | app/Models/** | .ai/rules/models.md | ``` `.mcp.json` や生成されたガイドラインファイルとは異なり、`.ai/rules` ディレクトリはチームでルールを共有するためにソース管理へコミットします。 ### ルールを記録する エージェントに覚えておくよう依頼するだけで、ルールを記録できます。 ```text theme={null} 金額はすべて浮動小数点数ではなく、整数のセント単位で保存する。 ``` エージェントは `glob`、短い `title`、`note` を指定してBoostの `record-rule` MCPツールを呼び出します。Boostは該当する領域にルールを記録し、必要に応じてルールファイルを作成してインデックスを更新します。 ルールファイルを手動で作成せず、必ず `record-rule` ツールで記録してください。ルールの記録時にBoostが `.ai/rules/index.md` を再生成します。エージェントはこのインデックスから適用対象のルールを見つけるため、手動で追加したルールファイルはインデックスを再生成するまで発見されません。 ### アプリケーションの規約を推測する 既存のアプリケーションには、長年にわたって蓄積された規約があります。`infer-conventions` スキルは、既に記述されたコードからルールを作成します。エージェントに次のように依頼して開始します。 ```text theme={null} infer-conventions スキルを使う ``` このスキルは、validation、controller、authorization、model、architecture、testing、frontend、database、consoleなどの規約をチェックリストに沿って調査し、さらに基底クラス、共有トレイト、モジュール構成などのパターンを自由形式で調査します。 スキルは「こうあるべき」という理想ではなく、実際のコードの内容を記録します。十分な根拠がある独自の規約だけを記録し、フレームワークのデフォルトやPint・Rectorがすでに強制している内容は除外します。パターンが混在している場合は、無理にルール化せずその事実を報告します。ルールを書き込む前に、発見した規約と根拠を提示して承認を求めます。確認なしですべての規約を記録する場合は「yolo」と指示できます。 ### プロジェクトルールを無効にする プロジェクトルールはデフォルトで有効です。完全に無効化するには、次の環境変数を定義します。`record-rule` MCPツールが削除され、Boostによる `.ai/rules` ディレクトリの管理も停止します。 ```ini theme={null} BOOST_RULES_ENABLED=false ``` ## ドキュメントAPI Laravel Boostには、1万7,000件以上のLaravel固有情報を含む知識ベースへのアクセスをAIエージェントに提供するドキュメントAPIが含まれています。このAPIは埋め込みを使った意味的検索を使用して、精度の高いコンテキスト対応の結果を提供します。 `Search Docs` MCPツールにより、エージェントはインストール済みパッケージに基づいてLaravelのホスト型ドキュメントAPIサービスに問い合わせることができます。BoostのAIガイドラインとスキルがコーディングエージェントにこのAPIを使用するよう自動的に指示します。 | パッケージ | 対応バージョン | | - | - | | Laravel Framework | 10.x, 11.x, 12.x | | Filament | 2.x, 3.x, 4.x, 5.x | | Flux UI | 2.x Free, 2.x Pro | | Inertia | 1.x, 2.x | | Livewire | 1.x, 2.x, 3.x, 4.x | | Nova | 4.x, 5.x | | Pest | 3.x, 4.x | | Tailwind CSS | 3.x, 4.x | ## Boostの拡張 Boostは多くの一般的なIDEやAIエージェントですぐに使えます。お使いのコーディングツールがまだサポートされていない場合は、独自のエージェントを作成してBoostと統合できます。 ### 他のIDE/AIエージェントへの対応追加 新しいIDEやAIエージェントのサポートを追加するには、`Laravel\Boost\Install\Agents\Agent` を継承するクラスを作成し、必要に応じて以下のコントラクトを1つ以上実装します。 * `Laravel\Boost\Contracts\SupportsGuidelines` — AIガイドラインのサポートを追加 * `Laravel\Boost\Contracts\SupportsMcp` — MCPのサポートを追加 * `Laravel\Boost\Contracts\SupportsSkills` — Agent Skillsのサポートを追加 #### エージェントの実装 ```php theme={null} ブロードキャストはLaravelのイベントシステムの上に構築されています。 まず[イベントとリスナー](/jp/events)の基本を理解しておくことを推奨します。 ```mermaid theme={null} flowchart LR A["サーバーサイド
    イベント発火
    dispatch()"] --> B["ブロードキャスト
    ドライバー
    (Reverb等)"] B --> C["WebSocket
    サーバー"] C --> D["チャンネル
    認可チェック"] D --> E["クライアント
    Laravel Echo"] E --> F["UIの
    リアルタイム更新"] ``` ## セットアップ 新規Laravelアプリケーションではブロードキャストはデフォルトで無効です。 `install:broadcasting` Artisanコマンドで有効化します。 ```shell theme={null} php artisan install:broadcasting ``` このコマンドを実行すると、使用するブロードキャストサービスを選択するよう促されます。 また、`config/broadcasting.php` と `routes/channels.php` が生成されます。 ## Laravel Reverb Laravel 11以降では、公式WebSocketサーバーである **Laravel Reverb** が推奨されます。 Reverbはセルフホスト型で、追加の外部サービス不要でリアルタイム通信を実現します。 ### インストール `install:broadcasting` コマンドに `--reverb` オプションを付けると、Reverbに必要なComposerパッケージ・NPMパッケージのインストールと `.env` の設定を一括で行います。 ```shell theme={null} php artisan install:broadcasting --reverb ``` 手動でインストールする場合は、Composerでパッケージを追加してからインストールコマンドを実行します。 ```shell theme={null} composer require laravel/reverb php artisan reverb:install ``` ### .env の主な設定値 ```ini theme={null} BROADCAST_CONNECTION=reverb REVERB_APP_ID=my-app-id REVERB_APP_KEY=my-app-key REVERB_APP_SECRET=my-app-secret REVERB_HOST=localhost REVERB_PORT=8080 REVERB_SCHEME=http VITE_REVERB_APP_KEY="${REVERB_APP_KEY}" VITE_REVERB_HOST="${REVERB_HOST}" VITE_REVERB_PORT="${REVERB_PORT}" VITE_REVERB_SCHEME="${REVERB_SCHEME}" ``` ### Reverbサーバーの起動 ```shell theme={null} php artisan reverb:start ``` 本番環境ではSupervisorなどのプロセスマネージャーでデーモンとして管理します。 ブロードキャストイベントはキュー経由で処理されます。 Reverbサーバーと別にキューワーカーも起動しておく必要があります。 ```shell theme={null} php artisan queue:work ``` ## ブロードキャストイベントの作成 ### イベントクラスの生成 ```shell theme={null} php artisan make:event OrderShipmentStatusUpdated ``` 生成されたイベントクラスに `ShouldBroadcast` インターフェースを実装します。 ```php theme={null} order->id); } } ``` `ShouldBroadcast` を実装するだけで、イベントが発火されると自動的にキュー経由でブロードキャストされます。 ### ブロードキャストのキューを一元管理する すべてのブロードキャストイベントで同じキューを使う場合は、イベントクラスごとに設定する代わりに、`ShouldBroadcast` 契約を[キューのルーティング](/jp/queues#queue-routing)へ登録できます。 ```php theme={null} use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Support\Facades\Queue; Queue::route(ShouldBroadcast::class, queue: 'broadcasts'); ``` これにより、ブロードキャストイベントのキュー名をインフラ設定として一元管理できます。特定のイベントだけ別のキューを使う場合は、そのイベントで `onQueue()` などの設定を行ってください。 ### ブロードキャストデータのカスタマイズ デフォルトでは、イベントクラスの `public` プロパティがすべてブロードキャストのペイロードに含まれます。 送信するデータを絞り込む場合は `broadcastWith` メソッドを定義します。 ```php theme={null} public function broadcastWith(): array { return [ 'order_id' => $this->order->id, 'status' => $this->order->status, ]; } ``` ### ブロードキャスト名のカスタマイズ デフォルトではクラス名がイベント名になります。 `broadcastAs` メソッドでカスタム名を指定できます。 ```php theme={null} public function broadcastAs(): string { return 'order.status.updated'; } ``` フロントエンドでリスニングするときは、先頭に `.` を付けてアプリの名前空間プレフィックスを無効にします。 ```js theme={null} Echo.private(`orders.${orderId}`) .listen('.order.status.updated', (e) => { console.log(e); }); ``` ## チャンネルの種類 | チャンネル | クラス | 説明 | | - | - | - | | **Public** | `Channel` | 認証不要。誰でもサブスクライブできる | | **Private** | `PrivateChannel` | 認証済みユーザーのみ。認可ロジックが必要 | | **Presence** | `PresenceChannel` | Privateの拡張。チャンネルの参加者一覧を取得できる | ```php theme={null} use Illuminate\Broadcasting\Channel; use Illuminate\Broadcasting\PresenceChannel; use Illuminate\Broadcasting\PrivateChannel; // Publicチャンネル public function broadcastOn(): Channel { return new Channel('posts'); } // Privateチャンネル public function broadcastOn(): Channel { return new PrivateChannel('orders.' . $this->order->id); } // Presenceチャンネル public function broadcastOn(): Channel { return new PresenceChannel('rooms.' . $this->room->id); } ``` 複数チャンネルにブロードキャストするには配列で返します。 ```php theme={null} public function broadcastOn(): array { return [ new PrivateChannel('orders.' . $this->order->id), new Channel('admin.orders'), ]; } ``` ## チャンネル認可 PrivateチャンネルとPresenceチャンネルは、サブスクライブ前にサーバーサイドで認可チェックが行われます。 ### routes/channels.php `install:broadcasting` コマンドで生成される `routes/channels.php` に認可コールバックを定義します。 ```php theme={null} use App\Models\Order; use App\Models\User; use Illuminate\Support\Facades\Broadcast; Broadcast::channel('orders.{orderId}', function (User $user, int $orderId) { return $user->id === Order::findOrNew($orderId)->user_id; }); ``` コールバックの第1引数は認証済みユーザー、以降の引数はチャンネル名のワイルドカードです。 `true` またはtruthyな値を返すと認可成功、`false` を返すと拒否されます。 ルートモデルバインディングも使用できます。 チャンネル名を `orders.{order}` とすると、`Order` モデルのインスタンスが渡されます。 ```php theme={null} Broadcast::channel('orders.{order}', function (User $user, Order $order) { return $user->id === $order->user_id; }); ``` ### チャンネルクラスによる認可 チャンネルが増えてきた場合は、チャンネルクラスを使って整理できます。 ```shell theme={null} php artisan make:channel OrderChannel ``` ```php theme={null} id === $order->user_id; } } ``` `routes/channels.php` で登録します。 ```php theme={null} use App\Broadcasting\OrderChannel; Broadcast::channel('orders.{order}', OrderChannel::class); ``` ## イベントの発火 `ShouldBroadcast` を実装したイベントは、通常のイベントと同じように発火します。 ```php theme={null} use App\Events\OrderShipmentStatusUpdated; OrderShipmentStatusUpdated::dispatch($order); ``` 自分自身を除く他のユーザーにだけブロードキャストしたい場合は `toOthers` を使います。 ```php theme={null} broadcast(new OrderShipmentStatusUpdated($order))->toOthers(); ``` `toOthers` を使うには、イベントクラスが `InteractsWithSockets` トレイトを使用している必要があります。 ## フロントエンドでの受信 ### Laravel Echoのセットアップ Reverbを使う場合、`resources/js/bootstrap.js` にEchoインスタンスを設定します。 ```shell theme={null} npm install --save-dev laravel-echo pusher-js ``` ```js theme={null} import Echo from 'laravel-echo'; import Pusher from 'pusher-js'; window.Pusher = Pusher; window.Echo = new Echo({ broadcaster: 'reverb', key: import.meta.env.VITE_REVERB_APP_KEY, wsHost: import.meta.env.VITE_REVERB_HOST, wsPort: import.meta.env.VITE_REVERB_PORT ?? 80, wssPort: import.meta.env.VITE_REVERB_PORT ?? 443, forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https', enabledTransports: ['ws', 'wss'], }); ``` ### イベントのリスニング ```js theme={null} // Publicチャンネル Echo.channel('posts') .listen('PostPublished', (e) => { console.log(e.post); }); // Privateチャンネル Echo.private(`orders.${orderId}`) .listen('OrderShipmentStatusUpdated', (e) => { console.log(e.order); }); // Presenceチャンネル Echo.join(`rooms.${roomId}`) .here((users) => { console.log('現在のメンバー:', users); }) .joining((user) => { console.log(user.name, 'が参加しました'); }) .leaving((user) => { console.log(user.name, 'が退出しました'); }) .listen('MessagePosted', (e) => { console.log(e.message); }); ``` ### React / Vue フックの使用 React・Vueのスターターキットを使っている場合、専用フックを使うと簡潔に書けます。 ```js theme={null} import { useEcho } from "@laravel/echo-react"; // Privateチャンネル useEcho( `orders.${orderId}`, "OrderShipmentStatusUpdated", (e) => { console.log(e.order); }, ); // Publicチャンネル import { useEchoPublic } from "@laravel/echo-react"; useEchoPublic("posts", "PostPublished", (e) => { console.log(e.post); }); ``` `useEcho` フックはコンポーネントのアンマウント時に自動でチャンネルを離脱します。 フロントエンドのアセットをビルドする前に、`.env` の `VITE_REVERB_*` 変数が正しく設定されていることを確認してください。 ```shell theme={null} npm run build ``` ## 実践例: 注文ステータスのリアルタイム更新 ```shell theme={null} php artisan install:broadcasting --reverb ``` Reverbサーバーとキューワーカーを起動します。 ```shell theme={null} php artisan reverb:start php artisan queue:work ``` ```shell theme={null} php artisan make:event OrderShipmentStatusUpdated ``` `app/Events/OrderShipmentStatusUpdated.php` を編集します。 ```php theme={null} order->id); } public function broadcastWith(): array { return [ 'order_id' => $this->order->id, 'status' => $this->order->status, ]; } } ``` `routes/channels.php` に認可ロジックを追加します。 ```php theme={null} use App\Models\Order; use App\Models\User; use Illuminate\Support\Facades\Broadcast; Broadcast::channel('orders.{order}', function (User $user, Order $order) { return $user->id === $order->user_id; }); ``` 注文ステータスを更新するコントローラやJobで発火します。 ```php theme={null} use App\Events\OrderShipmentStatusUpdated; $order->update(['status' => 'shipped']); OrderShipmentStatusUpdated::dispatch($order); ``` Bladeテンプレートにインラインスクリプト、またはReact/Vueコンポーネントで受信します。 ```js theme={null} Echo.private(`orders.${orderId}`) .listen('OrderShipmentStatusUpdated', (e) => { document.getElementById('status').textContent = e.status; }); ``` ## 次のステップ Reverbサーバーのセットアップ、本番環境での運用、スケーリングの詳細を確認してください。 ブロードキャストはキュー経由で処理されます。キューの設定と運用方法を確認してください。 ブロードキャストの基盤となるLaravelイベントシステムの詳細を学びます。 Reverb/Pusherとは異なるSSEベースの新しいブロードキャストドライバーの仕組みを解説します。 # キャッシュ Source: https://kawax.biz/jp/cache Laravelのキャッシュシステムを使ってアプリケーションのパフォーマンスを向上させる方法を学びます。 ## キャッシュとは データベースへの問い合わせや外部APIの呼び出しは、CPUやネットワークのコストが高く、処理に数秒かかることがあります。 同じデータを何度も取得する場合、結果を**キャッシュ**に保存しておくことで、次回以降のリクエストを高速に処理できます。 Laravelは Memcached、Redis、DynamoDB、データベースなど、さまざまなキャッシュバックエンドに対応した統一されたAPIを提供しています。 デフォルトでは `database` ドライバが設定されています。Redis や Memcached に切り替えることで、さらに高速なキャッシュを実現できます。 ## キャッシュの設定 ### config/cache.php キャッシュの設定は `config/cache.php` に集約されています。 `CACHE_STORE` 環境変数でデフォルトのドライバを切り替えます。 ```php theme={null} // config/cache.php 'default' => env('CACHE_STORE', 'database'), ``` ### 利用可能なドライバ シリアライズされたキャッシュデータをデータベーステーブルに保存します。 Laravel 11以降の新規プロジェクトには最初からマイグレーションが含まれています。 ```ini theme={null} CACHE_STORE=database ``` マイグレーションが含まれていない場合は Artisan コマンドで作成します。 ```shell theme={null} php artisan make:cache-table php artisan migrate ``` キャッシュデータをファイルシステムに保存します。 追加のセットアップは不要で、小規模なアプリケーションに向いています。 ```ini theme={null} CACHE_STORE=file ``` インメモリで動作する高速なキャッシュドライバです。 本番環境で最もよく使われます。PhpRedis PHP 拡張または `predis/predis` パッケージが必要です。 ```ini theme={null} CACHE_STORE=redis REDIS_HOST=127.0.0.1 REDIS_PORT=6379 ``` Memcached PECL パッケージが必要です。 `config/cache.php` でサーバーを設定します。 ```php theme={null} 'memcached' => [ 'servers' => [ [ 'host' => env('MEMCACHED_HOST', '127.0.0.1'), 'port' => env('MEMCACHED_PORT', 11211), 'weight' => 100, ], ], ], ``` AWS DynamoDB をキャッシュストアとして使います。 事前に DynamoDB テーブルを作成し、AWS SDK をインストールします。 ```shell theme={null} composer require aws/aws-sdk-php ``` ```ini theme={null} CACHE_STORE=dynamodb DYNAMODB_CACHE_TABLE=cache AWS_DEFAULT_REGION=us-east-1 AWS_ACCESS_KEY_ID=your-key-id AWS_SECRET_ACCESS_KEY=your-secret-key ``` 任意のファイルシステムディスクをキー/バリューのキャッシュストアとして使います。 既存のS3ディスクをそのままキャッシュに使いたい場合に便利です。 ```php theme={null} 'storage' => [ 'driver' => 'storage', 'disk' => env('CACHE_STORAGE_DISK'), 'path' => env('CACHE_STORAGE_PATH', 'framework/cache/data'), ], ``` `array` はリクエスト内でのみ有効なインメモリキャッシュです。 `null` はすべての操作を無視します。どちらも自動テストで便利です。 ```ini theme={null} CACHE_STORE=array ``` ### キャッシュドライバーの階層構造 用途や速度に応じてドライバを使い分けます。 ```mermaid theme={null} flowchart LR A["アプリケーション"] --> B["Cache ファサード"] subgraph L1 ["L1: リクエスト内(最速)"] C["array
    テスト・開発用"] D["memo(ラッパー)
    重複アクセス削減"] end subgraph L2 ["L2: インメモリ(高速)"] E["Redis
    本番環境推奨"] F["Memcached
    分散キャッシュ"] end subgraph L3 ["L3: 永続化ストレージ(標準)"] G["database(デフォルト)"] H["file"] I["DynamoDB(AWS)"] J["storage
    S3などのディスク"] end B --> C B --> D B --> E B --> F B --> G B --> H B --> I B --> J ``` ## 基本的な操作 ### Cache ファサードの取得 `Cache` ファサードを使ってキャッシュを操作します。 ```php theme={null} get('foo'); Cache::store('redis')->put('bar', 'baz', 600); // 10分間保存 ``` ### データの取得: `Cache::get()` `get()` メソッドでキャッシュからデータを取得します。 キャッシュが存在しない場合は `null` を返します。デフォルト値を指定することもできます。 ```php theme={null} $value = Cache::get('key'); // デフォルト値を指定する $value = Cache::get('key', 'default'); // クロージャでデフォルト値を遅延取得する $value = Cache::get('key', function () { return DB::table('settings')->get(); }); ``` ### データの保存: `Cache::put()` `put()` メソッドでデータをキャッシュに保存します。第3引数に有効期限(秒)を指定します。 ```php theme={null} // 10秒間保存 Cache::put('key', 'value', 10); // Carbon インスタンスで有効期限を指定 Cache::put('key', 'value', now()->plus(minutes: 10)); // 有効期限なし(永続保存) Cache::put('key', 'value'); ``` キャッシュが存在しない場合のみ保存する `add()` メソッドもあります。 ```php theme={null} // 存在しない場合のみ追加(アトミック操作) Cache::add('key', 'value', $seconds); ``` 永続的に保存するには `forever()` を使います。 ```php theme={null} Cache::forever('key', 'value'); ``` ### 取得または保存: `Cache::remember()` **最もよく使われる操作**です。キャッシュにデータが存在すればそれを返し、なければクロージャを実行してその結果を保存します。 ```php theme={null} $users = Cache::remember('users', 3600, function () { return DB::table('users')->get(); }); ``` `Cache::remember()` を使うことで、「キャッシュの確認 → なければ取得 → キャッシュに保存」という3ステップを1行で書けます。データベースクエリ結果や外部APIレスポンスのキャッシュに最適です。 ### remember() のフロー ```mermaid theme={null} flowchart TD A["Cache::remember('key', $ttl, fn)"] --> B{"キャッシュストアに
    'key' が存在する?"} B -->|"ヒット (Hit)"| C["キャッシュからデータを取得"] B -->|"ミス (Miss)"| D["クロージャ fn を実行
    例: DB クエリ / API 呼び出し"] D --> E["結果をキャッシュに保存
    有効期限 = $ttl 秒"] E --> F["データを返す"] C --> F ``` 永続保存バージョンの `rememberForever()` もあります。 ```php theme={null} $value = Cache::rememberForever('users', function () { return DB::table('users')->get(); }); ``` キャッシュが「ヒット」したのか、クロージャで新しく取得されたのかを知りたい場合は `rememberWithWarmth()` メソッドを使用します。このメソッドはキャッシュ値とそれが新鮮(キャッシュから取得)かどうかを示す真偽値を配列で返します。 ```php theme={null} [$value, $warm] = Cache::rememberWithWarmth('users', 3600, function () { return DB::table('users')->get(); }); if ($warm) { // キャッシュからのデータ } else { // クロージャで新しく取得したデータ } ``` `rememberWithWarmth()` はキャッシュの効率性を監視したい場合に便利です。\$warm が `false` の場合が多い場合は、キャッシュの有効期限(TTL)を調整することで改善できるかもしれません。 #### Stale While Revalidate(柔軟なキャッシュ更新) `Cache::flexible()` はキャッシュが「新鮮な期間」と「古くなっても使える期間」を配列で指定します。 古くなったデータをユーザーに返しながら、バックグラウンドでキャッシュを更新するパターンです。 ```php theme={null} // [新鮮な期間(秒), 古くても使える期間(秒)] $value = Cache::flexible('users', [5, 10], function () { return DB::table('users')->get(); }); ``` ### 存在確認: `Cache::has()` ```php theme={null} if (Cache::has('key')) { // キャッシュが存在する場合の処理 } ``` ### 値のインクリメント / デクリメント 整数値のカウンターを操作できます。 ```php theme={null} // 値が存在しない場合は初期化 Cache::add('key', 0, now()->addHours(4)); // インクリメント / デクリメント Cache::increment('key'); Cache::increment('key', $amount); Cache::decrement('key'); Cache::decrement('key', $amount); ``` ### 取得と削除: `Cache::pull()` 取得後にキャッシュから削除します。ワンタイムデータの管理に便利です。 ```php theme={null} $value = Cache::pull('key'); ``` ### データの削除: `Cache::forget()` ```php theme={null} // 特定のキーを削除 Cache::forget('key'); // キャッシュ全体をクリア Cache::flush(); ``` `Cache::flush()` はキャッシュの「プレフィックス」設定に関係なく、すべてのエントリを削除します。複数のアプリケーションで共有しているキャッシュの場合は注意してください。 `Cache::flushLocks()` を使うとキャッシュ内のすべてのアトミックロックをクリアできます。 ```php theme={null} Cache::flushLocks(); ``` ### TTL の延長: `Cache::touch()` 既存のキャッシュアイテムの有効期限を延長します。 ```php theme={null} // 秒数で指定 Cache::touch('key', 3600); // Carbon インスタンスで指定 Cache::touch('key', now()->addHours(2)); ``` ## キャッシュメモ化 `memo` ドライバを使うと、同一リクエスト内でのキャッシュアクセスをメモリにキャッシュします。 同じキーへの繰り返しアクセスが多い場合に、キャッシュストアへのラウンドトリップを削減できます。 ```php theme={null} use Illuminate\Support\Facades\Cache; // デフォルトストアをメモ化 $value = Cache::memo()->get('key'); // Redis ストアをメモ化 $value = Cache::memo('redis')->get('key'); ``` ```php theme={null} // 最初のアクセスはキャッシュストアにアクセス $value = Cache::memo()->get('key'); // 2回目以降はメモリから返す(ストアにアクセスしない) $value = Cache::memo()->get('key'); ``` ## キャッシュタグ 関連するキャッシュアイテムをタグでグループ化し、まとめて削除できます。 キャッシュタグは `file`、`dynamodb`、`database`、`storage` ドライバでは使用できません。`redis` や `memcached` ドライバが必要です。 ### タグ付きキャッシュの構造 複数のタグを付けたキャッシュアイテムは、いずれかのタグでまとめて削除できます。 ```mermaid theme={null} flowchart TD A["Cache::tags(['people', 'artists'])
    .put('John', $data)"] --> B["John のキャッシュ"] C["Cache::tags(['people', 'authors'])
    .put('Anne', $data)"] --> D["Anne のキャッシュ"] B --> T1["タグ: people"] B --> T2["タグ: artists"] D --> T1 D --> T3["タグ: authors"] T1 -->|"flush()"| E["John と Anne を削除"] T2 -->|"flush()"| F["John のみ削除"] T3 -->|"flush()"| G["Anne のみ削除"] ``` ### タグ付きキャッシュの保存と取得 ```php theme={null} use Illuminate\Support\Facades\Cache; // タグ付きで保存 Cache::tags(['people', 'artists'])->put('John', $john, $seconds); Cache::tags(['people', 'authors'])->put('Anne', $anne, $seconds); // タグを指定して取得 $john = Cache::tags(['people', 'artists'])->get('John'); $anne = Cache::tags(['people', 'authors'])->get('Anne'); ``` ### タグ付きキャッシュの削除 ```php theme={null} // 'people' と 'authors' タグを持つすべてのキャッシュを削除(JohnもAnneも削除) Cache::tags(['people', 'authors'])->flush(); // 'authors' タグのみ削除(Anneだけ削除。Johnは残る) Cache::tags('authors')->flush(); ``` ユーザーごとや記事ごとなど、グループ単位でキャッシュを無効化したい場合にタグが役立ちます。 例: `Cache::tags(['user', "user:{$userId}"])->flush()` でそのユーザーに関するすべてのキャッシュをクリアできます。 ## アトミック操作(ロック) `Cache::lock()` を使うと分散ロックを実装でき、複数のプロセスや並列リクエストからの競合状態を防ぎます。 この機能は `memcached`、`redis`、`dynamodb`、`database`、`file`、`array` キャッシュドライバで利用できます。すべてのサーバーが同じ中央キャッシュサーバーと通信している必要があります。 ### 基本的なロック ```php theme={null} use Illuminate\Support\Facades\Cache; $lock = Cache::lock('foo', 10); if ($lock->get()) { // ロック取得成功(10秒間有効) // 何らかの排他処理を行う $lock->release(); } ``` クロージャを渡すと、処理完了後に自動でロックを解放します。 ```php theme={null} Cache::lock('foo', 10)->get(function () { // ロックを自動解放 }); ``` ### ロック待機 ロックが取得できるまで最大で指定秒数待機します。タイムアウトすると `LockTimeoutException` が投げられます。 ```php theme={null} use Illuminate\Contracts\Cache\LockTimeoutException; $lock = Cache::lock('foo', 10); try { $lock->block(5); // 最大5秒待機 // ロック取得後の処理 } catch (LockTimeoutException $e) { // ロック取得失敗 } finally { $lock->release(); } ``` クロージャを使うとシンプルに書けます。 ```php theme={null} Cache::lock('foo', 10)->block(5, function () { // 最大5秒待機してロックを取得し、完了後に自動解放 }); ``` ### 重複実行の防止: `withoutOverlapping()` 同一処理の多重実行を防ぐシンプルな方法です。 ```php theme={null} Cache::withoutOverlapping('foo', function () { // 同時に1つだけ実行される }); // 待機時間とロック保持時間をカスタマイズ Cache::withoutOverlapping('foo', function () { // ... }, lockFor: 120, waitFor: 5); ``` ### 同時実行数の制限: `funnel()` 同時に実行できる処理数を制限します。 ```php theme={null} Cache::funnel('foo') ->limit(3) // 最大3つの並列実行を許可 ->releaseAfter(60) // 60秒後に自動解放 ->block(10) // 最大10秒待機 ->then(function () { // 同時実行枠を確保できた場合の処理 }, function () { // 枠を確保できなかった場合の処理 }); ``` ### プロセス間でのロック引き渡し Webリクエストでロックを取得し、キュージョブで解放するケースです。 ```php theme={null} // リクエストでロックを取得してジョブをディスパッチ $lock = Cache::lock('processing', 120); if ($lock->get()) { ProcessPodcast::dispatch($podcast, $lock->owner()); } // キュージョブ内でロックを解放 Cache::restoreLock('processing', $this->owner)->release(); ``` 現在のオーナーに関係なくロックを強制解放するには `forceRelease` メソッドを使います。 ```php theme={null} Cache::lock('processing')->forceRelease(); ``` ### ロックのリフレッシュ 現在保持しているロックの有効期限を延長するには `refresh` メソッドを使います。秒数を指定しない場合はロック取得時の元の有効期限が使われます。長時間処理で短いロックを定期的に延長したい場合に便利で、最初から非常に長い有効期限を設定する必要がなくなります。 ```php theme={null} $lock = Cache::lock('generate-reports', 60); if ($lock->get()) { foreach ($reports as $report) { $report->generate(); // ロックをさらに60秒延長する $lock->refresh(); } $lock->release(); } ``` ## キャッシュヘルパー `cache()` ヘルパー関数を使うと、`Cache` ファサードと同じ操作をよりシンプルに書けます。 ```php theme={null} // 値を取得 $value = cache('key'); // 値を保存(有効期限付き) cache(['key' => 'value'], $seconds); cache(['key' => 'value'], now()->addMinutes(10)); // 引数なしでファサードと同じインスタンスを取得 cache()->remember('users', $seconds, function () { return DB::table('users')->get(); }); ``` ## 実践例 ### データベースクエリ結果のキャッシュ ```php theme={null} use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; public function index(): array { $users = Cache::remember('all-users', 3600, function () { return DB::table('users')->orderBy('name')->get(); }); return compact('users'); } ``` ```php theme={null} public function store(Request $request): RedirectResponse { User::create($request->validated()); // キャッシュを削除して次回アクセス時に再取得 Cache::forget('all-users'); return redirect()->route('users.index'); } ``` ### Eloquent モデルのキャッシュ ```php theme={null} use App\Models\Product; use Illuminate\Support\Facades\Cache; // カテゴリ別商品一覧を1時間キャッシュ public function byCategory(int $categoryId): array { $products = Cache::remember( "products:category:{$categoryId}", 3600, fn () => Product::where('category_id', $categoryId) ->where('is_active', true) ->orderBy('name') ->get() ); return compact('products'); } ``` ### APIレスポンスのキャッシュ ```php theme={null} use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; public function getWeather(string $city): array { return Cache::remember( "weather:{$city}", 1800, // 30分間キャッシュ function () use ($city) { $response = Http::get('https://api.weather.example.com/current', [ 'city' => $city, ]); return $response->json(); } ); } ``` ### キャッシュタグを使ったグループ管理 ```php theme={null} use Illuminate\Support\Facades\Cache; class ArticleController extends Controller { public function show(Article $article): array { $data = Cache::tags(['articles', "article:{$article->id}"]) ->remember("article:{$article->id}:detail", 3600, function () use ($article) { return $article->load(['author', 'tags', 'comments']); }); return compact('data'); } public function update(Request $request, Article $article): RedirectResponse { $article->update($request->validated()); // この記事に関するすべてのキャッシュをクリア Cache::tags(["article:{$article->id}"])->flush(); return redirect()->route('articles.show', $article); } } ``` ## まとめ | メソッド | 説明 | | - | - | | `Cache::get('key')` | キャッシュを取得する | | `Cache::put('key', $value, $ttl)` | キャッシュを保存する | | `Cache::remember('key', $ttl, fn)` | 取得または保存する(最重要) | | `Cache::forget('key')` | キャッシュを削除する | | `Cache::has('key')` | キャッシュの存在確認 | | `Cache::flush()` | すべてのキャッシュをクリア | | `Cache::forever('key', $value)` | 永続保存する | | `Cache::pull('key')` | 取得してから削除する | | `Cache::increment('key')` | 数値をインクリメント | | `Cache::tags([...])->flush()` | タグ付きキャッシュをクリア | * **開発・小規模**: `file` または `database` * **本番・高トラフィック**: `redis`(Laravel Horizon と組み合わせると監視も容易) * **AWS 環境**: `dynamodb` または `storage`(S3ディスクを流用する場合) * **テスト**: `array` または `null` * キャッシュキーはアプリ全体でユニークになるよう命名する(例: `users:1:profile`) * データが更新されたら `Cache::forget()` または `Cache::tags()->flush()` でキャッシュを無効化する * キャッシュの TTL は短すぎず長すぎず、データの更新頻度に合わせて設定する * `Cache::remember()` を使うことでキャッシュのミス時の処理を簡潔に書ける * 本番環境では Redis を使い、フェイルオーバー設定も検討する # Laravel Cashier (Stripe) Source: https://kawax.biz/jp/cashier Laravel Cashier (Stripe) を使って Subscription 課金、1回払い、請求書、Webhook を実装する基本フローを解説します。 ## 概要 Laravel Cashier (Stripe) は、Stripe の課金機能を Laravel から扱うための公式パッケージです。あなたは Subscription の作成・状態確認・キャンセル、1回払い、請求書のダウンロード、Webhook 処理を統一された API で実装できます。 ## インストールと設定 まず Cashier をインストールし、必要なテーブルを作成してください。 ```shell theme={null} composer require laravel/cashier php artisan vendor:publish --tag="cashier-migrations" php artisan migrate ``` 次に、`App\Models\User` などの課金対象モデルに `Billable` トレイトを追加します。 ```php theme={null} createOrGetStripeCustomer(); ``` 明示的に作成するなら `createAsStripeCustomer()` を使います。 ```php theme={null} $stripeCustomer = $user->createAsStripeCustomer(); ``` ## Subscription ### 新規作成 `newSubscription()` と `create()` で Subscription を開始します。 `$paymentMethodId` には Stripe.js などで取得した Payment Method ID を渡します。 ```php theme={null} $user->newSubscription('default', 'price_monthly') ->create($paymentMethodId); ``` `price_monthly` は例です。実装時は Stripe ダッシュボードで作成した実際の Price ID を指定してください。 ### 状態確認 `subscribed()` で有効な Subscription かどうかを確認できます。 ```php theme={null} if ($user->subscribed('default')) { // Active subscription... } ``` ### キャンセルと再開 ```php theme={null} $user->subscription('default')->cancel(); if ($user->subscription('default')->onGracePeriod()) { // The user is on the grace period... } $user->subscription('default')->resume(); ``` ```mermaid theme={null} stateDiagram-v2 [*] --> Active Active --> GracePeriod: cancel() GracePeriod --> Active: resume() GracePeriod --> Canceled: grace period ends Active --> Canceled: cancelNow() ``` ## 1回払い (Charge) `charge()` で 1 回だけ課金できます。金額は通貨の最小単位で渡してください(例: USD なら `100` は `$1.00`)。 ここでも `$paymentMethodId` には Stripe で作成した Payment Method ID を使います。 ```php theme={null} $payment = $user->charge(100, $paymentMethodId); ``` 課金に失敗した場合、`charge()` は例外をスローします。 ## Payment Element Stripe の [Payment Element](https://stripe.com/docs/payments/payment-element) を使うと、カード・Apple Pay・Google Pay・iDEAL など複数の決済手段を統一的に扱えます。 ### Subscription での利用 Setup Intent を作成してビューに渡します。 ```php theme={null} return view('subscribe', [ 'intent' => $user->createSetupIntent() ]); ``` Blade ビューで Payment Element をマウントし、Setup Intent の `client_secret` を渡します。 ```html theme={null}
    ``` Stripe のリダイレクト先 URL には `setup_intent` パラメータが付与されます。これを使って Payment Method ID を取得し、Subscription を作成します。 ```php theme={null} use Illuminate\Http\Request; Route::get('/subscription/complete', function (Request $request) { $setupIntent = $request->user()->findSetupIntent( $request->setup_intent ); $paymentMethod = $setupIntent->payment_method; $request->user() ->newSubscription('default', 'price_xxx') ->create($paymentMethod); return redirect('/dashboard'); })->name('subscription.complete'); ``` ### 1回払いでの利用 1 回払いでは `pay()` で Payment Intent を作成します。Order モデルには `user_id`、`amount`、`status`、`stripe_payment_intent_id` カラムが必要です。 ```php theme={null} use App\Models\Order; use Illuminate\Http\Request; Route::post('/pay', function (Request $request) { $amount = 1000; $payment = $request->user()->pay($amount); $order = Order::create([ 'user_id' => $request->user()->id, 'amount' => $amount, 'status' => 'pending', 'stripe_payment_intent_id' => $payment->id, ]); return view('checkout', [ 'clientSecret' => $payment->client_secret, 'order' => $order, ]); }); ``` Payment Element をマウントして決済を確定します。 ```html theme={null}
    ``` リダイレクト後は `payment_intent` パラメータで Order を検索し、Payment Intent が認証済みユーザーのものであり `succeeded` ステータスであることを確認してから注文を確定します。 ```php theme={null} use App\Models\Order; use Illuminate\Http\Request; Route::get('/payment/complete', function (Request $request) { $order = Order::where('user_id', $request->user()->id) ->where('stripe_payment_intent_id', $request->payment_intent) ->firstOrFail(); $paymentIntent = $request->user() ->stripe() ->paymentIntents ->retrieve($request->payment_intent); if ($paymentIntent->customer === $request->user()->stripe_id && $paymentIntent->status === 'succeeded') { $order->update(['status' => 'paid']); // 注文を確定する処理... } return redirect('/dashboard'); })->name('payment.complete'); ``` ```mermaid theme={null} sequenceDiagram participant User as ユーザー participant App as Laravel
    アプリ participant Stripe User->>App: POST /pay App->>Stripe: pay() で PaymentIntent 作成 Stripe-->>App: client_secret App-->>User: checkout ビュー表示 User->>Stripe: confirmPayment() Stripe-->>User: return_url へリダイレクト User->>App: GET /payment/complete App->>Stripe: PaymentIntent 取得・検証 App->>App: order.status = paid App-->>User: ダッシュボードへリダイレクト ``` ## 請求書 請求書一覧は `invoices()` で取得できます。 ```php theme={null} $invoices = $user->invoices(); ``` PDF ダウンロードには `dompdf/dompdf` をインストールし、`downloadInvoice()` を使います。`$invoiceId` には `invoices()` で取得した請求書 ID を渡してください。 ```shell theme={null} composer require dompdf/dompdf ``` ```php theme={null} return $user->downloadInvoice($invoiceId); ``` ## Webhook の設定 Cashier は Stripe Webhook 用のルートを自動登録し、デフォルトで `/stripe/webhook` を使います。Stripe ダッシュボードにこの URL を設定してください。 Webhook の作成は `cashier:webhook` で実行できます。 ```shell theme={null} php artisan cashier:webhook ``` CSRF 保護から `stripe/*` を除外してください。 ```php theme={null} ->withMiddleware(function (Middleware $middleware): void { $middleware->preventRequestForgery(except: [ 'stripe/*', ]); }) ``` `STRIPE_WEBHOOK_SECRET` を `.env` に設定すると、Cashier の署名検証ミドルウェアで Webhook リクエストを検証できます。 # 並行処理 Source: https://kawax.biz/jp/concurrency Laravel 13のConcurrencyファサードを使って複数の処理を同時実行し、アプリケーションのパフォーマンスを向上させる方法を解説します。 ## 並行処理とは 複数の外部APIへのリクエストやデータベースの集計処理など、互いに依存しない複数の処理を**順番に実行**すると、合計時間はそれぞれの処理時間の合計になります。 これらを**同時に実行**すれば、合計時間は最も時間のかかる処理1件分に短縮できます。 Laravelの `Concurrency` ファサードは、こうした並行実行をシンプルなAPIで実現します。 `Concurrency` ファサードはLaravel 11で導入され、Laravel 13でも引き続き利用できます。 デフォルトドライバは子PHPプロセスを使用するため、追加パッケージなしで動作します。 ## 仕組み `Concurrency` ファサードは、渡されたクロージャをシリアライズして隠しArtisanコマンドに送り、それぞれ別のPHPプロセスとして実行します。 処理が完了すると戻り値をシリアライズして親プロセスに返します。 3つのドライバが用意されています。 | ドライバ | 説明 | | - | - | | `process` | デフォルト。子PHPプロセスを起動して実行する。Webリクエストでも動作する | | `fork` | `spatie/fork` パッケージが必要。プロセスをforkするためCLIのみで動作するが高速 | | `sync` | 並行実行せず順番に実行する。テスト用途に向いている | ## 基本的な使い方 ### Concurrency::run() `run()` メソッドにクロージャの配列を渡すと、それらが並行実行されます。 戻り値は各クロージャの返した値を配列で受け取ります。 ```php theme={null} use Illuminate\Support\Facades\Concurrency; use Illuminate\Support\Facades\DB; [$userCount, $orderCount] = Concurrency::run([ fn () => DB::table('users')->count(), fn () => DB::table('orders')->count(), ]); ``` 配列のデストラクチャリングで各結果を変数に受け取れます。 クロージャの順番と戻り値の順番は一致します。 ### ドライバの指定 特定のドライバを使いたい場合は `driver()` メソッドで指定します。 ```php theme={null} $results = Concurrency::driver('fork')->run([ fn () => fetchFromApiA(), fn () => fetchFromApiB(), ]); ``` デフォルトのドライバを変更するには、設定ファイルを公開して `default` オプションを更新します。 ```shell theme={null} php artisan config:publish concurrency ``` ## forkドライバの使い方 `fork` ドライバは `process` ドライバよりも高速ですが、PHPのCLI環境(Artisanコマンドやキューワーカー)でのみ使用できます。 Webリクエストの中では使用できません。 使用前に `spatie/fork` パッケージをインストールしてください。 ```shell theme={null} composer require spatie/fork ``` `fork` ドライバはWebリクエスト中では動作しません。Artisanコマンドやキューワーカーの中で並行処理を行いたいときに使用してください。 ## 実行結果を受け取らない: Concurrency::defer() 処理の結果には関心がなく、HTTPレスポンスを返した後にバックグラウンドで実行したい場合は `defer()` メソッドを使います。 ```php theme={null} use App\Services\Metrics; use Illuminate\Support\Facades\Concurrency; Concurrency::defer([ fn () => Metrics::report('users'), fn () => Metrics::report('orders'), ]); ``` `defer()` を呼んだ時点ではクロージャは実行されません。 HTTPレスポンスがユーザーに送信された後に、並行して実行されます。 分析データの記録やキャッシュのウォームアップなど、ユーザーを待たせる必要のない処理に `defer()` は最適です。 ## 実践例: 複数の外部APIを同時に呼び出す ECサイトのダッシュボードで、在庫管理API・売上集計API・配送状況APIの3つから情報を取得する例を考えます。 ### 順次実行(改善前) ```php theme={null} use Illuminate\Support\Facades\Http; public function dashboard(): array { // 各リクエストが順番に完了するまで待つ(合計約3秒) $inventory = Http::get('https://api.example.com/inventory')->json(); $sales = Http::get('https://api.example.com/sales')->json(); $shipping = Http::get('https://api.example.com/shipping')->json(); return compact('inventory', 'sales', 'shipping'); } ``` ### 並行実行(改善後) ```php theme={null} use Illuminate\Support\Facades\Concurrency; use Illuminate\Support\Facades\Http; public function dashboard(): array { // 3つのリクエストを同時に実行(最も遅いAPI1件分の時間で完了) [$inventory, $sales, $shipping] = Concurrency::run([ fn () => Http::get('https://api.example.com/inventory')->json(), fn () => Http::get('https://api.example.com/sales')->json(), fn () => Http::get('https://api.example.com/shipping')->json(), ]); return compact('inventory', 'sales', 'shipping'); } ``` 各APIが1秒かかる場合、順次実行では合計3秒かかりますが、並行実行では約1秒で完了します。 ### 複数のデータベース集計を同時実行する ```php theme={null} use Illuminate\Support\Facades\Concurrency; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Cache; public function statistics(): array { [$totalUsers, $activeUsers, $totalOrders, $revenue] = Concurrency::run([ fn () => DB::table('users')->count(), fn () => DB::table('users')->where('active', true)->count(), fn () => DB::table('orders')->count(), fn () => DB::table('orders')->sum('total_amount'), ]); return [ 'total_users' => $totalUsers, 'active_users' => $activeUsers, 'total_orders' => $totalOrders, 'revenue' => $revenue, ]; } ``` データベース集計に `process` ドライバを使う場合、子プロセスごとに新しいデータベース接続が確立されます。 同時実行数が多い場合はデータベースの最大接続数に注意してください。 ## Contextの自動伝播 Laravel 13.31([#61419](https://github.com/laravel/framework/pull/61419))で、`process` ドライバが現在の [Context](/jp/context) を子プロセスへ自動的に伝播するようになりました。 それまでは `Concurrency::run()` や `Concurrency::defer()` で起動した子プロセスに `Context::add()` で設定した値が引き継がれず、ログに出力される `context` が親プロセスと子プロセスで食い違うことがありました。 修正後は、内部的にキュージョブへのコンテキスト引き継ぎと同じ仕組みが使われます。 1. 子プロセスを起動する直前に `Context::dehydrate()` の結果をJSONへシリアライズし、`__LARAVEL_CONTEXT` 環境変数として渡す 2. 子プロセス(隠しArtisanコマンド `invoke-serialized-closure`)が起動すると、`__LARAVEL_CONTEXT` を読み取って `Context` リポジトリへ `hydrate()` する そのため、クロージャの外側で設定したコンテキストを、クロージャの中でそのまま参照できます。 ```php theme={null} use Illuminate\Support\Facades\Concurrency; use Illuminate\Support\Facades\Context; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; Context::add('request_id', (string) Str::uuid()); Concurrency::run([ function () { // 親プロセスで設定した request_id がそのまま取得できる Log::info('inventory fetched', ['request_id' => Context::get('request_id')]); return Http::get('https://api.example.com/inventory')->json(); }, ]); ``` `fork` ドライバは `spatie/fork` によるOSレベルの `fork()` を使うため、子プロセスは親プロセスのメモリ空間をそのままコピーして起動します。 そのためコンテキストは元々自然に共有されており、今回の修正は主に `process` ドライバ(別プロセスを起動する方式)に関するものです。 リクエストIDやユーザーIDなど、ログの相関に使う値を `Context` に載せておけば、`Concurrency::run()` で並行実行した処理のログも同じキーで追跡できます。 詳しい使い方は[コンテキストの共有](/jp/context)を参照してください。 ## テスト時の設定 テスト環境では `sync` ドライバを使うと、クロージャを順番に実行します。 並行実行によるプロセスの立ち上げコストがなく、テストが高速になります。 ```php theme={null} // tests/Feature/DashboardTest.php use Illuminate\Support\Facades\Concurrency; public function test_dashboard_returns_correct_data(): void { Concurrency::fake(); // syncドライバに切り替える $response = $this->get('/dashboard'); $response->assertStatus(200); } ``` または `.env.testing` でデフォルトドライバを変更する方法もあります。 ```ini theme={null} CONCURRENCY_DRIVER=sync ``` ## 注意事項 クロージャはシリアライズされて子プロセスに渡されます。 シリアライズできないオブジェクト(データベース接続、ファイルハンドル、リソースなど)をクロージャの外側からキャプチャすることはできません。 必要なオブジェクトはクロージャの中で新たに生成してください。 ```php theme={null} // NG: 外側のEloquentモデルをキャプチャ $user = User::find(1); Concurrency::run([ fn () => $user->orders()->count(), // シリアライズできない可能性がある ]); // OK: クロージャの中でデータを取得する $userId = 1; Concurrency::run([ fn () => Order::where('user_id', $userId)->count(), ]); ``` `process` ドライバは子プロセスの起動コストがあるため、処理自体が非常に短い(数ミリ秒以下)場合は並行実行しても速くならないことがあります。 HTTPリクエストやデータベース集計など、処理に100ms以上かかるものを並行化するときに効果を発揮します。 `fork` ドライバはPHPのプロセスをforkするため、Webリクエスト中(FPMやApache)では使用できません。 Artisanコマンドやキューワーカーの中でのみ使用してください。 並行実行中に例外が発生すると、`run()` は例外を再スローします。 個々の処理で例外が発生しても他の処理を継続させたい場合は、クロージャの中で `try/catch` を使ってください。 ```php theme={null} Concurrency::run([ function () { try { return Http::get('https://api.example.com/data')->json(); } catch (\Exception $e) { return null; // 失敗時はnullを返す } }, ]); ``` ## 次のステップ 処理をバックグラウンドで非同期実行するキューとジョブの使い方を学ぶ # Context(コンテキスト) Source: https://kawax.biz/jp/context LaravelのContext機能を使ってリクエスト・ジョブ・コマンドをまたいで情報を共有し、ログに自動付与する方法を解説します。 ## Context とは Laravel の Context 機能は、リクエスト・キュージョブ・コマンドの実行をまたいで情報を記録・共有するための仕組みです。 `Illuminate\Support\Facades\Context` ファサードを通じて情報を追加すると、その情報はアプリケーションが書き出すすべてのログエントリに自動的に付与されます。 これにより、個別のログ呼び出しに渡した情報と、Context が保持する共有情報を明確に区別できます。 分散システムやキューを使ったアーキテクチャでトレーシングを行う際に特に有用です。 ### コンテキストの伝播フロー ```mermaid theme={null} sequenceDiagram participant Client participant Middleware participant Controller participant Queue participant Job Client->>Middleware: HTTP リクエスト Middleware->>Middleware: Context::add('trace_id', uuid) Middleware->>Controller: next($request) Controller->>Controller: Log::info(...) — trace_id 自動付与 Controller->>Queue: ProcessPodcast::dispatch() Queue->>Job: コンテキストをシリアライズして送信 Job->>Job: コンテキストを復元 (Hydrate) Job->>Job: Log::info(...) — trace_id 自動付与 ``` ## 基本的な使い方 最も典型的な使い方はミドルウェアで `trace_id` を設定することです。以降のすべてのログエントリに自動的に含まれます。 ```shell theme={null} php artisan make:middleware AddContext ``` ```php theme={null} url()); Context::add('trace_id', Str::uuid()->toString()); return $next($request); } } ``` `bootstrap/app.php` でグローバルミドルウェアとして登録します。 ```php theme={null} ->withMiddleware(function (Middleware $middleware) { $middleware->append(\App\Http\Middleware\AddContext::class); }) ``` この設定後、コントローラーやサービスで書き込むログには `url` と `trace_id` が自動的に付与されます。 ```php theme={null} Log::info('User authenticated.', ['auth_id' => Auth::id()]); ``` ```text theme={null} User authenticated. {"auth_id":27} {"url":"https://example.com/login","trace_id":"e04e1a11-e75c-4db3-b5b5-cfef4ef56697"} ``` ## コンテキストへの書き込み ### add — 値を追加する ```php theme={null} use Illuminate\Support\Facades\Context; Context::add('key', 'value'); // 複数まとめて追加 Context::add([ 'first_key' => 'value', 'second_key' => 'value', ]); ``` `add` は既存のキーを上書きします。キーが存在しない場合のみ追加したいときは `addIf` を使います。 ```php theme={null} Context::add('key', 'first'); Context::addIf('key', 'second'); Context::get('key'); // "first" — 上書きされない ``` ### increment / decrement — カウンターを管理する 数値を増減させる専用メソッドです。第2引数で変化量を指定できます。 ```php theme={null} Context::increment('records_added'); Context::increment('records_added', 5); Context::decrement('records_added'); Context::decrement('records_added', 5); ``` ### when — 条件付きで追加する `when` メソッドを使うと条件が `true` のとき・`false` のときそれぞれで異なるデータを追加できます。 ```php theme={null} use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Context; Context::when( Auth::user()->isAdmin(), fn ($context) => $context->add('permissions', Auth::user()->permissions), fn ($context) => $context->add('permissions', []), ); ``` ### push — スタックに追加する Context はリスト形式のデータを保持する「スタック」をサポートしています。 `push` を使うと追加した順序でデータが積み重なります。 ```php theme={null} Context::push('breadcrumbs', 'first_value'); Context::push('breadcrumbs', 'second_value', 'third_value'); Context::get('breadcrumbs'); // ['first_value', 'second_value', 'third_value'] ``` クエリの実行履歴をスタックで記録する例です。 ```php theme={null} use Illuminate\Support\Facades\Context; use Illuminate\Support\Facades\DB; // AppServiceProvider.php の boot メソッドで登録 DB::listen(function ($event) { Context::push('queries', [$event->time, $event->sql]); }); ``` ## コンテキストの取得 ### get / all ```php theme={null} $value = Context::get('key'); // すべて取得 $data = Context::all(); ``` ### only / except — 一部だけ取得する ```php theme={null} $data = Context::only(['first_key', 'second_key']); $data = Context::except(['first_key']); ``` ### pull / pop — 取得して削除する `pull` はキーの値を取得すると同時にコンテキストから削除します。 ```php theme={null} $value = Context::pull('key'); ``` スタックから最後の値を取り出すには `pop` を使います。 ```php theme={null} Context::push('breadcrumbs', 'first_value', 'second_value'); Context::pop('breadcrumbs'); // 'second_value' Context::get('breadcrumbs'); // ['first_value'] ``` ### remember — 存在しなければ設定して返す ```php theme={null} $permissions = Context::remember( 'user-permissions', fn () => $user->permissions, ); ``` ### has / missing — キーの存在確認 ```php theme={null} if (Context::has('key')) { // ... } if (Context::missing('key')) { // ... } ``` `has` は `null` が格納されていても `true` を返します。キーが登録されているかどうかだけを確認します。 ## コンテキストの削除 `forget` でキーを削除します。 ```php theme={null} Context::add(['first_key' => 1, 'second_key' => 2]); Context::forget('first_key'); Context::all(); // ['second_key' => 2] // 複数まとめて削除 Context::forget(['first_key', 'second_key']); ``` ## スコープ付きコンテキスト `scope` メソッドを使うと、クロージャの実行中だけコンテキストを一時的に変更し、実行後に元の状態へ自動的に戻せます。 テストや局所的な処理で一時的な追加情報をログに含めたいときに便利です。 ```php theme={null} use Illuminate\Support\Facades\Context; use Illuminate\Support\Facades\Log; Context::add('trace_id', 'abc-999'); Context::addHidden('user_id', 123); Context::scope( function () { Context::add('action', 'adding_friend'); $userId = Context::getHidden('user_id'); Log::debug("Adding user [{$userId}] to friends list."); // Adding user [987] to friends list. {"trace_id":"abc-999","user_name":"taylor_otwell","action":"adding_friend"} }, data: ['user_name' => 'taylor_otwell'], hidden: ['user_id' => 987], ); // スコープ終了後、元の値に戻っている Context::all(); // ['trace_id' => 'abc-999'] Context::allHidden(); // ['user_id' => 123] ``` スコープ内でオブジェクトを変更した場合、その変更はスコープの外にも反映されます。プリミティブ値を使う場合は問題ありません。 ## Hidden Context ログに出力したくないデータ(パスワード・APIキー・個人識別情報など)は Hidden Context に格納します。 通常の `get` メソッドでは取得できず、`getHidden` などの専用メソッドでのみアクセスできます。 ```php theme={null} use Illuminate\Support\Facades\Context; Context::addHidden('key', 'value'); Context::getHidden('key'); // 'value' Context::get('key'); // null — 通常の get では取得できない ``` Hidden Context は通常のコンテキストと同じメソッド群を持ちます。 ```php theme={null} Context::addHidden(/* ... */); Context::addHiddenIf(/* ... */); Context::pushHidden(/* ... */); Context::getHidden(/* ... */); Context::pullHidden(/* ... */); Context::popHidden(/* ... */); Context::onlyHidden(/* ... */); Context::exceptHidden(/* ... */); Context::allHidden(/* ... */); Context::hasHidden(/* ... */); Context::missingHidden(/* ... */); Context::forgetHidden(/* ... */); ``` ## キュージョブへの引き継ぎ ジョブをキューにディスパッチすると、現在のコンテキストは自動的にシリアライズされてジョブのペイロードに含まれます。 ジョブ実行時に元のコンテキストが復元されるため、リクエストで付与した `trace_id` がキュー上のログにも自動的に引き継がれます。 ```php theme={null} // ミドルウェアで設定 Context::add('trace_id', Str::uuid()->toString()); // コントローラーでジョブをディスパッチ ProcessPodcast::dispatch($podcast); ``` ```php theme={null} class ProcessPodcast implements ShouldQueue { use Queueable; public function handle(): void { Log::info('Processing podcast.', ['podcast_id' => $this->podcast->id]); } } ``` ```text theme={null} Processing podcast. {"podcast_id":95} {"url":"https://example.com/login","trace_id":"e04e1a11-e75c-4db3-b5b5-cfef4ef56697"} ``` リクエスト時の `trace_id` がキュー上のログにも含まれることが確認できます。 ### Dehydrating — ジョブ送信時のカスタマイズ `Context::dehydrating` を使うと、ジョブ送信の直前にコンテキストを加工できます。 たとえば、`Accept-Language` ヘッダーで決まるロケールをキューに渡したい場合に使います。 ```php theme={null} // AppServiceProvider.php use Illuminate\Log\Context\Repository; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Context; public function boot(): void { Context::dehydrating(function (Repository $context) { $context->addHidden('locale', Config::get('app.locale')); }); } ``` `dehydrating` コールバック内では `Context` ファサードを使わず、コールバックに渡された `$context` リポジトリだけを操作してください。 ファサードを使うと現在のプロセスのコンテキストを変更してしまいます。 ### Hydrated — ジョブ実行時の復元 `Context::hydrated` を使うと、ジョブ実行の直前にコンテキストが復元されたタイミングで処理を追加できます。 たとえば、保存していたロケールを設定ファイルに反映させます。 ```php theme={null} // AppServiceProvider.php use Illuminate\Log\Context\Repository; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Context; public function boot(): void { Context::hydrated(function (Repository $context) { if ($context->hasHidden('locale')) { Config::set('app.locale', $context->getHidden('locale')); } }); } ``` `hydrated` コールバック内でも `Context` ファサードは使わず、渡された `$context` リポジトリだけを操作してください。 ## まとめ | | `Context` | `Log::withContext` | | - | - | - | | 対象 | すべてのログチャンネル | 特定のチャンネルのみ | | キュージョブへの引き継ぎ | 自動(Dehydrate/Hydrate) | なし | | 並行処理(`Concurrency`)への引き継ぎ | 自動(Dehydrate/Hydrate、Laravel 13.31〜) | なし | | Hidden データ | サポート | なし | | 用途 | トレーシング・分散システム | チャンネル固有のメタデータ | Hidden Context はログに出力されないため、以下のようなデータを安全に格納できます。 * セッション ID やユーザー ID(ログに残したくない場合) * API キーや認証トークン * ロケールや設定値(キューに引き継ぎたいが、ログには不要) * 内部的なフラグや状態 1. **ロケールの引き継ぎ**: `dehydrating` で `app.locale` を Hidden Context に保存し、`hydrated` で `Config::set` して復元。 2. **認証情報の伝播**: リクエストで認証したユーザーの情報をキュージョブでも参照できるようにする。 3. **テナント ID**: マルチテナントアプリでテナント識別子をキューをまたいで共有する。 Laravel 13.31([#61419](https://github.com/laravel/framework/pull/61419))以降、`Concurrency::run()` / `Concurrency::defer()` の `process` ドライバでも、キュージョブと同じDehydrate/Hydrateの仕組みでコンテキストが子プロセスへ自動的に引き継がれます。 詳しくは[並行処理](/jp/concurrency)を参照してください。 # ブラウザテスト(Dusk) Source: https://kawax.biz/jp/dusk Laravel Duskを使ってChromeDriverベースのブラウザE2Eテストを実行する方法 ## Laravel Duskとは Laravel Duskは、ブラウザを実際に操作して画面全体を検証するE2Eテストツールです。\ デフォルトではSeleniumサーバーを別途準備しなくても、ChromeDriverでテストを実行できます。 公式ドキュメントでは、新規プロジェクトではPest 4のブラウザテストも推奨されています。既存プロジェクトでDuskを使っている場合や、DuskのAPIを使いたい場合に本ページを活用してください。 ## インストール まずGoogle Chromeをインストールし、Duskを開発依存として追加します。 ```shell theme={null} composer require laravel/dusk --dev php artisan dusk:install ``` ChromeDriverのバージョンを調整したい場合は、`dusk:chrome-driver` コマンドを使います。 ```shell theme={null} # ローカルChromeに合うバージョンを検出してインストール php artisan dusk:chrome-driver --detect ``` ## 環境設定(`.env.dusk.local`) Dusk専用の環境変数を使うには、プロジェクトルートに `.env.dusk.{environment}` を作成します。\ ローカル環境なら `.env.dusk.local` を使います。 ```ini theme={null} APP_URL=http://127.0.0.1:8000 DB_CONNECTION=mysql DB_DATABASE=app_testing DB_USERNAME=root DB_PASSWORD=root ``` Dusk実行中は `.env` がバックアップされ、`.env.dusk.local` の内容が一時的に適用されます。 ## 基本的なブラウザテスト テストを生成し、`php artisan dusk` で実行します。 ```shell theme={null} php artisan dusk:make LoginTest php artisan dusk ``` 失敗したテストだけを再実行する場合: ```shell theme={null} php artisan dusk:fails ``` ### 基本操作の例 ```php theme={null} create([ 'email' => 'taylor@laravel.com', ]); $this->browse(function (Browser $browser) use ($user) { $browser->visit('/login') ->type('email', $user->email) ->type('password', 'password') ->press('Login') ->assertPathIs('/dashboard') ->assertSee('Dashboard'); }); } } ``` `visit()`、`type()`、`press()`、`assertSee()` はDuskで最もよく使う操作です。 Duskテストでは `RefreshDatabase` は使いません。公式ドキュメントのとおり、`DatabaseMigrations` か `DatabaseTruncation` を使ってテスト間のデータを初期化してください。 ## ページオブジェクトパターン 複雑な画面をテストするときは、`dusk:page` でページオブジェクトを作ると保守しやすくなります。 ```shell theme={null} php artisan dusk:page Login ``` `tests/Browser/Pages/Login.php` で `url()` や `elements()` を定義して、テスト側では再利用します。 ```php theme={null} use Tests\Browser\Pages\Login; $browser->visit(new Login) ->type('@email', 'taylor@laravel.com') ->type('@password', 'password') ->click('@submit') ->assertSee('Dashboard'); ``` ## スクリーンショットとデバッグ 失敗調査ではスクリーンショットとコンソールログの保存が有効です。 ```php theme={null} $browser->screenshot('login-failed'); $browser->responsiveScreenshots('login-page'); $browser->screenshotElement('#login-form', 'login-form'); $browser->storeConsoleLog('login-console'); ``` * スクリーンショット: `tests/Browser/screenshots` * コンソールログ: `tests/Browser/console` ## CI(GitHub Actions)で実行する GitHub Actionsでは、ChromeDriverとLaravelの内蔵サーバーを起動してから `php artisan dusk` を実行します。 ```yaml theme={null} name: Dusk tests on: [push, pull_request] jobs: dusk: runs-on: ubuntu-latest env: APP_URL: http://127.0.0.1:8000 DB_USERNAME: root DB_PASSWORD: root MAIL_MAILER: log steps: - uses: actions/checkout@v5 - run: cp .env.example .env - run: composer install --no-progress --prefer-dist --optimize-autoloader - run: php artisan key:generate - run: php artisan dusk:chrome-driver --detect - run: ./vendor/laravel/dusk/bin/chromedriver-linux --port=9515 & - run: php artisan serve --no-reload & - run: php artisan dusk ``` ## Dusk実行フロー ```mermaid theme={null} flowchart TD A["php artisan dusk"] --> B[".env をバックアップ"] B --> C[".env.dusk.local を適用"] C --> D["ChromeDriver を起動"] D --> E["Browserテストを実行
    visit / type / click / assertSee"] E --> F{"失敗した?"} F -->|Yes| G["スクリーンショットとログを保存"] F -->|No| H["テスト完了"] G --> I[".env を復元"] H --> I ``` ## 公式ドキュメント Duskの全API(待機、アサーション、キーボード操作、iframe操作など)は公式ドキュメントで確認してください。 # Envoy Source: https://kawax.biz/jp/envoy Laravel Envoy を使ってリモートサーバー上のデプロイタスクやArtisanコマンドをBlade構文で定義・実行する方法を解説します。 ## はじめに [Laravel Envoy](https://github.com/laravel/envoy) は、リモートサーバー上でよく使うタスクを実行するためのツールです。[Blade](/jp/blade) と同じ構文を使って、デプロイやArtisanコマンドの実行などのタスクを簡単に定義できます。 Envoyは現在、macOSとLinuxのみをサポートしています。Windowsで使う場合は[WSL2](https://docs.microsoft.com/ja-jp/windows/wsl/install-win10)経由での利用が必要です。 ## インストール Composerパッケージマネージャーを使って、プロジェクトにEnvoyをインストールします。 ```shell theme={null} composer require laravel/envoy --dev ``` インストール後、`vendor/bin`ディレクトリにEnvoyの実行ファイルが利用可能になります。 ```shell theme={null} php vendor/bin/envoy ``` ## タスクの作成 ### タスクの定義 タスクはEnvoyの基本的な構成要素です。タスクは、タスク実行時にリモートサーバー上で実行すべきシェルコマンドを定義します。たとえば、すべてのキューワーカーサーバーで `php artisan queue:restart` コマンドを実行するタスクを定義できます。 Envoyタスクはすべて、アプリケーションのルートにある `Envoy.blade.php` ファイルに定義します。 ```blade theme={null} @servers(['web' => ['user@192.168.1.1'], 'workers' => ['user@192.168.1.2']]) @task('restart-queues', ['on' => 'workers']) cd /home/user/example.com php artisan queue:restart @endtask ``` 上記のように、ファイルの先頭で `@servers` の配列を定義し、各タスク宣言の `on` オプションでこれらのサーバーを参照します。`@servers` 宣言は必ず1行で記述してください。`@task` 宣言の中には、タスク実行時にサーバー上で実行するシェルコマンドを記述します。 #### ローカルタスク サーバーのIPアドレスを `127.0.0.1` に指定することで、スクリプトをローカルコンピューター上で強制的に実行できます。 ```blade theme={null} @servers(['localhost' => '127.0.0.1']) ``` #### Envoyタスクのインポート `@import` ディレクティブを使うと、他のEnvoyファイルをインポートして、そのストーリーやタスクを自分のファイルに追加できます。インポート後は、自身のEnvoyファイルで定義したのと同じようにタスクを実行できます。 ```blade theme={null} @import('vendor/package/Envoy.blade.php') ``` ### 複数サーバー Envoyでは、1つのタスクを複数のサーバーで簡単に実行できます。まず `@servers` 宣言に追加のサーバーを登録し、各サーバーに一意の名前を割り当てます。サーバーを追加したら、タスクの `on` 配列に各サーバーを列挙します。 ```blade theme={null} @servers(['web-1' => '192.168.1.1', 'web-2' => '192.168.1.2']) @task('deploy', ['on' => ['web-1', 'web-2']]) cd /home/user/example.com git pull origin {{ $branch }} php artisan migrate --force @endtask ``` #### 並列実行 デフォルトでは、タスクは各サーバーで直列に実行されます。つまり、あるタスクが1台目のサーバーで完了してから、2台目のサーバーでの実行に進みます。複数サーバーでタスクを並列実行したい場合は、タスク宣言に `parallel` オプションを追加します。 ```blade theme={null} @servers(['web-1' => '192.168.1.1', 'web-2' => '192.168.1.2']) @task('deploy', ['on' => ['web-1', 'web-2'], 'parallel' => true]) cd /home/user/example.com git pull origin {{ $branch }} php artisan migrate --force @endtask ``` ### セットアップ Envoyタスクを実行する前に、任意のPHPコードを実行する必要がある場合は `@setup` ディレクティブを使います。 ```php theme={null} @setup $now = new DateTime; @endsetup ``` タスク実行前に他のPHPファイルを読み込む必要がある場合は、`Envoy.blade.php` ファイルの先頭で `@include` ディレクティブを使います。 ```blade theme={null} @include('vendor/autoload.php') @task('restart-queues') # ... @endtask ``` ### 変数 Envoyタスクを呼び出す際に、コマンドラインで引数を指定してタスクに渡すことができます。 ```shell theme={null} php vendor/bin/envoy run deploy --branch=master ``` タスク内では、Bladeの「echo」構文でオプションにアクセスできます。Bladeの `if` 文やループも定義可能です。たとえば `git pull` コマンドを実行する前に `$branch` 変数の存在を確認する場合は次のようにします。 ```blade theme={null} @servers(['web' => ['user@192.168.1.1']]) @task('deploy', ['on' => 'web']) cd /home/user/example.com @if ($branch) git pull origin {{ $branch }} @endif php artisan migrate --force @endtask ``` ### ストーリー ストーリーは、一連のタスクを1つの便利な名前でグループ化します。たとえば `deploy` ストーリーで、`update-code` タスクと `install-dependencies` タスクをまとめて実行できます。 ```blade theme={null} @servers(['web' => ['user@192.168.1.1']]) @story('deploy') update-code install-dependencies @endstory @task('update-code') cd /home/user/example.com git pull origin master @endtask @task('install-dependencies') cd /home/user/example.com composer install @endtask ``` ストーリーを記述したら、タスクと同じ方法で呼び出せます。 ```shell theme={null} php vendor/bin/envoy run deploy ``` ### 完了フック タスクやストーリーの実行時には、複数のフックが実行されます。Envoyがサポートするフックの種類は `@before`、`@after`、`@error`、`@success`、`@finished` です。これらのフック内のコードはすべてPHPとして解釈され、タスクが操作するリモートサーバーではなく、**ローカルで**実行されます。 各フックはいくつでも定義でき、Envoyスクリプトに記述された順序で実行されます。 ```blade theme={null} @before if ($task === 'deploy') { // ... } @endbefore @after if ($task === 'deploy') { // ... } @endafter @error if ($task === 'deploy') { // ... } @enderror @success // ... @endsuccess @finished if ($exitCode > 0) { // いずれかのタスクでエラーが発生した... } @endfinished ``` `@finished` フックは、実行が完了したタスクのステータスコードを受け取ります。ステータスコードは `null` または `0` 以上の整数です。 ## タスクの実行 アプリケーションの `Envoy.blade.php` ファイルに定義したタスクやストーリーを実行するには、Envoyの `run` コマンドを実行し、実行したいタスクまたはストーリーの名前を渡します。Envoyはタスクを実行し、実行中にリモートサーバーからの出力を表示します。 ```shell theme={null} php vendor/bin/envoy run deploy ``` ### タスク実行の確認 サーバー上で特定のタスクを実行する前に確認を求めたい場合は、タスク宣言に `confirm` ディレクティブを追加します。この機能は破壊的な操作を実行する場合に特に便利です。 ```blade theme={null} @task('deploy', ['on' => 'web', 'confirm' => true]) cd /home/user/example.com git pull origin {{ $branch }} php artisan migrate @endtask ``` ## 通知 ### Slack Envoyは、各タスク実行後に[Slack](https://slack.com)へ通知を送信できます。`@slack` ディレクティブは、SlackのWebhook URLとチャンネル名(またはユーザー名)を受け取ります。Webhook URLは、Slackのコントロールパネルで「Incoming WebHooks」インテグレーションを作成することで取得できます。 `@slack` ディレクティブの第1引数にはWebhook URL全体を渡します。第2引数には、チャンネル名(`#channel`)またはユーザー名(`@user`)を指定します。 ```blade theme={null} @finished @slack('webhook-url', '#bots') @endfinished ``` デフォルトでは、実行されたタスクを説明するメッセージが通知チャンネルに送信されます。`@slack` ディレクティブに第3引数を渡すことで、独自のメッセージに上書きできます。 ```blade theme={null} @finished @slack('webhook-url', '#bots', 'Hello, Slack.') @endfinished ``` ### Discord Envoyは、各タスク実行後に[Discord](https://discord.com)へ通知を送信することもサポートしています。`@discord` ディレクティブはDiscordのWebhook URLとメッセージを受け取ります。Webhook URLは、サーバー設定内で「Webhook」を作成し、投稿先チャンネルを選択することで取得できます。 ```blade theme={null} @finished @discord('discord-webhook-url') @endfinished ``` ### Telegram Envoyは、各タスク実行後に[Telegram](https://telegram.org)へ通知を送信することもサポートしています。`@telegram` ディレクティブは、Telegram Bot IDとChat IDを受け取ります。Bot IDは[BotFather](https://t.me/botfather)で新しいボットを作成することで取得できます。Chat IDは[@username\_to\_id\_bot](https://t.me/username_to_id_bot)を使って取得できます。 ```blade theme={null} @finished @telegram('bot-id','chat-id') @endfinished ``` ### Microsoft Teams Envoyは、各タスク実行後に[Microsoft Teams](https://www.microsoft.com/microsoft-teams)へ通知を送信することもサポートしています。`@teams` ディレクティブは、Teamsのwebhook(必須)、メッセージ、テーマカラー(success, info, warning, error)、そしてオプションの設定配列を受け取ります。Teamsのwebhookは、[Incoming Webhook](https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook)を作成することで取得できます。Teams APIには他にも多くの設定項目があり、独自のメッセージカードを自由に作成できます。詳細は[Microsoft Teamsのドキュメント](https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using)を参照してください。 ```blade theme={null} @finished @teams('webhook-url') @endfinished ``` Envoyはリモートサーバーへの[SSH](https://ja.wikipedia.org/wiki/Secure_Shell)接続を前提としています。実行環境によっては、鍵認証の設定やサーバーへのアクセス制御を事前に済ませておく必要があります。より高度なデプロイ自動化が必要な場合は、[デプロイメント](/jp/deployment)ガイドや[GitHub Actions](/jp/advanced/github-actions-pinning)との組み合わせも検討してください。 # エラーハンドリング Source: https://kawax.biz/jp/error-handling Laravelの例外処理の仕組みを学びます。例外のレポート・レンダリング・カスタムエラーページ・HTTPエラーレスポンスの生成まで包括的に解説します。 ## 概要 Laravel プロジェクトを作成すると、エラーと例外の処理はあらかじめ設定された状態で用意されています。 カスタマイズは `bootstrap/app.php` の `withExceptions` メソッドで行います。 ### 例外処理フロー 例外が発生してからクライアントにレスポンスが返るまでの流れを示します。 ```mermaid theme={null} flowchart TD A["例外発生"] --> B["ExceptionHandler::report"] B --> C{"ログ対象?"} C -->|"Yes"| D["ログ記録"] C -->|"No"| E["スキップ"] D --> F["ExceptionHandler::render"] E --> F F --> G{"リクエストタイプ"} G -->|"Web"| H["HTMLエラーページ"] G -->|"API/JSON"| I["JSONエラーレスポンス"] H --> J["クライアントへ返す"] I --> J ``` ```php theme={null} // bootstrap/app.php use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; return Application::configure(basePath: dirname(__DIR__)) ->withExceptions(function (Exceptions $exceptions): void { // 例外の報告・レンダリングをここで設定する })->create(); ``` `withExceptions` クロージャに渡される `$exceptions` オブジェクトは `Illuminate\Foundation\Configuration\Exceptions` のインスタンスで、アプリケーション全体の例外ハンドリングを管理します。 ### デバッグ設定 `config/app.php` の `debug` オプションがエラー情報の表示量を制御します。 デフォルトでは `.env` の `APP_DEBUG` 環境変数の値が使われます。 ```ini theme={null} # ローカル開発 APP_DEBUG=true # 本番環境 APP_DEBUG=false ``` 本番環境では `APP_DEBUG` を必ず `false` にしてください。`true` のままにすると、機密情報がエンドユーザーに露出するリスクがあります。 ## 例外のレポート 例外のレポートとは、例外をログに記録したり [Laravel Nightwatch](https://nightwatch.laravel.com)、[Sentry](https://github.com/getsentry/sentry-laravel)、[Flare](https://flareapp.io) などの外部サービスに送信したりする処理です。 デフォルトでは `config/logging.php` の設定に基づいてログに記録されます。 ### カスタムレポートコールバック 例外の種類によって異なるレポート処理をしたい場合は `report` メソッドにクロージャを渡します。 Laravelはクロージャの型ヒントから例外の種類を判断します。 ```php theme={null} use App\Exceptions\InvalidOrderException; ->withExceptions(function (Exceptions $exceptions): void { $exceptions->report(function (InvalidOrderException $e) { // 外部サービスへの通知など }); }) ``` カスタムコールバックを登録してもデフォルトのログ記録は継続されます。 デフォルトへの伝播を止めたい場合は `stop()` を呼ぶか、`false` を返します。 ```php theme={null} ->withExceptions(function (Exceptions $exceptions): void { $exceptions->report(function (InvalidOrderException $e) { // ... })->stop(); }) ``` ### `report()` ヘルパー エラーページを表示せずに例外だけを報告したい場合は `report()` ヘルパーを使います。 ```php theme={null} public function isValid(string $value): bool { try { // バリデーション処理... } catch (Throwable $e) { report($e); return false; } } ``` `report()` ヘルパーはユーザーへのレスポンスを中断せずにエラーを記録できます。バックグラウンドジョブや非重要な処理の例外処理に便利です。 ### 重複レポートの防止 同じ例外インスタンスが複数回 `report()` に渡されると、ログに重複エントリが作成されることがあります。 `dontReportDuplicates()` を設定すると、同じインスタンスは最初の1回だけ記録されます。 ```php theme={null} ->withExceptions(function (Exceptions $exceptions): void { $exceptions->dontReportDuplicates(); }) ``` ```php theme={null} $original = new RuntimeException('Whoops!'); report($original); // 記録される try { throw $original; } catch (Throwable $caught) { report($caught); // 無視される(同じインスタンス) } ``` ### グローバルログコンテキスト すべての例外ログに共通の情報を付与したい場合は `context` メソッドを使います。 利用可能であれば現在のユーザーIDは自動的に付与されます。 ```php theme={null} ->withExceptions(function (Exceptions $exceptions): void { $exceptions->context(fn () => [ 'app_version' => config('app.version'), ]); }) ``` ### 例外クラスへの `context()` メソッド追加 例外クラス自身に `context()` メソッドを定義すると、その例外に固有のコンテキスト情報をログに含められます。 ```php theme={null} */ public function context(): array { return ['order_id' => $this->orderId]; } } ``` ### ログレベルの変更 特定の例外を特定のログレベルで記録したい場合は `level` メソッドを使います。 ```php theme={null} use PDOException; use Psr\Log\LogLevel; ->withExceptions(function (Exceptions $exceptions): void { $exceptions->level(PDOException::class, LogLevel::CRITICAL); }) ``` ### 例外レポートのスロットリング 大量の例外が発生する場合、`throttle` メソッドでレポート数を制御できます。 ```php theme={null} use Illuminate\Support\Lottery; use Throwable; ->withExceptions(function (Exceptions $exceptions): void { // 1000回に1回だけランダムにレポート $exceptions->throttle(function (Throwable $e) { return Lottery::odds(1, 1000); }); }) ``` 1分あたりの件数で制限したい場合は `Limit` を使います。 ```php theme={null} use Illuminate\Broadcasting\BroadcastException; use Illuminate\Cache\RateLimiting\Limit; use Throwable; ->withExceptions(function (Exceptions $exceptions): void { $exceptions->throttle(function (Throwable $e) { if ($e instanceof BroadcastException) { return Limit::perMinute(300); } }); }) ``` ## 例外のレンダリング レンダリングとは、例外を HTTP レスポンスに変換する処理です。 デフォルトでは Laravel が自動的に適切なレスポンスを生成しますが、カスタマイズも可能です。 ### カスタムレンダリングコールバック `render` メソッドにクロージャを渡して例外をレスポンスに変換します。 ```php theme={null} use App\Exceptions\InvalidOrderException; use Illuminate\Http\Request; ->withExceptions(function (Exceptions $exceptions): void { $exceptions->render(function (InvalidOrderException $e, Request $request) { return response()->view('errors.invalid-order', status: 500); }); }) ``` 組み込みの例外(`NotFoundHttpException` など)のレンダリングも上書きできます。 クロージャが値を返さない場合は、デフォルトのレンダリングが使われます。 ```php theme={null} use Illuminate\Http\Request; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; ->withExceptions(function (Exceptions $exceptions): void { $exceptions->render(function (NotFoundHttpException $e, Request $request) { if ($request->is('api/*')) { return response()->json([ 'message' => 'Record not found.', ], 404); } // null を返すとデフォルトの404ページが表示される }); }) ``` ### JSON / HTML の自動判定 Laravel はリクエストの `Accept` ヘッダーに基づいて HTML と JSON のどちらで返すかを自動判定します。 この判定ロジックをカスタマイズしたい場合は `shouldRenderJsonWhen` を使います。 ```php theme={null} use Illuminate\Http\Request; use Throwable; ->withExceptions(function (Exceptions $exceptions): void { $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) { if ($request->is('admin/*')) { return true; // 管理画面は常にJSONで返す } return $request->expectsJson(); }); }) ``` ### レスポンス全体のカスタマイズ `respond` メソッドを使うと、生成されたレスポンスをさらに加工できます。 ```php theme={null} use Symfony\Component\HttpFoundation\Response; ->withExceptions(function (Exceptions $exceptions): void { $exceptions->respond(function (Response $response) { if ($response->getStatusCode() === 419) { return back()->with([ 'message' => 'ページの有効期限が切れました。もう一度お試しください。', ]); } return $response; }); }) ``` ## カスタム例外クラス `app/Exceptions/` ディレクトリに独自の例外クラスを作成できます。 `report()` メソッドと `render()` メソッドを定義すると、`bootstrap/app.php` に設定を書かなくても自動的に呼ばれます。 ### 例外クラスの作成 ```shell theme={null} php artisan make:exception InvalidOrderException ``` ```php theme={null} view('errors.invalid-order', [ 'orderId' => $this->orderId, ], 422); } } ``` `report()` メソッドには型ヒントで依存性注入が使えます。Laravel のサービスコンテナが自動的に解決します。 ### `ShouldntReport` インターフェース レポート不要な例外には `ShouldntReport` インターフェースを実装します。 このインターフェースを実装した例外は一切レポートされません。 ```php theme={null} B["NotFoundHttpException"] C["abort(403)"] --> D["AccessDeniedHttpException"] E["abort(500)"] --> F["HttpException
    500"] G["abort(422)"] --> H["UnprocessableEntityHttpException"] B --> I["resources/views/errors/404.blade.php"] D --> J["resources/views/errors/403.blade.php"] F --> K["resources/views/errors/500.blade.php"] H --> L["resources/views/errors/422.blade.php"] ``` ```php theme={null} // 404 Not Found abort(404); // メッセージ付き abort(403, 'この操作を行う権限がありません。'); ``` ### `abort_if()` / `abort_unless()` 条件付きで例外をスローするヘルパーです。 ```php theme={null} // $condition が true のときに abort する abort_if(! $user->isAdmin(), 403); // $condition が false のときに abort する abort_unless($user->hasPermission('edit'), 403, 'Permission denied.'); ``` コントローラーやミドルウェアで権限チェックをするときに便利です。ゲートやポリシーと組み合わせて使われることも多いです。 ## 例外のグローバルな制御 ### 特定の例外を無視する 報告しない例外を `dontReport` で指定します。レンダリングのカスタムロジックは引き続き機能します。 ```php theme={null} use App\Exceptions\InvalidOrderException; ->withExceptions(function (Exceptions $exceptions): void { $exceptions->dontReport([ InvalidOrderException::class, ]); }) ``` 条件付きで無視したい場合は `dontReportWhen` にクロージャを渡します。 ```php theme={null} use Throwable; ->withExceptions(function (Exceptions $exceptions): void { $exceptions->dontReportWhen(function (Throwable $e) { return $e instanceof PodcastProcessingException && $e->reason() === 'Subscription expired'; }); }) ``` Laravel はデフォルトで、404エラーや CSRF トークン不正 (419)、オリジン不一致 (403) などの一部の例外を自動的に無視しています。 ### Laravel が無視している例外を有効にする デフォルトで無視されている例外をレポート対象に戻すには `stopIgnoring` を使います。 ```php theme={null} use Symfony\Component\HttpKernel\Exception\HttpException; ->withExceptions(function (Exceptions $exceptions): void { $exceptions->stopIgnoring(HttpException::class); }) ``` ## HTTPエラーページ Laravel では HTTP ステータスコードごとにカスタムエラービューを定義できます。 ### カスタムエラービューの作成 `resources/views/errors/` ディレクトリに、ステータスコードをファイル名としたBladeテンプレートを作成します。 ``` resources/ └── views/ └── errors/ ├── 404.blade.php ├── 403.blade.php └── 500.blade.php ``` ビュー内では `$exception` 変数を使ってエラー情報にアクセスできます。 ```blade theme={null} {{-- resources/views/errors/404.blade.php --}} ページが見つかりません

    404 - ページが見つかりません

    {{ $exception->getMessage() }}

    トップページへ戻る ``` ### デフォルトのエラーテンプレートを公開する Laravel 標準のエラーページをカスタマイズの出発点として使いたい場合は `vendor:publish` で取得します。 ```shell theme={null} php artisan vendor:publish --tag=laravel-errors ``` ### フォールバックエラーページ 特定のステータスコードに対応するビューがない場合のフォールバックとして、`4xx.blade.php` と `5xx.blade.php` を作成できます。 ``` resources/ └── views/ └── errors/ ├── 4xx.blade.php # 400番台のフォールバック └── 5xx.blade.php # 500番台のフォールバック ``` `404`、`500`、`503` については Laravel がデフォルトのエラーページを用意しています。これらをカスタマイズするには、フォールバックではなく個別のファイル(`404.blade.php` など)を作成してください。 ## 実践例: API 例外ハンドラー API を提供するアプリケーションでは、例外を常に JSON で返す必要があります。 以下は `bootstrap/app.php` でAPIエラーを一元管理する実装例です。 ```php theme={null} use Illuminate\Auth\AuthenticationException; use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; ->withExceptions(function (Exceptions $exceptions): void { // API リクエストには常にJSONで返す $exceptions->render(function (NotFoundHttpException $e, Request $request) { if ($request->is('api/*')) { return response()->json([ 'message' => 'リソースが見つかりません。', ], 404); } }); $exceptions->render(function (AuthenticationException $e, Request $request) { if ($request->is('api/*')) { return response()->json([ 'message' => '認証が必要です。', ], 401); } }); $exceptions->render(function (ValidationException $e, Request $request) { if ($request->is('api/*')) { return response()->json([ 'message' => 'バリデーションエラー。', 'errors' => $e->errors(), ], 422); } }); }) ``` ### カスタム API 例外クラスの実装 API 専用の基底例外クラスを作ると、各エンドポイントで統一したエラーレスポンスを返せます。 ```php theme={null} $this->getMessage()]; if (! empty($this->errors)) { $data['errors'] = $this->errors; } return response()->json($data, $this->statusCode); } } ``` コントローラーでの使用例です。 ```php theme={null} isCancelled()) { throw new ApiException('この注文はキャンセル済みです。', 422); } return response()->json($order); } } ``` ## まとめ | 方法 | 用途 | | - | - | | `$exceptions->report()` | 例外の種類別にカスタムレポートロジックを登録 | | `$exceptions->context()` | すべての例外ログに共通情報を付与 | | `context()` メソッド | 例外クラス自身に固有のコンテキストを持たせる | | `report()` ヘルパー | レスポンスを中断せず例外だけを報告 | | `dontReportDuplicates()` | 同じインスタンスの重複レポートを防止 | | `ShouldntReport` インターフェース | 一切レポートしない例外クラスを作成 | | 方法 | 用途 | | - | - | | `$exceptions->render()` | 例外の種類別にカスタムレスポンスを返す | | `render()` メソッド | 例外クラス自身にレンダリングロジックを持たせる | | `shouldRenderJsonWhen()` | JSON/HTML の判定ロジックをカスタマイズ | | `respond()` | 生成されたレスポンスをさらに加工 | * `resources/views/errors/404.blade.php` などのファイルを作るだけで自動的に使われる * `$exception` 変数でエラーの詳細にアクセスできる * `php artisan vendor:publish --tag=laravel-errors` でデフォルトテンプレートを取得できる * `4xx.blade.php` / `5xx.blade.php` でフォールバックページを定義できる * `APP_DEBUG=false` を必ず設定し、スタックトレースをユーザーに見せない * Sentry や Flare などの外部エラー追跡サービスと連携してエラーを一元管理する * `throttle()` を使って大量の例外が発生した際のログ溢れを防ぐ * API エンドポイントでは一貫したJSONエラーレスポンス形式を維持する # イベントとリスナー Source: https://kawax.biz/jp/events Laravelのイベントシステムを使って、アプリケーションのコンポーネントを疎結合に保つ方法を学びます。 ## イベントとは Laravelのイベントシステムは、シンプルなオブザーバーパターンの実装です。 アプリケーション内で起きた出来事(イベント)を発火し、それに反応するリスナーを定義することで、コンポーネント間の依存を最小限に抑えられます。 たとえば「注文が確定した」というイベントを発火すると、「確認メールを送る」「在庫を減らす」「Slackに通知する」といった複数のリスナーがそれぞれ独立して動きます。 注文処理のコードはメール送信やSlack通知の実装を一切知る必要がありません。 イベントクラスは `app/Events` ディレクトリに、リスナークラスは `app/Listeners` ディレクトリに置きます。 どちらも存在しない場合は Artisan コマンドが自動で作成します。 ```mermaid theme={null} flowchart TD A["イベント発火
    dispatch()"] --> B["イベントディスパッチャー"] B --> C["リスナー1
    (同期)"] B --> D["リスナー2
    (同期)"] B --> E["リスナー3
    ShouldQueue実装"] C --> F["即時実行"] D --> G["即時実行"] E --> H["キューに積まれる"] H --> I["ワーカーが非同期実行"] ``` ## イベントとリスナーの生成 `make:event` と `make:listener` Artisan コマンドでクラスのひな型を生成します。 ```bash theme={null} php artisan make:event UserRegistered php artisan make:listener SendWelcomeEmail --event=UserRegistered ``` 引数なしで実行するとインタラクティブに入力を求められます。 ```bash theme={null} php artisan make:event php artisan make:listener ``` ## イベントの登録 ### イベントディスカバリー(自動検出) デフォルトでは、Laravel は `app/Listeners` ディレクトリをスキャンしてリスナーを自動登録します。 `handle` または `__invoke` という名前のメソッドの引数型からイベントとのマッピングを推論します。 ```php theme={null} use App\Events\UserRegistered; class SendWelcomeEmail { public function handle(UserRegistered $event): void { // ウェルカムメールを送信する } } ``` PHP のユニオン型を使うと、複数イベントを一つのメソッドで受け取れます。 ```php theme={null} public function handle(UserRegistered|UserUpdated $event): void { // ... } ``` リスナーを別ディレクトリに置く場合は `bootstrap/app.php` で追加スキャン先を指定します。 ```php theme={null} ->withEvents(discover: [ __DIR__.'/../app/Domain/Orders/Listeners', ]) ``` ワイルドカードを使って複数のディレクトリをまとめて指定できます。 ```php theme={null} ->withEvents(discover: [ __DIR__.'/../app/Domain/*/Listeners', ]) ``` 登録されているリスナーの一覧は次のコマンドで確認できます。 ```bash theme={null} php artisan event:list ``` 本番環境ではリスナーのマニフェストをキャッシュしておくとパフォーマンスが向上します。 デプロイ時に `php artisan optimize` または `php artisan event:cache` を実行してください。 キャッシュを削除するには `php artisan event:clear` を使います。 ### 手動登録 `AppServiceProvider` の `boot` メソッドで `Event` ファサードを使って手動登録することもできます。 ```php theme={null} use App\Events\UserRegistered; use App\Listeners\SendWelcomeEmail; use Illuminate\Support\Facades\Event; public function boot(): void { Event::listen( UserRegistered::class, SendWelcomeEmail::class, ); } ``` クロージャで登録することも可能です。 ```php theme={null} use App\Events\UserRegistered; use Illuminate\Support\Facades\Event; public function boot(): void { Event::listen(function (UserRegistered $event) { // ... }); } ``` ### 匿名リスナーをキューに追加する クロージャで登録するリスナーをキューで実行する場合は、`Illuminate\Events\queueable` でクロージャをラップします。 ```php theme={null} use App\Events\UserRegistered; use Illuminate\Support\Facades\Event; use function Illuminate\Events\queueable; Event::listen(queueable(function (UserRegistered $event) { // ... })); ``` 通常のキューイングされたジョブと同じように、`onConnection()`、`onQueue()`、`delay()` で接続・キュー名・遅延時間を設定できます。 ```php theme={null} Event::listen(queueable(function (UserRegistered $event) { // ... })->onConnection('redis')->onQueue('emails')->delay(now()->addSeconds(10))); ``` キューに追加した匿名リスナーが失敗したときの処理は、`catch()` にクロージャを渡して定義します。クロージャにはイベントと発生した `Throwable` が渡されます。 ```php theme={null} use Throwable; Event::listen(queueable(function (UserRegistered $event) { // ... })->catch(function (UserRegistered $event, Throwable $e) { // キューイングされたリスナーが失敗したときの処理 })); ``` ## イベントの定義 イベントクラスはデータの入れ物です。ロジックは持たず、イベントに関連する情報をプロパティとして保持します。 ```php theme={null} user->email) ->send(new WelcomeMail($event->user)); } } ``` リスナーの `handle` メソッドで `false` を返すと、後続のリスナーへのイベント伝播を止められます。 ## キューイングされたリスナー メール送信や HTTP リクエストなど時間のかかる処理は、キューイングされたリスナーとして非同期に実行できます。 `ShouldQueue` インターフェースを実装するだけで、イベント発火時にリスナーが自動的にキューに積まれます。 ```php theme={null} キューイングされたリスナーを使う前に、キューの設定とワーカーの起動が必要です。 詳しくは[キューとジョブ](/jp/queues)のページを参照してください。 ### イベントリスナーのデバウンス 短時間に同じ対象のイベントが何度も発火する場合、最後のイベントだけを処理したいことがあります。キューイングされたリスナーに `DebounceFor` アトリビュートを付けると、指定した期間内のイベントをまとめられます。 ```php theme={null} product->getKey(); } } ``` この例では、同じ商品の `ProductUpdated` イベントが 30 秒以内に繰り返し発火すると、リスナーはデバウンスされ、最後のイベントだけが処理されます。`debounceId` が異なるイベントは、それぞれ独立して処理されます。 頻繁に発火するイベントがリスナーを先延ばしにできる時間に上限を設けるには、`maxWait` を指定します。 ```php theme={null} #[DebounceFor(30, maxWait: 120)] class UpdateProductSearchIndex implements ShouldQueue { // ... } ``` デバウンスの追跡に使うキャッシュストアは、イベントを受け取る `debounceVia` メソッドで変更できます。 ```php theme={null} use Illuminate\Contracts\Cache\Repository; use Illuminate\Support\Facades\Cache; public function debounceVia(ProductUpdated $event): Repository { return Cache::driver('redis'); } ``` デバウンスされたリスナーと一意なリスナーは同時に使えません。`DebounceFor` アトリビュートを使うリスナーでは `ShouldBeUnique` を実装しないでください。複数のWebサーバーやコンテナからイベントを発火する場合は、すべてのサーバーが同じ共有キャッシュサーバーを使うように設定します。 ### キューの接続・名前・遅延時間をカスタマイズする PHP アトリビュートを使って接続先・キュー名・遅延時間を設定できます。 ```php theme={null} */ public function subscribe(Dispatcher $events): array { return [ Login::class => 'handleLogin', Logout::class => 'handleLogout', ]; } } ``` ### サブスクライバーの登録 イベントディスカバリーが有効な場合、`subscribe` メソッドから配列を返すサブスクライバーは自動登録されます。 手動で登録する場合は `AppServiceProvider` の `boot` メソッドで `Event::subscribe` を呼び出します。 ```php theme={null} use App\Listeners\UserActivitySubscriber; use Illuminate\Support\Facades\Event; public function boot(): void { Event::subscribe(UserActivitySubscriber::class); } ``` ## 実践例: ユーザー登録時にウェルカムメールを送信する ```bash theme={null} php artisan make:event UserRegistered ``` `app/Events/UserRegistered.php` を編集して登録ユーザーを保持するプロパティを追加します。 ```php theme={null} ```bash theme={null} php artisan make:listener SendWelcomeEmail --event=UserRegistered ``` メール送信をキューで非同期に行うため `ShouldQueue` を実装します。 ```php theme={null} user->email) ->send(new WelcomeMail($event->user)); } } ``` ユーザー登録処理の後に `UserRegistered::dispatch()` を呼び出します。 ```php theme={null} validated()); UserRegistered::dispatch($user); return redirect('/dashboard'); } } ``` `RegisterController` は `UserRegistered` イベントを発火するだけで、メール送信の実装を知りません。 将来「登録時にSlack通知も送る」となっても、コントローラには何も変更が不要です。 キューイングされたリスナーを処理するため、ワーカーを起動します。 ```bash theme={null} php artisan queue:work ``` イベントディスカバリーが有効な場合、`AppServiceProvider` への手動登録は不要です。 `app/Listeners` ディレクトリに置かれたリスナーは自動で検出されます。 `php artisan event:list` で登録済みのイベントとリスナーの一覧を確認できます。 想定外のリスナーが登録されていないか定期的にチェックしてください。 # ファイルストレージ Source: https://kawax.biz/jp/filesystem LaravelのFlysystem統合を使ったファイルの読み書き、アップロード、クラウドストレージ連携の方法を解説します。 ## ファイルストレージとは Laravelは [Flysystem](https://github.com/thephpleague/flysystem) というPHPパッケージをベースにした、強力なファイルシステム抽象化レイヤーを提供しています。 ローカルディスク・SFTP・Amazon S3など、異なるストレージバックエンドを同じAPIで操作できるため、環境に応じて切り替えても**コードの変更が不要**です。 ドライバーを切り替えてもコードは変わりません。開発環境ではローカル、本番環境ではS3、という構成が簡単に実現できます。 ## 設定 ファイルシステムの設定は `config/filesystems.php` にまとめられています。 ここで「ディスク」を定義します。ディスクとは、特定のドライバーとストレージ場所の組み合わせです。 主なドライバーは次のとおりです。 | ドライバー | 用途 | | - | - | | `local` | サーバーのローカルファイルシステム | | `public` | Web公開用のローカルディスク | | `s3` | Amazon S3(またはS3互換サービス) | | `sftp` | SFTPサーバー | | `ftp` | FTPサーバー | | `read-through` | 移行中のプライマリ・フォールバックディスク | ### localドライバー `local` ドライバーを使うと、`filesystems` 設定の `root` ディレクトリを基準にファイルを操作します。 デフォルトでは `storage/app/private` が `root` です。 ```php theme={null} use Illuminate\Support\Facades\Storage; Storage::disk('local')->put('example.txt', 'Contents'); // storage/app/private/example.txt に書き込まれる ``` ### デフォルトディスクの変更 `FILESYSTEM_DISK` 環境変数でデフォルトのディスクを切り替えられます。 ```ini theme={null} FILESYSTEM_DISK=s3 ``` ## publicディスクとシンボリックリンク `public` ディスクはWebからアクセスできるファイルを置くためのものです。 デフォルトでは `storage/app/public` ディレクトリに保存されます。 Webサーバーからアクセスできるようにするには、`public/storage` から `storage/app/public` へのシンボリックリンクを作成します。 ```shell theme={null} php artisan storage:link ``` シンボリックリンク作成後、`asset` ヘルパーでURLを生成できます。 ```php theme={null} echo asset('storage/file.txt'); ``` 追加のシンボリックリンクが必要な場合、`config/filesystems.php` の `links` 配列で設定できます。 ```php theme={null} 'links' => [ public_path('storage') => storage_path('app/public'), public_path('images') => storage_path('app/images'), ], ``` シンボリックリンクを削除するには `storage:unlink` を使います。 ```shell theme={null} php artisan storage:unlink ``` ## Storageファサードの基本操作 ### ファイルの読み込み ```php theme={null} use Illuminate\Support\Facades\Storage; // ファイルの内容を文字列で取得 $contents = Storage::get('file.jpg'); // JSONファイルを取得してデコード $data = Storage::json('orders.json'); // ファイルの存在確認 if (Storage::exists('file.jpg')) { // ファイルが存在する } // ファイルが存在しないことを確認 if (Storage::missing('file.jpg')) { // ファイルが存在しない } ``` ### ファイルの書き込み ```php theme={null} // ファイルに書き込む Storage::put('file.jpg', $contents); // リソースをストリームとして書き込む Storage::put('file.jpg', $resource); // 先頭・末尾に追記する Storage::prepend('file.log', 'Prepended Text'); Storage::append('file.log', 'Appended Text'); // ファイルのコピーと移動 Storage::copy('old/file.jpg', 'new/file.jpg'); Storage::move('old/file.jpg', 'new/file.jpg'); ``` `put` などの書き込みが失敗した場合、デフォルトでは `false` を返し、内部の例外はスローも記録もされません。 ディスク設定で `'throw' => true` とすると、`League\Flysystem\UnableToWriteFile` 例外をスローできます。 書き込みに失敗したときに例外をスローせず、アプリケーションの例外ハンドラーで内部例外を記録したい場合は、ディスク設定で `report` を `true` にします。 ```php theme={null} 'public' => [ 'driver' => 'local', // ... 'report' => true, ], ``` `report` は書き込みメソッドの戻り値を変更せず、失敗時に内部例外をログへ記録します。`throw` と `report` のどちらも設定しない場合は、書き込みが失敗しても `false` が返ります。 ### ファイルの削除 ```php theme={null} // 単一ファイルの削除 Storage::delete('file.jpg'); // 複数ファイルの削除 Storage::delete(['file.jpg', 'file2.jpg']); // 特定ディスクのファイルを削除 Storage::disk('s3')->delete('path/file.jpg'); ``` ### ファイルのダウンロードレスポンス ```php theme={null} // ブラウザにダウンロードを促すレスポンスを返す return Storage::download('file.jpg'); // ファイル名とヘッダーを指定する場合 return Storage::download('file.jpg', 'my-file.jpg', $headers); ``` ## URLの生成 ### 通常のURL ```php theme={null} use Illuminate\Support\Facades\Storage; $url = Storage::url('file.jpg'); ``` `local` ドライバーでは `/storage/file.jpg` のような相対URLを返します。 `s3` ドライバーでは完全なリモートURLを返します。 ### 一時URL(Temporary URL) 期限付きのURLを生成したい場合は `temporaryUrl` メソッドを使います。 `local` と `s3` ドライバーで利用できます。 ```php theme={null} use Illuminate\Support\Facades\Storage; $url = Storage::temporaryUrl( 'file.jpg', now()->plus(minutes: 5) ); ``` S3では追加のリクエストパラメータも指定できます。 ```php theme={null} $url = Storage::temporaryUrl( 'file.jpg', now()->plus(minutes: 5), [ 'ResponseContentType' => 'application/octet-stream', 'ResponseContentDisposition' => 'attachment; filename=file.jpg', ] ); ``` 一時アップロードURLが必要な場合は `temporaryUploadUrl` メソッドを使います。 クライアントサイドから直接S3へアップロードするようなサーバーレス構成で活用できます。 ```php theme={null} ['url' => $url, 'headers' => $headers] = Storage::temporaryUploadUrl( 'file.jpg', now()->plus(minutes: 5) ); ``` ## ファイルのアップロード ユーザーがフォームからアップロードしたファイルを保存する一般的なパターンです。 ### storeメソッド(ファイル名を自動生成) ```php theme={null} file('avatar')->store('avatars'); return $path; } } ``` ### storeAsメソッド(ファイル名を指定) ```php theme={null} $path = $request->file('avatar')->storeAs( 'avatars', $request->user()->id ); ``` ### ディスクを指定してアップロード ```php theme={null} // s3ディスクに保存 $path = $request->file('avatar')->store( 'avatars/' . $request->user()->id, 's3' ); ``` ### Storageファサード経由でアップロード ```php theme={null} use Illuminate\Support\Facades\Storage; // ファイル名を自動生成 $path = Storage::putFile('avatars', $request->file('avatar')); // ファイル名を指定 $path = Storage::putFileAs( 'avatars', $request->file('avatar'), $request->user()->id ); ``` `getClientOriginalName()` や `getClientOriginalExtension()` はユーザーが改ざんできるため安全ではありません。 ファイル名には `hashName()`、拡張子には `extension()` を使ってください。 ```php theme={null} $file = $request->file('avatar'); $name = $file->hashName(); // ユニークなランダム名を生成 $extension = $file->extension(); // MIMEタイプから拡張子を判定 ``` ## ファイルの可視性(Visibility) Flysystemでは、ファイルの公開・非公開を `visibility` で管理します。 ```php theme={null} use Illuminate\Support\Facades\Storage; // 書き込み時に可視性を指定 Storage::put('file.jpg', $contents, 'public'); // 可視性の取得と変更 $visibility = Storage::getVisibility('file.jpg'); Storage::setVisibility('file.jpg', 'public'); ``` アップロードしたファイルを公開状態で保存する場合は `storePublicly` を使います。 ```php theme={null} $path = $request->file('avatar')->storePublicly('avatars', 's3'); ``` ## 複数ディスクの使い分け `disk` メソッドで操作するディスクを切り替えられます。 ```php theme={null} // デフォルトディスクに保存 Storage::put('avatars/1', $content); // s3ディスクに保存 Storage::disk('s3')->put('avatars/1', $content); // オンデマンドでディスクを作成 $disk = Storage::build([ 'driver' => 'local', 'root' => '/path/to/root', ]); $disk->put('image.jpg', $content); ``` ## Read-throughディスク `read-through` ディスクを使うと、ダウンタイムなしでファイルを別のディスクへ移行できます。ファイルの読み込み時にまずプライマリディスクを確認し、そこに存在しない場合はフォールバックディスクから読み込んで、以降のリクエストに備えてプライマリディスクへコピーします。 ```php theme={null} 'assets' => [ 'driver' => 'read-through', 'primary' => 's3', 'fallback' => 'legacy-s3', ], ``` 書き込みとディレクトリ一覧の取得はプライマリディスクを対象にします。ファイルの存在確認とメタデータの取得は両方のディスクを確認しますが、ファイルをプライマリディスクへコピーしません。 フォールバックディスクからプライマリディスクへのコピーに失敗しても、デフォルトでは読み込み自体は成功します。コピーの失敗時に例外をスローするには、`throw_on_promotion_failure` を `true` に設定してください。 ## クラウドストレージ(S3)の設定 ### パッケージのインストール ```shell theme={null} composer require league/flysystem-aws-s3-v3 "^3.0" --with-all-dependencies ``` ### 環境変数の設定 `.env` ファイルにS3の認証情報を設定します。 ```ini theme={null} AWS_ACCESS_KEY_ID=your-key-id AWS_SECRET_ACCESS_KEY=your-secret-access-key AWS_DEFAULT_REGION=us-east-1 AWS_BUCKET=your-bucket-name AWS_USE_PATH_STYLE_ENDPOINT=false ``` DigitalOcean Spaces、Cloudflare R2、Vultr Object StorageなどのS3互換サービスも `s3` ドライバーで利用できます。 `endpoint` オプションにサービスのエンドポイントURLを指定してください。 ```php theme={null} 'endpoint' => env('AWS_ENDPOINT', 'https://your-endpoint.example.com'), ``` ## ファイルメタデータの取得 ```php theme={null} use Illuminate\Support\Facades\Storage; // ファイルサイズ(バイト) $size = Storage::size('file.jpg'); // 最終更新日時(UNIXタイムスタンプ) $time = Storage::lastModified('file.jpg'); // MIMEタイプ $mime = Storage::mimeType('file.jpg'); // ファイルパスの取得 $path = Storage::path('file.jpg'); ``` `path` メソッドが返す値はドライバーによって異なります。`local` ドライバーではファイルの絶対パスを返しますが、`s3` ドライバーではS3バケット内の相対パスを返します。 ## ディレクトリ操作 ```php theme={null} use Illuminate\Support\Facades\Storage; // ディレクトリ内のファイル一覧 $files = Storage::files($directory); // サブディレクトリを含むファイル一覧 $files = Storage::allFiles($directory); // ディレクトリ一覧 $directories = Storage::directories($directory); // ディレクトリの作成 Storage::makeDirectory($directory); // ディレクトリとその中のファイルをまとめて削除 Storage::deleteDirectory($directory); ``` ## テスト `Storage::fake()` を使うと、実際のディスクに触れることなくファイル操作のテストが書けます。 ```php theme={null} use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Storage; test('アバターをアップロードできる', function () { Storage::fake('avatars'); $file = UploadedFile::fake()->image('avatar.jpg'); $this->post('/user/avatar', ['avatar' => $file]); // ファイルが保存されたことを確認 Storage::disk('avatars')->assertExists('avatar.jpg'); // ファイルが保存されていないことを確認 Storage::disk('avatars')->assertMissing('other.jpg'); }); ``` `UploadedFile::fake()->image()` を使うには、PHPの [GD拡張](https://www.php.net/manual/ja/book.image.php) が必要です。 ## 実践的なユースケース: プロフィール画像のアップロード バリデーション・保存・DBへのパス記録を組み合わせた実践的なコントローラーの例です。 ```php theme={null} validate([ 'avatar' => ['required', 'image', 'max:2048'], ]); $user = $request->user(); // 既存のアバターを削除 if ($user->avatar_path) { Storage::disk('public')->delete($user->avatar_path); } // 新しいアバターを保存 $path = $request->file('avatar')->store('avatars', 'public'); // パスをDBに保存 $user->update(['avatar_path' => $path]); return back()->with('status', 'avatar-updated'); } } ``` テンプレートでは `Storage::url()` を使ってURLを取得します。 ```blade theme={null} アバター ``` # Laravel Folio Source: https://kawax.biz/jp/folio Laravel Folioでファイルベースルーティングを実装する方法を、インストールからルートモデルバインディングまで解説します。 ## はじめに Laravel Folioは、Bladeファイルを配置するだけでルートを定義できるページベースのルーターです。 従来の `routes/web.php` 中心の定義と違い、ファイルシステムの構造をそのままURLに反映できます。 コンテンツ中心のサイトや、画面ごとに素早くページを追加したい管理画面で特に有効です。 APIのように細かいHTTP制御が必要な領域は、通常のルーティングと併用するのが実践的です。 ## インストール まずはComposerでFolioを追加します。 ```bash theme={null} composer require laravel/folio php artisan folio:install ``` `folio:install` はFolioのサービスプロバイダーを登録します。 デフォルトでは `resources/views/pages/` がページディレクトリになります。 複数のページディレクトリやベースURIを使い分ける場合は、サービスプロバイダーの `boot` で `Folio::path()` と `uri()` を設定します。 ```php theme={null} use Laravel\Folio\Folio; Folio::path(resource_path('views/pages/guest'))->uri('/'); Folio::path(resource_path('views/pages/admin')) ->uri('/admin'); ``` ## ルートの作成 Folioは、マウントされたディレクトリ配下のBladeファイル名からURLを自動生成します。 ```text theme={null} resources/views/pages/schedule.blade.php -> /schedule ``` ```bash theme={null} php artisan folio:list ``` ### ネストされたルート ディレクトリをネストすると、URLも同じ構造でネストされます。 ```bash theme={null} php artisan folio:page user/profile # pages/user/profile.blade.php -> /user/profile ``` ### インデックスルート `index.blade.php` は、そのディレクトリのルートにマッピングされます。 ```bash theme={null} php artisan folio:page index # pages/index.blade.php -> / php artisan folio:page users/index # pages/users/index.blade.php -> /users ``` ## ルートパラメーター ファイル名の `[]` でURLセグメントを受け取れます。 ```bash theme={null} php artisan folio:page "users/[id]" # pages/users/[id].blade.php -> /users/1 ``` ```blade theme={null}
    User {{ $id }}
    ``` 複数セグメントを受け取る場合は `...` を使います。 ```bash theme={null} php artisan folio:page "users/[...ids]" # pages/users/[...ids].blade.php -> /users/1/2/3 ``` ```blade theme={null} @foreach ($ids as $id)
  • User {{ $id }}
  • @endforeach ``` ## ルートモデルバインディング `[User].blade.php` のようにモデル名を使うと、自動でモデル解決されます。 ```bash theme={null} php artisan folio:page "users/[User]" # pages/users/[User].blade.php -> /users/1 ``` ```blade theme={null}
    User {{ $user->id }}
    ``` ソフトデリート済みモデルも扱う場合は、ページ内で `withTrashed()` を呼び出します。 ```php theme={null} `[Post:slug].blade.php` のように書くと、`id` 以外のキー(例: `slug`)でもモデル解決できます。 ## ミドルウェア 特定ページにだけ適用する場合は、ページテンプレート内で `middleware()` を使います。 ```php theme={null} middleware()` を使います。 ```php theme={null} middleware([ 'admin/*' => ['auth', 'verified'], ]); ``` ## 名前付きルート Folioページにも `name()` でルート名を付けられます。 ```php theme={null} `users/[User].blade.php` のような個別ページ側で `name('users.show')` を定義すると、パラメーター付きURLを `route('users.show', ['user' => $user])` で生成できます。 付与したルート名は `route()` ヘルパーでURL生成できます。 ```php theme={null} route('users.index'); route('users.show', ['user' => $user]); ``` ## ファイルとURLの対応 ```mermaid theme={null} graph LR A["pages/index.blade.php"] --> B["/"] C["pages/about.blade.php"] --> D["/about"] E["pages/users/[User].blade.php"] --> F["/users/1"] ``` ## 従来のルーティングとの比較 | 特徴 | 通常ルーティング | Folio | | - | - | - | | ルート定義 | `routes/web.php` | ファイル名で自動 | | コントローラー | 必要(またはクロージャ) | 不要(Bladeに直接) | | 向いているケース | APIやSPA、複雑なHTTP制御 | コンテンツサイト、管理画面 | Folioを使う場合でも、`php artisan route:cache` でルートキャッシュを有効にすると本番性能を最適化できます。 # フロントエンド Source: https://kawax.biz/jp/frontend Laravel 13 で Blade、Livewire、Inertia、React、Vue、Svelte、SPA、Vite をどう組み合わせるかを判断するための入門ガイドです。 ## 概要 Laravelはバックエンドフレームワークですが、フロントエンドまで含めた開発体験も重視しています。\ Laravel 13 では、PHP中心で進める方法と、React・Vue・Svelte などのJavaScriptフレームワークを活用する方法の両方が用意されています。 どのアプローチを選ぶかは、次の3点で考えると整理しやすくなります。 * UIを主に **PHP** で書きたいか * UIを主に **JavaScript / TypeScript** で書きたいか * 1つのリポジトリで完結させたいか、APIとSPAを分離したいか ```mermaid theme={null} flowchart TD A["Laravel アプリケーション"] --> B{"どこでUIを組み立てる?"} A --> G["Vite でアセットをビルド"] B --> C["Blade"] B --> D["Livewire + Alpine.js"] B --> E["Inertia + React / Vue / Svelte"] B --> F["API + 独立したSPA"] C --> C1["サーバーでHTMLを返す"] D --> D1["PHP主導で動的UIを作る"] E --> E1["1つのリポジトリでSPA風の体験を作る"] F --> F1["別フロントエンドやモバイルアプリと連携する"] G --> C1 G --> D1 G --> E1 G --> F1 ``` ## PHPで進める ### Blade [Blade](/jp/blade) は、Laravel標準のテンプレートエンジンです。コントローラーやルートからビューを返し、サーバーで生成したHTMLをブラウザへ返します。 **長所** * Laravelのルーティング、バリデーション、認証と自然に統合できる * PHPだけで完結しやすく、学習コストが低い * SEOや初期表示を重視するページと相性がよい **短所** * ページ遷移やフォーム送信のたびに全面再描画になりやすい * 複雑なインタラクションはJavaScriptの補助が必要になる Bladeは、管理画面、社内ツール、コンテンツ中心の画面、フォーム主体のアプリケーションに向いています。 ### Livewire [Laravel Livewire](https://livewire.laravel.com) は、Bladeをベースにしながら、より動的なUIをPHP中心で実装できるアプローチです。モーダル、検索、インライン編集のような操作を、ReactやVueに近い体験で構築できます。 **長所** * PHPとBladeの延長で動的UIを作れる * Laravelの状態管理やバリデーションと結び付きやすい * 必要な箇所だけJavaScriptを足したい場合は Alpine.js と組み合わせやすい **短所** * 複雑なクライアントサイド状態管理はJavaScriptフレームワークほど得意ではない * フロントエンドの責務が大きい大規模SPAには不向きなことがある Livewireを詳しく知りたい場合は [Livewire入門](/jp/blog/livewire-introduction) を参照してください。 ### PHPファーストのスターターキット Laravel 13 の [スターターキット](/jp/starter-kits) には、PHP中心で**動的UI**まで作りたいチーム向けに **Livewire** ベースの選択肢があります。Bladeだけで始める構成ももちろん可能ですが、スターターキットとして用意されているPHPファーストの選択肢はLivewireです。認証・レイアウト・Vite設定までまとめて用意されるため、最短で開発を始められます。 ## JavaScriptフレームワークを使う ### Inertia + React / Vue / Svelte [Inertia](https://inertiajs.com) は、Laravelのルート・コントローラー・認証を活かしたまま、React・Vue・Svelte でページコンポーネントを構築するための橋渡しです。APIサーバーとSPAを完全分離せず、1つのLaravelアプリケーション内でモダンなUIを作れます。 **長所** * React / Vue / Svelte のコンポーネントモデルを利用できる * ルーティングや認証はLaravel側に寄せられる * バックエンドとフロントエンドを1つのリポジトリで管理しやすい **短所** * Bladeだけの構成より学習対象が増える * Node.jsツールチェーンやTypeScriptの知識が必要になりやすい Laravel 13 の公式スターターキットでは、**React / Vue / Svelte + Inertia** が最初から選べます。 詳しくは [Inertia入門](/jp/blog/inertia-introduction)、[React入門](/jp/blog/react-introduction)、[Vue入門](/jp/blog/vue-introduction)、[Svelte入門](/jp/blog/svelte-introduction) を参照してください。 ### APIと独立したSPAを分ける場合 フロントエンドをLaravelと完全に分離し、別のSPAやモバイルアプリからAPIを利用する構成も選べます。複数クライアントで同じバックエンドを共有したい場合に向いています。 この場合、Laravelは主に次の役割を担います。 * APIルートの提供 * 認証・認可(例: [Sanctum](/jp/sanctum)) * JSONレスポンス整形(例: [Eloquent API Resources](/jp/eloquent-resources)) **長所** * Web、モバイル、デスクトップなど複数クライアントに展開しやすい * フロントエンドの技術選定をLaravel本体から切り離せる **短所** * 認証、データ取得、デプロイを別々に設計する必要がある * 1つのLaravelアプリだけで完結する構成より複雑になりやすい ### JavaScriptファーストのスターターキット [スターターキット](/jp/starter-kits) を使うと、Inertia・React / Vue / Svelte・Tailwind CSS・Vite が組み込まれた状態で始められます。認証画面も最初から揃うため、UI開発に集中しやすくなります。 ## Viteでアセットをまとめる どのアプローチを選んでも、Laravel 13 では通常 [Vite](/jp/vite) を使ってCSSやJavaScriptをビルドします。BladeやLivewireでは軽量なスクリプトやスタイルを、Inertiaではアプリケーション全体のフロントエンドコードをViteでまとめます。 Viteを使うメリットは次のとおりです。 * ローカル開発時の高速なHMR * 本番用アセットのバンドルとバージョニング * Laravelの `@vite()` ディレクティブとの統合 Laravel 13 のスターターキットでは、Viteベースのフロントエンド構成があらかじめ設定されています。 ## アプローチ比較 | アプローチ | 向いているケース | 長所 | 短所 | | - | - | - | - | | Blade | コンテンツ中心のページ、フォーム主体の管理画面 | シンプルで学びやすい。Laravel標準機能と自然に統合できる | 高度なUIではページ全体の再描画が増えやすい | | Livewire | PHP中心で動的UIを作りたい業務アプリ | PHPのまま反応のよいUIを作れる | 複雑なクライアントサイド状態管理はやや苦手 | | Inertia + React / Vue / Svelte | モダンなUIとLaravelの一体運用を両立したいアプリ | 1つのリポジトリでSPA風の体験を構築できる | JavaScript / TypeScript の習熟が必要 | | API + 独立SPA | 複数クライアントやモバイル連携が前提のプロダクト | フロントエンドを独立して展開しやすい | 認証・デプロイ・運用が複雑になりやすい | ## 次のステップ サーバーサイドレンダリングの基本を確認します。 React、Vue、Svelte、Livewire の初期構成を比較します。 開発サーバー、HMR、本番ビルドの流れを学びます。 InertiaでLaravelとJavaScriptフレームワークをつなぐ考え方を確認します。 # Laravel Head Source: https://kawax.biz/jp/head Blade・Livewire・Inertiaでdocument を管理するLaravel Headの使い方。title、meta、Open Graph、canonical URL、robots、パフォーマンスヒント、構造化データを流暢なAPIで設定します。 ## はじめに [laravel/head](https://github.com/laravel/head) は、アプリケーションのドキュメント `` を流暢なAPIで管理するLaravel公式パッケージです。title・meta タグ、Open Graph、canonical URL、robots ディレクティブ、パフォーマンスヒント、構造化データをサポートし、Blade・Livewire・Inertia のいずれでも動作します。2026年7月28日にv0.1.0がリリースされました。 ```bash theme={null} composer require laravel/head ``` ## 解決の優先順位 ページの head データは、優先度の低い順から次の5つの層で解決されます。 1. ページのデフォルト 2. ルートグループのメタデータ 3. ルートのメタデータ 4. ランタイムのメタデータ 5. エラーページのメタデータ 上位の層は下位の層をフィールド単位で上書きします。例えばランタイムで設定した title はルートの title を置き換えますが、description までは置き換えません。 ```mermaid theme={null} graph TD A["ページのデフォルト"] --> B["ルートグループ
    メタデータ"] B --> C["ルートメタデータ"] C --> D["ランタイムメタデータ"] D --> E["エラーページ
    メタデータ"] E --> F["最終的な
    <head> 出力"] ``` ## デフォルトの登録 サービスプロバイダーでサイト全体のデフォルトを登録します。 ```php theme={null} use Laravel\Head\Facades\Head; use Laravel\Head\HeadBuilder; use Laravel\Head\Enums\OgType; Head::defaults(function (HeadBuilder $head) { $head ->title('Acme', suffix: ' - Acme') ->description('Build something great.') ->canonical() ->og(siteName: 'Acme', type: OgType::Website) ->searchableByRobots() ->preconnect('https://fonts.example.com'); }); ``` デフォルト層は最も優先度が低いページ層です。上位の層で title が設定されない限り `Acme` がそのまま表示され、上位層が title を設定すると、継承された suffix が適用されます(`Head::title('About')` は `About - Acme` になります)。 ## ルートメタデータ 静的なページはルート定義に直接メタデータを紐付けられます。 ```php theme={null} Route::view('/contact', 'contact') ->name('contact') ->withHead( title: 'Contact Us', description: 'Get in touch.', ); ``` グループ全体に共通のメタデータを適用することもできます。 ```php theme={null} Route::withHead(robots: 'noindex, nofollow') ->prefix('admin') ->name('admin.') ->group(function () { Route::get('/dashboard', DashboardController::class) ->name('dashboard') ->withHead(title: 'Dashboard'); }); ``` `withHead()` は Laravel 標準のルートメタデータAPI(`->metadata()` の `head` キー配下)を通じてプレーンな配列を保存するため、キャッシュされたルートとの互換性も保たれます。 ## ランタイムメタデータ 投稿タイトルのようにリクエストが来るまで分からない値は、`Head` ファサードで実行時に設定します。 ```php theme={null} use App\Models\Post; use Laravel\Head\Facades\Head; public function show(Post $post) { Head::title($post->title) ->description($post->description); return view('posts.show', ['post' => $post]); } ``` 条件付きのメタデータは `when()` / `unless()` で流暢に記述できます。 ```php theme={null} Head::title($post->title) ->when($post->isDraft(), fn ($head) => $head->hiddenFromRobots()); ``` ## エラーページ ステータスコードごとのメタデータも登録できます。 ```php theme={null} use Laravel\Head\ErrorPages; use Laravel\Head\Facades\Head; Head::errors(function (ErrorPages $errors) { $errors->defaults(robots: 'noindex, follow'); $errors->status(404, title: 'Page Not Found', description: 'The page you are looking for could not be found.', ); }); ``` 登録済みのエラーステータスがレンダリングされる場合、このメタデータは他のどの層よりも優先されます。 ## Open Graph と Twitter Card `og()` で Open Graph プロパティを設定し、`ogImage()` などのメソッドで画像・動画・音声を追加できます。 ```php theme={null} use Laravel\Head\Enums\ImageType; use Laravel\Head\Enums\OgType; Head::og(type: OgType::Article, title: $post->title) ->ogImage($post->hero_image_url) ->ogImage( $post->gallery_image_url, alt: $post->gallery_image_alt, width: 1200, height: 630, type: ImageType::Jpeg, ); ``` document の `title` と `description` は、未設定の `og:title` / `og:description` を自動的に補完します。 Twitter Card はデフォルトに登録しておくだけで、Open Graph と同じ title・description・画像から自動的にレンダリングされます。 ```php theme={null} use Laravel\Head\Enums\TwitterCard; use Laravel\Head\Facades\Head; use Laravel\Head\HeadBuilder; Head::defaults(fn (HeadBuilder $head) => $head->twitter( card: TwitterCard::SummaryWithLargeImage, )); ``` 個別のページでTwitterの値を明示的に上書きすることもできます。 ```php theme={null} Head::twitter(title: $post->social_title) ->twitterImage($post->social_image_url, alt: $post->title); ``` ## PWA・パフォーマンス・アイコン `pwa()` ヘルパーはインストール可能な Web アプリに必要な `` タグをまとめて設定します。 ```php theme={null} Head::pwa( name: 'Acme', manifest: '/site.webmanifest', themeColor: '#0f172a', appleTouchIcon: '/apple-touch-icon.png', appleWebAppStatusBarStyle: 'black', ); ``` ## テーマカラー テーマカラーはグローバル・ルート・ランタイムのいずれでも設定できます。`Media` enum を使えばメディア別のテーマカラーも指定できます。 ```php theme={null} use Laravel\Head\Enums\Media; Head::themeColor('#ffffff', media: Media::Light) ->themeColor('#111827', media: Media::Dark); ``` `Media` には `Portrait` と `Landscape` も含まれます。 ## アプリメタデータとアイコン Laravel Head にはブラウザやアプリの一般的なメタデータ用ヘルパーが含まれています。 ```php theme={null} use Laravel\Head\Enums\ImageType; use Laravel\Head\Enums\Media; Head::applicationName('Acme') ->colorScheme('light dark') ->referrer('strict-origin-when-cross-origin') ->viewport('width=device-width, initial-scale=1') ->appleWebAppTitle('Acme') ->webAppCapable() ->appleWebAppStatusBarStyle('black') ->favicon('/favicon.svg', type: ImageType::Svg) ->icon('/favicon-32x32.png', type: ImageType::Png, sizes: '32x32') ->appleTouchIcon('/apple-touch-icon.png', sizes: '180x180') ->appleTouchStartupImage('/launch.png', media: Media::Portrait) ->maskIcon('/safari-pinned-tab.svg', color: '#111827') ->manifest('/site.webmanifest'); ``` `favicon()` は `icon()` のエイリアスで、同じ `type`・`sizes`・`media` 引数を受け付けます。 ## パフォーマンスと発見可能性 Laravel Head はパフォーマンスヒント、ページネーションリンク、ロケール代替表現、フィードの発見用タグもレンダリングできます。 ```php theme={null} Head::preload(asset('fonts/inter.woff2'), as: 'font', crossorigin: true) ->prefetch(asset('images/next.webp')) ->preconnect('https://cdn.example.com') ->dnsPrefetch('https://analytics.example.com') ->paginate($posts) ->alternates([ 'en' => 'https://example.com/en/about', 'fr' => 'https://example.com/fr/about', 'x-default' => 'https://example.com/about', ]) ->feed('/feed', title: 'Acme RSS') ->feed('/feed.atom', type: 'atom', title: 'Acme Atom'); ``` `preloadAsset()` / `prefetchAsset()` は `asset()` ヘルパーでURLを解決し、拡張子から `as` 属性を自動検出します。 ```php theme={null} Head::preloadAsset('fonts/inter.woff2') ->prefetchAsset('images/next.webp'); ``` ```html theme={null} ``` ## カスタムタグ 専用メソッドがないタグは `meta()` / `link()` で追加できます。 ```php theme={null} Head::meta('format-detection', 'telephone=no') ->meta('article:author', $post->author->name) ->link('search', '/opensearch.xml', [ 'type' => 'application/opensearchdescription+xml', 'title' => 'Acme Search', ]); ``` `meta()` は通常のmetaタグには `name=` を使いますが、Open Graph(`og:`)や記事メタデータ(`article:`)のように本来 `property=` を使うキーの場合は自動的に切り替わります。 ```php theme={null} Head::meta('description', 'About Acme') ->meta('og:title', 'About Acme'); ``` ```html theme={null} ``` ## 構造化データ(JSON-LD) 組み込みのスキーマビルダーは主要なJSON-LDタイプをカバーしています。 ```php theme={null} use Laravel\Head\Enums\OfferAvailability; use Laravel\Head\Facades\Schema; Head::schema( Schema::product() ->name($product->name) ->offers( Schema::offer() ->price($product->price) ->currency('USD') ->availability(OfferAvailability::InStock) ) ); ``` 組み込みのファクトリーメソッドは `article`・`blogPosting`・`product`・`offer`・`brand`・`breadcrumbs`・`faq`・`organization`・`person`・`webPage`・`webSite` です。未知のファクトリーメソッドは汎用のスキーマオブジェクトにフォールバックするため、カスタムのschema.orgタイプも表現できます。 パンくずリストの項目は1件ずつ、またはまとめて追加できます。位置は追加順に自動で割り当てられます。 ```php theme={null} Head::schema( Schema::breadcrumbs()->items([ 'Home' => route('home'), 'Shop' => route('shop.index'), 'Shoes' => route('shop.category', 'shoes'), ]) ); ``` FAQの質問も同様のパターンです。`question()` で1件ずつ、`questions()` でまとめて追加できます。 ```php theme={null} Head::schema( Schema::faq()->questions([ 'What is Laravel Head?' => 'A fluent API for managing the document head.', 'Is it free?' => 'Yes, it is open source.', ]) ); ``` カスタムのスキーマタイプは明示的に登録できます。 ```php theme={null} use DateTimeInterface; use Laravel\Head\Facades\Schema; use Laravel\Head\Schema\SchemaObject; use Laravel\Head\SchemaType; #[SchemaType('JobPosting')] class JobPosting extends SchemaObject { public function title(string $title): static { return $this->set('title', $title); } public function datePosted(DateTimeInterface|string $date): static { return $this->date('datePosted', $date); } } Schema::register(JobPosting::class); ``` ## まとめ `laravel/head` は、Blade・Livewire・Inertia を横断してSEOやソーシャルシェアに必要なメタデータを一元管理できるパッケージです。デフォルト・ルート・ランタイム・エラーページの5層構造により、サイト全体の一貫性を保ちつつページ単位の柔軟なカスタマイズも可能になります。 ソースコードと最新情報はこちら。 # Laravel Horizon Source: https://kawax.biz/jp/horizon Laravel Horizonを使ってRedisキューを視覚的に管理・監視する方法を解説します。ダッシュボード、バランス戦略、Supervisor設定、通知まで網羅します。 ## Horizon とは [Laravel Horizon](https://github.com/laravel/horizon) は、Laravelの**Redisキュー専用**の監視ダッシュボードです。ジョブのスループット・実行時間・失敗状況をリアルタイムで可視化し、ワーカー設定をコードで管理できます。 Horizon はキューの基礎機能を拡張するパッケージです。まず[キューとジョブ](/jp/queues)の基本を理解してから読み進めてください。また、バックエンドには必ず[Redis](/jp/redis)が必要です。 ```mermaid theme={null} flowchart LR Browser["ブラウザ"] -->|"/horizon"| Dashboard["Horizon
    ダッシュボード"] Dashboard -->|"監視・制御"| Horizon["Horizon
    プロセス"] Horizon -->|"ジョブ管理"| Redis["Redis
    Queue"] Redis -->|"ジョブ取得"| Worker1["ワーカー 1"] Redis -->|"ジョブ取得"| Worker2["ワーカー 2"] Redis -->|"ジョブ取得"| Worker3["ワーカー 3"] ``` ## インストール Horizon は Redis をキューバックエンドとして使用します。`config/queue.php` の `QUEUE_CONNECTION` が `redis` に設定されていることを確認してください。現時点で Redis Cluster には対応していません。 Composer でインストールします。 ```shell theme={null} composer require laravel/horizon ``` インストール後、Horizon のアセットと設定ファイルを公開します。 ```shell theme={null} php artisan horizon:install ``` このコマンドで `config/horizon.php` と `app/Providers/HorizonServiceProvider.php` が生成されます。 ## 設定 ### config/horizon.php の構成 `config/horizon.php` はワーカーの設定をすべて管理するファイルです。中心となる設定が `environments` オプションです。 ```php theme={null} 'environments' => [ 'production' => [ 'supervisor-1' => [ 'connection' => 'redis', 'queue' => ['default', 'notifications'], 'balance' => 'auto', 'autoScalingStrategy' => 'time', 'minProcesses' => 1, 'maxProcesses' => 10, 'balanceMaxShift' => 1, 'balanceCooldown' => 3, 'tries' => 3, 'timeout' => 60, ], ], 'local' => [ 'supervisor-1' => [ // その他の値は defaults セクションから継承されます 'maxProcesses' => 3, ], ], ], ``` Horizon は内部で `horizon` という名前の Redis 接続を使用します。`config/database.php` でこの名前を他の接続に使わないでください。 ### CSP nonce(Content Security Policy) [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) の一環として Horizon のビュー内で使われる `script` / `style` タグに [nonce属性](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/nonce) を設定したい場合は、`Horizon::cspNonce` メソッドを使います。リクエストごとに新しい nonce を割り当てるため、通常はミドルウェア内で呼び出します。 ```php theme={null} use Closure; use Illuminate\Http\Request; use Laravel\Horizon\Horizon; use Symfony\Component\HttpFoundation\Response; public function handle(Request $request, Closure $next): Response { Horizon::cspNonce('csp-nonce'); return $next($request); } ``` このミドルウェアは `config/horizon.php` の `middleware` オプションに追加します。 ```php theme={null} 'middleware' => [ 'web', App\Http\Middleware\AddHorizonCspNonce::class, ], ``` ### スーパーバイザー(Supervisor) 各環境は1つ以上の「スーパーバイザー」を持てます。スーパーバイザーはワーカーグループの管理単位で、異なるキュー・バランス戦略・プロセス数を持つ複数のスーパーバイザーを同一環境で動かすことができます。 ### デフォルト値 `defaults` オプションで、すべてのスーパーバイザーに適用されるデフォルト値を設定できます。 ```php theme={null} 'defaults' => [ 'supervisor-1' => [ 'connection' => 'redis', 'queue' => ['default'], 'balance' => 'auto', 'tries' => 1, 'timeout' => 60, 'maxProcesses' => 1, ], ], ``` ### メンテナンスモード アプリケーションがメンテナンスモードのとき、Horizon はデフォルトでジョブを処理しません。強制的に処理させるには `force` オプションを使います。 ```php theme={null} 'environments' => [ 'production' => [ 'supervisor-1' => [ 'force' => true, ], ], ], ``` ### ジョブの最大試行回数 ```php theme={null} 'environments' => [ 'production' => [ 'supervisor-1' => [ 'tries' => 10, ], ], ], ``` `tries` を `0` にすると無制限の再試行を許可します。 ### ジョブのタイムアウト ```php theme={null} 'environments' => [ 'production' => [ 'supervisor-1' => [ 'timeout' => 60, ], ], ], ``` `timeout` は `config/queue.php` の `retry_after` より数秒短い値を設定してください。また、`auto` バランス戦略ではこの値より長いジョブを強制終了する場合があります。 ### バックオフ(再試行の待機時間) 例外発生後に再試行するまでの待機秒数を指定します。 ```php theme={null} // 固定値 'backoff' => 10, // 段階的(指数バックオフ) 'backoff' => [1, 5, 10], ``` ### その他のワーカーオプション `tries`、`timeout`、`backoff` 以外にも、各スーパーバイザーはワーカープロセスの挙動や自動再起動のタイミングを制御するオプションを受け付けます。長時間稼働するプロセスを定期的に再起動することは、メモリリークを防ぐ良い習慣です。 ```php theme={null} 'environments' => [ 'production' => [ 'supervisor-1' => [ // ... 'memory' => 128, 'maxJobs' => 1000, 'maxTime' => 3600, 'sleep' => 3, 'rest' => 0, 'nice' => 0, ], ], ], ``` * `memory` — 再起動までにワーカープロセスが消費できる最大メモリ量(MB)。デフォルトは `128` * `maxJobs` — 再起動までに処理するジョブ数。`0` は無制限を意味する。デフォルトは `0` * `maxTime` — 再起動までにワーカーが稼働できる秒数。`0` は時間による再起動なしを意味する。デフォルトは `0` * `sleep` — ジョブがない場合に次のポーリングまで待機する秒数。デフォルトは `3` * `rest` — 各ジョブの処理間に一時停止する秒数。デフォルトは `0` * `nice` — ワーカープロセスの優先度("niceness")。値が大きいほど優先度は低くなる。デフォルトは `0` ## バランス戦略 Horizon には3種類のワーカーバランス戦略があります。 キューの負荷に応じてワーカー数を自動調整します。`minProcesses` と `maxProcesses` で範囲を指定します。 ```php theme={null} 'supervisor-1' => [ 'balance' => 'auto', 'autoScalingStrategy' => 'time', // または 'size' / 'log' 'minProcesses' => 1, 'maxProcesses' => 10, 'balanceMaxShift' => 1, 'balanceCooldown' => 3, ], ``` * `time` — キューを空にするまでの推定時間でスケーリング * `size` — キュー内のジョブ数でスケーリング * `log` — キュー内のジョブ数の対数に基づいてスケーリング。キューが大幅に増えた場合でも、ワーカーが特定のキューに偏って割り当てられることを防止 `auto` 戦略ではキューの順序が優先度を意味しません。優先度を強制したい場合は複数のスーパーバイザーを使用してください。 ワーカー数を固定し、指定したキューに均等に分配します。 ```php theme={null} 'supervisor-1' => [ 'balance' => 'simple', 'processes' => 10, 'queue' => ['default', 'notifications'], ], ``` 上記では `default` と `notifications` にそれぞれ5プロセスずつ割り当てられます。 キューを列挙した順に厳密に優先します。Laravelデフォルトのキューシステムと同様の動作ですが、積み残しに応じてワーカー数をスケーリングします。 ```php theme={null} 'supervisor-1' => [ 'balance' => false, 'queue' => ['default', 'notifications'], 'minProcesses' => 1, 'maxProcesses' => 10, ], ``` `default` キューのジョブが常に `notifications` キューより先に処理されます。 ## ダッシュボードの認可 Horizon のダッシュボードは `/horizon` ルートでアクセスできます。ローカル環境ではデフォルトで誰でもアクセスできますが、**本番環境**ではゲート定義でアクセスを制限します。 `app/Providers/HorizonServiceProvider.php` の `gate()` メソッドを編集します。 ```php theme={null} use App\Models\User; use Illuminate\Support\Facades\Gate; protected function gate(): void { Gate::define('viewHorizon', function (User $user) { return in_array($user->email, [ 'admin@example.com', ]); }); } ``` 認証を必要としない場合(IP制限などで保護している場合)は、引数をオプションにします。 ```php theme={null} Gate::define('viewHorizon', function (User $user = null) { // IPアドレス等で制限している場合 return true; }); ``` ## Horizon の起動 ### 基本コマンド ```shell theme={null} # 起動 php artisan horizon # 一時停止 / 再開 php artisan horizon:pause php artisan horizon:continue # 特定スーパーバイザーの一時停止 / 再開 php artisan horizon:pause-supervisor supervisor-1 php artisan horizon:continue-supervisor supervisor-1 # ステータス確認 php artisan horizon:status php artisan horizon:supervisor-status supervisor-1 # グレースフルシャットダウン php artisan horizon:terminate ``` ### ローカル開発: 自動再起動 ファイル変更を検知してHorizonを自動再起動するには `horizon:listen` コマンドを使います。 ```shell theme={null} npm install --save-dev chokidar php artisan horizon:listen # Docker / Vagrant 環境では php artisan horizon:listen --poll ``` ### Supervisor による常時起動 本番環境では Supervisor を使って Horizon を常時稼働させます。 #### Supervisor のインストール ```shell theme={null} sudo apt-get install supervisor ``` #### 設定ファイルの作成 `/etc/supervisor/conf.d/horizon.conf` を作成します。 ```ini theme={null} [program:horizon] process_name=%(program_name)s command=php /home/forge/example.com/artisan horizon autostart=true autorestart=true user=forge redirect_stderr=true stdout_logfile=/home/forge/example.com/horizon.log stopwaitsecs=3600 ``` `stopwaitsecs` は最も長いジョブの実行時間より大きな値を設定してください。小さすぎると、Supervisorがジョブを途中で強制終了させてしまいます。 #### Supervisor の起動 ```shell theme={null} sudo supervisorctl reread sudo supervisorctl update sudo supervisorctl start horizon ``` #### デプロイ時 コードをデプロイするたびに Horizon を再起動して変更を反映します。 ```shell theme={null} php artisan horizon:terminate ``` Supervisor が `autostart=true` / `autorestart=true` になっていれば、終了後に自動で再起動されます。 ## ジョブの管理 ### タグ Horizon はジョブに関連する Eloquent モデルを自動検出してタグを付けます。 ```php theme={null} // Video モデル(id=1)を受け取るジョブ → 自動で "App\Models\Video:1" タグが付く RenderVideo::dispatch(Video::find(1)); ``` 手動でタグを定義するには `tags()` メソッドを実装します。 ```php theme={null} class RenderVideo implements ShouldQueue { /** * @return array */ public function tags(): array { return ['render', 'video:'.$this->video->id]; } } ``` イベントリスナーでは、イベントインスタンスが `tags()` メソッドに渡されます。 ```php theme={null} class SendRenderNotifications implements ShouldQueue { public function tags(VideoRendered $event): array { return ['video:'.$event->video->id]; } } ``` ### サイレント化 ダッシュボードの「完了済みジョブ」リストに表示したくないジョブは、`config/horizon.php` でサイレント化できます。 ```php theme={null} 'silenced' => [ App\Jobs\ProcessPodcast::class, ], // タグ単位でサイレント化 'silenced_tags' => [ 'notifications', ], ``` `Silenced` インターフェースを実装する方法もあります。 ```php theme={null} use Laravel\Horizon\Contracts\Silenced; class ProcessPodcast implements ShouldQueue, Silenced { use Queueable; // ... } ``` ## メトリクスとモニタリング Horizon のメトリクスダッシュボードにはジョブ・キューのスループットと実行時間が表示されます。定期的にスナップショットを取得するためのスケジュールを設定します。 ```php theme={null} // routes/console.php use Illuminate\Support\Facades\Schedule; Schedule::command('horizon:snapshot')->everyFiveMinutes(); ``` `config/horizon.php` の `metrics.trim_snapshots` オプションで、メトリクスグラフ用に保持するスナップショット数を設定できます。この設定はスナップショットの経過時間ではなく件数で制限するため、実際の保持期間は `horizon:snapshot` コマンドの実行頻度によって変わります。 ```php theme={null} 'metrics' => [ 'trim_snapshots' => [ 'job' => 24, 'queue' => 24, ], ], ``` メトリクスデータをすべて削除するには以下を実行します。 ```shell theme={null} php artisan horizon:clear-metrics ``` ## ジョブ失敗の通知 キューの待機時間が長くなったときに通知を受け取ることができます。`app/Providers/HorizonServiceProvider.php` の `boot()` メソッドで設定します。 ```php theme={null} use Laravel\Horizon\Horizon; public function boot(): void { parent::boot(); Horizon::routeMailNotificationsTo('admin@example.com'); Horizon::routeSlackNotificationsTo('slack-webhook-url', '#ops'); Horizon::routeSmsNotificationsTo('15556667777'); } ``` ### 待機時間のしきい値 `config/horizon.php` の `waits` オプションで通知トリガーとなる待機秒数を設定します。 ```php theme={null} 'waits' => [ 'redis:critical' => 30, // 30秒以上待機で通知 'redis:default' => 60, 'redis:batch' => 120, ], ``` `0` を設定するとそのキューの通知は無効になります。 ## 失敗ジョブの管理 失敗したジョブは ID または UUID で削除できます。 ```shell theme={null} # 特定の失敗ジョブを削除 php artisan horizon:forget 5 # すべての失敗ジョブを削除 php artisan horizon:forget --all ``` キューのジョブをすべて消去するには以下を使います。 ```shell theme={null} # デフォルトキューを消去 php artisan horizon:clear # 特定キューを消去 php artisan horizon:clear --queue=emails ``` ## アップグレード Horizon のメジャーバージョンアップ時は[アップグレードガイド](https://github.com/laravel/horizon/blob/master/UPGRADE.md)を必ず確認してください。 ## 関連ページ Laravel キューの基本。ジョブの作成・ディスパッチ・バッチ処理・失敗処理を解説。 Horizon のバックエンドとして必要な Redis の設定と使い方。 # 画像加工 Source: https://kawax.biz/jp/images Laravel の Image ファサードを使って画像のリサイズ・クロップ・変換・保存を行う方法を解説します。 ## はじめに Laravel は `Image` ファサードを通じて、流暢な画像加工 API を提供します。リサイズ・クロップ・エンコード・保存などの操作をフレームワーク全体で一貫した表現方法で記述できます。 内部的には [Intervention Image](https://image.intervention.io/) を利用しており、PHP の GD 拡張と Imagick 拡張の両方をサポートしています。 ```php theme={null} use Illuminate\Support\Facades\Image; $path = Image::fromStorage('avatars/photo.jpg', 'public') ->cover(400, 400) ->toWebp() ->quality(80) ->storePublicly('avatars', 'public'); ``` 画像処理は CPU とメモリを大量に消費する可能性があります。大規模な画像処理は HTTP リクエスト中に行わず、[キュージョブ](/jp/queues) として処理することを検討してください。 この機能は **Laravel v13.20.0 以上** で利用できます。古いバージョンを使用している場合は `composer update` でフレームワークを最新版に更新してください。 ## インストール 画像加工機能を使用する前に、Composer で Intervention Image パッケージをインストールします。 ```shell theme={null} composer require intervention/image:^4.0 ``` また、使用するドライバーに応じて PHP の GD 拡張または Imagick 拡張がインストールされていることを確認してください。 ### 設定 設定ファイルは `config/images.php` に配置されます。設定ファイルが存在しない場合は Artisan コマンドで公開します。 ```shell theme={null} php artisan config:publish images ``` デフォルトのドライバーは設定ファイルまたは環境変数 `IMAGE_DRIVER` で指定できます。サポートされているドライバーは `gd` と `imagick` です。 ```ini theme={null} IMAGE_DRIVER=imagick ``` ## 画像の読み込み `Image` ファサードはさまざまなソースから画像インスタンスを生成するメソッドを提供します。画像の内容は遅延読み込みされるため、処理またはバイト列の要求があるまで実際には読み込まれません。 ### アップロードファイル リクエストからアップロードされた画像を取得するには `image` メソッドを使います。ファイルが存在しない場合は `null` を返します。 ```php theme={null} use Illuminate\Http\Request; Route::post('/avatar', function (Request $request) { $request->validate(['avatar' => ['required', 'image']]); $path = $request->image('avatar') ->cover(400, 400) ->toWebp() ->storePublicly('avatars', 'public'); // ... }); ``` `UploadedFile` インスタンスから生成する場合は `fromUpload` メソッドを使います。 ```php theme={null} use Illuminate\Support\Facades\Image; $image = Image::fromUpload($request->file('avatar')); ``` アップロードファイルから生成した画像インスタンスは `file` メソッドで元のファイルを取得できます。 ```php theme={null} $file = $image->file(); ``` ### ストレージファイル [ファイルシステムディスク](/jp/filesystem) に保存されているファイルから画像インスタンスを生成するには `fromStorage` メソッドを使います。第一引数はファイルパス、第二引数はディスク名です。 ```php theme={null} use Illuminate\Support\Facades\Image; $image = Image::fromStorage('avatars/photo.jpg', disk: 'public'); ``` Storage ファサードの `image` メソッドからも生成できます。 ```php theme={null} use Illuminate\Support\Facades\Storage; $image = Storage::disk('public')->image('avatars/photo.jpg'); ``` ### その他のソース バイト列・ローカルパス・URL・Base64 からも画像インスタンスを生成できます。 ```php theme={null} use Illuminate\Support\Facades\Image; $image = Image::fromBytes($contents); $image = Image::fromBase64($base64); $image = Image::fromPath(storage_path('app/avatars/photo.jpg')); $image = Image::fromUrl('https://example.com/photo.jpg'); ``` ## 画像の加工 画像インスタンスはイミュータブルです。各メソッドは変換を処理パイプラインに追加した新しいインスタンスを返すため、メソッドチェーンで記述できます。変換はパイプラインに追加した順に処理され、エンコードは最後に一度だけ行われます。 ```php theme={null} $image = $request->image('avatar') ->orient() ->cover(400, 400) ->sharpen(10); ``` ### リサイズ | メソッド | 説明 | | - | - | | `resize(800, 600)` | 指定サイズにリサイズ。幅または高さのみも指定可 | | `scale(800, 600)` | 比率を保ちながら指定サイズ内に収まるよう縮小。拡大はしない | | `cover(400, 400)` | 指定サイズを完全に覆うようリサイズ+クロップ | | `contain(400, 400)` | 比率を保ちながら指定サイズ内に収まるようリサイズ。余白は背景色で塗りつぶし | | `crop(300, 200)` | 指定サイズでクロップ。`x`・`y` 座標も指定可 | ```php theme={null} $image = $image->resize(800, 600); $image = $image->resize(width: 800); // 幅のみ $image = $image->scale(800, 600); $image = $image->cover(400, 400); $image = $image->contain(400, 400, '#ffffff'); $image = $image->contain(400, 400, 'dominant'); $image = $image->crop(300, 200, x: 50, y: 25); ``` 背景色に `dominant` を指定すると、画像の dominant color で余白を塗りつぶせます。 ### その他の変換 ```php theme={null} $image = $image->orient(); // EXIF の向き情報に従って回転 $image = $image->rotate(90); // 時計回りに 90° 回転 $image = $image->rotate(90, '#ffffff'); // 背景色を指定して回転 $image = $image->rotate(90, 'dominant'); // 画像の dominant color を背景色にする $image = $image->blur(5); // ぼかし(0〜100) $image = $image->grayscale(); // グレースケール $image = $image->sharpen(10); // シャープ化(0〜100) $image = $image->flipVertically(); // 上下反転 $image = $image->flipHorizontally(); // 左右反転 ``` ### 条件付き変換 画像インスタンスは `Conditionable` トレイトをサポートしており、`when` / `unless` で条件付きの変換が可能です。 ```php theme={null} $image = $request->image('avatar') ->when($request->boolean('crop'), fn ($image) => $image->cover(400, 400)) ->unless($request->boolean('preserve_format'), fn ($image) => $image->toWebp()); ``` ## エンコード デフォルトでは元のフォーマットでエンコードされます。フォーマットを変換するには以下のメソッドを使います。 ```php theme={null} $image = $image->toWebp(); $image = $image->toJpg(); $image = $image->toJpeg(); $image = $image->toPng(); $image = $image->toGif(); $image = $image->toAvif(); $image = $image->toBmp(); ``` `quality` メソッドで出力品質(1〜100)を設定できます。 ```php theme={null} $image = $image->toWebp()->quality(80); ``` `optimize` メソッドはフォーマット変換と品質設定のショートカットです。デフォルトは WebP・品質 70 です。 ```php theme={null} $image = $image->optimize(); $image = $image->optimize(format: 'jpg', quality: 85); ``` バイト列・Base64・Data URI として取得することもできます。 ```php theme={null} $bytes = $image->toBytes(); $base64 = $image->toBase64(); $dataUri = $image->toDataUri(); $dataUri = (string) $image; // 文字列キャスト ``` ## 保存 `store` メソッドで画像をファイルシステムディスクに保存します。Laravel は一意なファイル名を自動生成し、保存パスを返します。 ```php theme={null} $path = $request->image('avatar') ->cover(400, 400) ->store(path: 'avatars'); $path = $request->image('avatar') ->cover(400, 400) ->store(path: 'avatars', disk: 's3'); ``` ファイル名を指定する場合は `storeAs` を使います。 ```php theme={null} $path = $request->image('avatar') ->cover(400, 400) ->storeAs(path: 'avatars', name: 'avatar.jpg', disk: 'public'); ``` `public` 可視性で保存する場合は `storePublicly` / `storePubliclyAs` を使います。 ```php theme={null} $path = $request->image('avatar') ->cover(400, 400) ->storePublicly(path: 'avatars', disk: 'public'); $path = $request->image('avatar') ->cover(400, 400) ->storePubliclyAs(path: 'avatars', name: 'avatar.webp', disk: 'public'); ``` 保存に失敗した場合は `false` を返します。 ## 画像情報の取得 `mimeType`・`extension`・`dimensions`・`width`・`height` メソッドで画像の情報を取得できます。これらのメソッドは処理後の画像を対象とします(例:`cover(400, 400)` 後に `width()` を呼ぶと `400` を返します)。 ```php theme={null} $mimeType = $image->mimeType(); $extension = $image->extension(); [$width, $height] = $image->dimensions(); $width = $image->width(); $height = $image->height(); $dominantColor = $image->dominantColor(); ``` ## カスタムドライバー Laravel の画像マネージャーは `Illuminate\Support\Manager` を継承しており、`extend` メソッドでカスタムドライバーを登録できます。 カスタムドライバーは `Illuminate\Contracts\Image\Driver` インターフェースを実装し、`process` メソッドで元の画像バイト列とパイプラインを受け取り、処理済み画像バイト列を返します。 ```php theme={null} image('avatar') ->using('vips') ->cover(400, 400); ``` `config/images.php` の `default` オプション、または環境変数 `IMAGE_DRIVER` でカスタムドライバーをデフォルトに設定することもできます。 ```ini theme={null} IMAGE_DRIVER=vips ``` ## カスタム変換 `Illuminate\Contracts\Image\Transformation` インターフェースを実装したクラスでカスタム変換を定義できます。 ```php theme={null} pixelate($transformation->size); }); ``` 登録後は `transform` メソッドで適用します。 ```php theme={null} use App\Images\Transformations\Pixelate; $image = $request->image('avatar') ->transform(new Pixelate(12)) ->store('avatars'); ``` # ローカライゼーション Source: https://kawax.biz/jp/localization Laravelの多言語対応機能を使って、PHPファイルとJSONファイルで翻訳文字列を管理する方法を解説します。 ## ローカライゼーションとは Laravelのローカライゼーション機能は、複数の言語で翻訳文字列を取得する便利な仕組みを提供します。 アプリケーション内で複数言語をサポートするために使います。 翻訳文字列の管理方法は2種類あります。 | 方法 | 特徴 | | - | - | | **PHPファイル形式** | `lang/ja/messages.php` のようなキー・バリュー配列。バリデーションエラーなど機能ごとに整理したい場合に向いている | | **JSON形式** | `lang/ja.json` に翻訳文字列をそのままキーとして定義。翻訳対象が多いアプリケーションに推奨 | デフォルトのLaravelアプリケーションには `lang` ディレクトリが含まれていません。カスタマイズするには `lang:publish` Artisanコマンドで公開します。 ```shell theme={null} php artisan lang:publish ``` ## ロケールの設定 ### デフォルトロケール アプリケーションのデフォルト言語は `config/app.php` の `locale` で設定します。 通常は `.env` ファイルの `APP_LOCALE` 環境変数を使います。 ```php theme={null} // config/app.php 'locale' => env('APP_LOCALE', 'en'), 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), ``` ```ini theme={null} # .env APP_LOCALE=ja APP_FALLBACK_LOCALE=en ``` `fallback_locale` は、指定した言語に翻訳文字列が存在しない場合のフォールバック言語です。 ### 実行時のロケール変更 `App` ファサードの `setLocale()` メソッドで、リクエスト単位にロケールを変更できます。 ```php theme={null} use Illuminate\Support\Facades\App; Route::get('/greeting/{locale}', function (string $locale) { if (! in_array($locale, ['en', 'ja', 'fr'])) { abort(400); } App::setLocale($locale); // ... }); ``` ### 現在のロケールを確認する ```php theme={null} use Illuminate\Support\Facades\App; $locale = App::currentLocale(); if (App::isLocale('ja')) { // 日本語の場合の処理 } ``` ## 言語ファイルの作成 ### PHPファイル形式 `lang/{言語コード}/` ディレクトリにPHPファイルを作成します。キー・バリュー形式の配列を返します。 ```text theme={null} /lang /en messages.php /ja messages.php ``` ```php theme={null} 'アプリケーションへようこそ!', 'goodbye' => 'またね!', ]; ``` 地域差のある言語は ISO 15897 に従ってディレクトリ名を付けます。たとえばイギリス英語は `en-gb` ではなく `en_GB` です。 ### JSON形式 翻訳する文字列が多い場合は、JSON形式が推奨されます。 `lang/` ディレクトリに言語コード名のJSONファイルを作成します。 ```text theme={null} /lang en.json ja.json ``` デフォルトの翻訳文字列(英語)をキーとして定義します。 ```json theme={null} { "Welcome to our application!": "アプリケーションへようこそ!", "I love programming.": "プログラミングが大好きです。", "Logout": "ログアウト", "Dashboard": "ダッシュボード" } ``` JSON形式では英語の文章そのものがキーになるため、英語のデフォルト表示が自動で機能します。翻訳ファイルがない言語ではキー(英語原文)がそのまま表示されます。 ### PHPファイル形式とJSON形式の使い分け * バリデーションエラーメッセージなど機能ごとに整理したい場合 * Laravelの組み込み翻訳(`validation.php`、`auth.php` など)を上書きする場合 * 階層的なキー管理が必要な場合 ```php theme={null} // lang/ja/validation.php return [ 'required' => ':attributeは必須です。', 'email' => ':attributeは有効なメールアドレスである必要があります。', ]; ``` * UIの文言が多く、キーを考えるのが煩わしい場合 * テンプレートに英語をそのまま書いておき、他言語は翻訳ファイルで対応する場合 * 国際化対応が後から追加になったアプリケーション ```blade theme={null} {{-- Bladeテンプレートに英語をそのまま書く --}} {{ __('Welcome to our application!') }} ``` ## 翻訳文字列の取得 ### `__()` ヘルパー 最もよく使う方法です。PHPファイル形式では「ファイル名.キー」形式で指定します。 ```php theme={null} // PHPファイル形式: lang/ja/messages.php の 'welcome' キー echo __('messages.welcome'); // => アプリケーションへようこそ! // JSON形式: キーに翻訳元の文字列をそのまま指定 echo __('I love programming.'); // => プログラミングが大好きです。 ``` 翻訳文字列が存在しない場合は、指定したキーがそのまま返ります。 ```php theme={null} echo __('messages.not_exists'); // => messages.not_exists(キーがそのまま返る) ``` ### Bladeテンプレートでの使用 Bladeテンプレートでは `{{ __() }}` を使います。 ```blade theme={null} {{-- PHPファイル形式 --}}

    {{ __('messages.welcome') }}

    {{-- JSON形式 --}} {{-- @lang ディレクティブ(非推奨・後方互換のため残っている) --}} @lang('messages.welcome') ``` `@lang` ディレクティブは非推奨です。現在は `{{ __() }}` を使うことが推奨されています。 ## プレースホルダー 翻訳文字列に `:名前` 形式のプレースホルダーを埋め込めます。 ```php theme={null} // lang/ja/messages.php return [ 'welcome' => 'ようこそ、:nameさん!', 'greet' => 'こんにちは、:Nameさん!', // 最初の文字を大文字に 'shout' => 'HEY :NAME!', // 全大文字に ]; ``` `__()` の第2引数に置換値の配列を渡します。 ```php theme={null} echo __('messages.welcome', ['name' => '田中']); // => ようこそ、田中さん! echo __('messages.greet', ['name' => 'taro']); // => こんにちは、Taroさん!(Nameは先頭大文字) echo __('messages.shout', ['name' => 'taro']); // => HEY TARO!(NAMEは全大文字) ``` Bladeテンプレートでも同様に使えます。 ```blade theme={null}

    {{ __('messages.welcome', ['name' => $user->name]) }}

    ``` ## 複数形 言語によって複数形のルールが異なります。Laravelは `|` 記号で単数形と複数形を切り替えられます。 ### 基本的な複数形 ```php theme={null} // lang/ja/messages.php return [ 'apples' => 'りんごが1個あります|りんごが複数あります', ]; ``` JSON形式でも同様に定義できます。 ```json theme={null} { "There is one apple|There are many apples": "りんごが1個あります|りんごが複数あります" } ``` `trans_choice()` 関数で数量を渡して取得します。 ```php theme={null} echo trans_choice('messages.apples', 1); // => りんごが1個あります echo trans_choice('messages.apples', 5); // => りんごが複数あります ``` ### 範囲を指定した複数形 より詳細な範囲指定ができます。 ```php theme={null} 'apples' => '{0} りんごはありません|[1,19] いくつかのりんごがあります|[20,*] たくさんのりんごがあります', ``` ```php theme={null} echo trans_choice('messages.apples', 0); // => りんごはありません echo trans_choice('messages.apples', 10); // => いくつかのりんごがあります echo trans_choice('messages.apples', 50); // => たくさんのりんごがあります ``` ### 複数形でのプレースホルダー `:count` で数量を表示できます。第3引数に追加の置換値を渡せます。 ```php theme={null} // lang/ja/messages.php return [ 'minutes_ago' => '{1} :value分前|[2,*] :value分前', 'items' => ':count件のアイテムがあります', ]; ``` ```php theme={null} echo trans_choice('messages.minutes_ago', 5, ['value' => 5]); // => 5分前 echo trans_choice('messages.items', 3); // => 3件のアイテムがあります ``` ## パッケージの言語ファイルをオーバーライド サードパーティパッケージが独自の言語ファイルを持っている場合、`lang/vendor/{パッケージ名}/{言語コード}/` に同名ファイルを置くことで上書きできます。 たとえば `skyrim/hearthfire` パッケージの英語メッセージをカスタマイズするには。 ```text theme={null} lang/ └── vendor/ └── hearthfire/ └── en/ └── messages.php ``` ```php theme={null} // lang/vendor/hearthfire/en/messages.php return [ 'welcome' => 'カスタマイズしたメッセージ', // 上書きしたいキーだけ定義する。他のキーは元のファイルから読み込まれる ]; ``` ## 実践例: 日英切り替えミドルウェア URLパスやセッション、ユーザー設定に基づいてロケールを自動切り替えするミドルウェアの実装例です。 ```shell theme={null} php artisan make:middleware SetLocale ``` ```php theme={null} session()->get('locale'); // セッションになければブラウザの Accept-Language を参照する if (! $locale) { $browserLocale = substr($request->getPreferredLanguage($supportedLocales) ?? 'ja', 0, 2); $locale = in_array($browserLocale, $supportedLocales) ? $browserLocale : 'ja'; } App::setLocale($locale); return $next($request); } } ``` `bootstrap/app.php` でミドルウェアを登録します。 ```php theme={null} use App\Http\Middleware\SetLocale; ->withMiddleware(function (Middleware $middleware) { $middleware->append(SetLocale::class); }) ``` ```php theme={null} // routes/web.php Route::post('/locale/{locale}', function (string $locale) { if (! in_array($locale, ['ja', 'en'])) { abort(400); } session(['locale' => $locale]); return back(); })->name('locale.switch'); ``` ```blade theme={null}
    @csrf
    @csrf
    ```
    ### 翻訳ファイルの構成例 ```text theme={null} lang/ ├── ja.json # JSON形式(UI文言) ├── en.json ├── ja/ │ └── validation.php # PHPファイル形式(バリデーション) └── en/ └── validation.php ``` ```json theme={null} // lang/ja.json { "Dashboard": "ダッシュボード", "Login": "ログイン", "Logout": "ログアウト", "Welcome, :name!": "ようこそ、:nameさん!", "Save": "保存", "Cancel": "キャンセル", "Are you sure?": "本当によろしいですか?" } ``` ```blade theme={null} {{-- resources/views/layouts/app.blade.php --}} ``` ## まとめ | 方法 | ファイルパス | キーの例 | 取得方法 | | - | - | - | - | | PHPファイル | `lang/ja/messages.php` | `'welcome' => '...'` | `__('messages.welcome')` | | JSON | `lang/ja.json` | `"Welcome": "..."` | `__('Welcome')` | | 関数・メソッド | 用途 | | - | - | | `__('key')` | 翻訳文字列を取得するヘルパー | | `trans('key')` | `__()` と同等のヘルパー | | `trans_choice('key', $count)` | 複数形対応の翻訳文字列を取得 | | `App::setLocale('ja')` | リクエスト単位でロケールを変更 | | `App::currentLocale()` | 現在のロケールを取得 | | `App::isLocale('ja')` | 現在のロケールを確認 | ```blade theme={null} {{-- 基本 --}} {{ __('messages.welcome') }} {{-- プレースホルダーあり --}} {{ __('messages.welcome', ['name' => $user->name]) }} {{-- 複数形 --}} {{ trans_choice('messages.apples', $count) }} ``` # ロギング Source: https://kawax.biz/jp/logging Laravelのロギングシステムを使って、アプリケーションの動作をファイル・Slack・外部サービスへ記録する方法を学びます。 ## ロギングとは Laravelのロギングは**チャンネル**という概念を軸に設計されています。 チャンネルとはログの書き込み先と方法を定義した設定の単位で、ファイル、Slack、syslog などさまざまな出力先を組み合わせられます。 内部では [Monolog](https://github.com/Seldaek/monolog) ライブラリを使用しており、豊富なハンドラーとフォーマッターを活用できます。 デフォルトでは `stack` チャンネルが使われます。`stack` は複数のチャンネルをまとめて扱う親チャンネルです。 ## 設定 ロギングの設定は `config/logging.php` に集中しています。環境変数 `LOG_CHANNEL` でデフォルトチャンネルを切り替えます。 ```php theme={null} // config/logging.php 'default' => env('LOG_CHANNEL', 'stack'), ``` ### 利用可能なチャンネルドライバ | ドライバ | 説明 | | - | - | | `single` | 1つのファイルにすべてのログを書き込む | | `daily` | 日付ごとにファイルを分けて書き込む(ローテーション付き) | | `slack` | Slack の Incoming Webhook にメッセージを送る | | `stack` | 複数チャンネルをまとめる親チャンネル | | `syslog` | システムのsyslogに書き込む | | `errorlog` | PHPのエラーログに書き込む | | `monolog` | Monologのハンドラーを直接指定する | | `custom` | ファクトリクラスでチャンネルを完全カスタマイズする | ### ログレベル Laravelは [RFC 5424](https://tools.ietf.org/html/rfc5424) で定義された8段階のログレベルをサポートしています。 深刻度の高い順に並んでいます。 | レベル | 用途の例 | | - | - | | `emergency` | システム全体が使用不能。即時対応が必要 | | `alert` | 即座に人間が対応しなければならない状態(DB接続断絶など) | | `critical` | アプリの主要機能が停止している重大な障害 | | `error` | 実行時エラー。対応が必要だが即時ではない場合も多い | | `warning` | 潜在的な問題。非推奨APIの使用、予期しないデータなど | | `notice` | 正常動作だが注目すべき情報 | | `info` | ユーザーログインや注文確定など、通常の操作ログ | | `debug` | 開発中の詳細なデバッグ情報 | チャンネルの `level` 設定は**最低ログレベル**を意味します。 たとえば `level` が `error` なら、`error` 以上(`critical`、`alert`、`emergency`)のみが書き込まれます。 ## 基本的な使い方 ### Log ファサード `Illuminate\Support\Facades\Log` ファサードを使って各レベルのメッセージを書き込みます。 ```php theme={null} use Illuminate\Support\Facades\Log; Log::emergency('システムが停止しています。'); Log::alert('データベース接続が切断されました。'); Log::critical('決済サービスが応答しません。'); Log::error('ユーザーのデータ更新に失敗しました。'); Log::warning('非推奨のメソッドが呼ばれました。'); Log::notice('設定ファイルが再読み込みされました。'); Log::info('ユーザーがログインしました。'); Log::debug('クエリの実行時間: 42ms'); ``` コントローラーでの使用例です。 ```php theme={null} $id]); return view('user.profile', [ 'user' => User::findOrFail($id), ]); } } ``` ### log() ヘルパー関数 `log()` ヘルパーを使うと `Log` ファサードを import せずに書けます。 ```php theme={null} log('ヘルパーからのログです。'); // レベルとコンテキストも指定できます log('ユーザーを作成しました。', 'info', ['user_id' => $user->id]); ``` ### コンテキスト情報の追加 メッセージと一緒にコンテキスト(配列)を渡すと、関連する情報をまとめて記録できます。 ```php theme={null} Log::info('ログイン失敗。', [ 'user_id' => $user->id, 'ip' => $request->ip(), 'reason' => 'パスワード不一致', ]); ``` #### withContext() — チャンネル全体に共通コンテキストを付与 特定のチャンネルに対して、以降のすべてのログエントリに共通の情報を付与したい場合は `withContext()` を使います。 リクエストIDのようにすべてのログに含めたい情報に向いています。 ```php theme={null} Log::withContext(['request-id' => (string) Str::uuid()]); // 以降のすべてのログに request-id が自動的に含まれる Log::info('処理を開始します。'); Log::error('エラーが発生しました。'); ``` #### shareContext() — すべてのチャンネルに共通コンテキストを付与 `withContext()` は対象チャンネルのみに効果が限られますが、`shareContext()` はすべてのチャンネルに共通のコンテキストを付与します。 ```php theme={null} Log::shareContext(['app-version' => config('app.version')]); ``` ## チャンネルの設定 ### stack チャンネル — 複数チャンネルへの同時書き込み `stack` チャンネルを使うと、1回のログ呼び出しで複数のチャンネルに書き込めます。 ```php theme={null} // config/logging.php 'channels' => [ 'stack' => [ 'driver' => 'stack', 'channels' => ['daily', 'slack'], ], 'daily' => [ 'driver' => 'daily', 'path' => storage_path('logs/laravel.log'), 'level' => env('LOG_LEVEL', 'debug'), 'days' => 14, ], 'slack' => [ 'driver' => 'slack', 'url' => env('LOG_SLACK_WEBHOOK_URL'), 'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'), 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), 'level' => 'critical', ], ], ``` この設定では `debug` 以上のすべてのログが `daily`(ファイル)に書き込まれ、`critical` 以上のみ `slack` にも送信されます。 本番環境では `slack` チャンネルのレベルを `error` か `critical` に設定するのがおすすめです。 軽微なログまで Slack に流すと通知が大量になり、重要なアラートが埋もれてしまいます。 ### daily チャンネル — ログローテーション `daily` チャンネルは日付ごとにファイルを分けて、古いファイルを自動削除します。 ```php theme={null} 'daily' => [ 'driver' => 'daily', 'path' => storage_path('logs/laravel.log'), 'level' => env('LOG_LEVEL', 'debug'), 'days' => env('LOG_DAILY_DAYS', 14), // 14日分保持 ], ``` `days` を小さくすると古いログが早く削除されます。 本番環境での障害調査に備えて、十分な保持期間を確保してください。 ### Slack へのエラー通知 Slack の [Incoming Webhook URL](https://slack.com/apps/A0F7XDUAZ-incoming-webhooks) を取得し、`.env` に設定します。 ```ini theme={null} LOG_SLACK_WEBHOOK_URL=https://hooks.slack.com/services/xxx/yyy/zzz ``` ```php theme={null} // config/logging.php 'slack' => [ 'driver' => 'slack', 'url' => env('LOG_SLACK_WEBHOOK_URL'), 'username' => 'Laravel Error Bot', 'emoji' => ':fire:', 'level' => 'error', ], ``` `stack` チャンネルに `slack` を含め、`LOG_CHANNEL=stack` を設定すればエラー発生時に自動通知されます。 ### 特定のチャンネルにログを書き込む `Log::channel()` で対象チャンネルを明示的に指定できます。 ```php theme={null} // slack チャンネルだけに書き込む Log::channel('slack')->error('決済サービスが応答しません。'); // 複数チャンネルに同時書き込み Log::stack(['daily', 'slack'])->critical('データベースへの接続が失敗しました。'); ``` ## オンデマンドチャンネル `Log::build()` を使うと、設定ファイルに定義せずにその場でカスタムチャンネルを作れます。 テストや一時的な出力先への書き込みに便利です。 ```php theme={null} use Illuminate\Support\Facades\Log; $channel = Log::build([ 'driver' => 'single', 'path' => storage_path('logs/import-' . now()->format('Ymd') . '.log'), ]); Log::stack([$channel])->info('CSVインポートを開始します。'); ``` ## 実践的なユースケース ### ミドルウェアでリクエストIDを付与する すべてのログに共通のリクエストIDを付与すると、ログから特定リクエストの流れを追いやすくなります。 ```shell theme={null} php artisan make:middleware AssignRequestId ``` ```php theme={null} $requestId]); $response = $next($request); $response->headers->set('X-Request-Id', $requestId); return $response; } } ``` `bootstrap/app.php` でグローバルミドルウェアとして登録します。 ```php theme={null} ->withMiddleware(function (Middleware $middleware) { $middleware->append(\App\Http\Middleware\AssignRequestId::class); }) ``` この設定後、すべてのログに `request-id` が自動的に付与されます。 ``` [2026-03-01 12:00:00] local.INFO: ユーザーがログインしました。 {"request-id":"550e8400-...","user_id":1} [2026-03-01 12:00:00] local.INFO: ダッシュボードを表示します。 {"request-id":"550e8400-..."} ``` ### 非推奨警告のログ記録 PHP や Laravel の非推奨機能を使った際の警告をログに残せます。 ```php theme={null} // config/logging.php 'deprecations' => [ 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), 'trace' => env('LOG_DEPRECATIONS_TRACE', false), ], ``` `.env` でチャンネルを指定します。 ```ini theme={null} LOG_DEPRECATIONS_CHANNEL=daily ``` ## Laravel Pail — リアルタイムログ監視 [Laravel Pail](https://github.com/laravel/pail) はターミナルでアプリケーションのログをリアルタイムに確認できる開発ツールです。標準の `tail` コマンドとは異なり、[Laravel Nightwatch](https://nightwatch.laravel.com)、Sentry、Flareを含むあらゆるログドライバーで動作するよう設計されています。 Pail の実行には PHP の [PCNTL](https://www.php.net/manual/ja/book.pcntl.php) 拡張が必要です。 ### インストール ```shell theme={null} composer require --dev laravel/pail ``` ### 基本的な使い方 ```shell theme={null} # ログを流し見する php artisan pail # 詳細表示(省略なし) php artisan pail -v # スタックトレースも表示 php artisan pail -vv ``` ### ログのフィルタリング ```shell theme={null} # キーワードで絞り込む php artisan pail --filter="QueryException" # メッセージのみで絞り込む php artisan pail --message="ログイン" # ログレベルで絞り込む php artisan pail --level=error # 特定ユーザーのログのみ表示 php artisan pail --user=1 ``` ## まとめ | レベル | 使うタイミング | | - | - | | `emergency` | システム全体が停止するような致命的な障害 | | `alert` | 即座に人が介入しなければならない状態 | | `critical` | 主要機能が動かない重大な障害 | | `error` | 予期しない実行時エラー。対応が必要 | | `warning` | 非推奨の使用や予期しないデータなど注意すべき状態 | | `notice` | 正常だが記録しておきたい重要な操作 | | `info` | ユーザー操作やビジネスイベントの記録 | | `debug` | 開発中の詳細なデバッグ情報(本番では不要) | * **開発環境**: `single` や `daily` でファイルに書き込む * **本番環境**: `stack` で `daily`(ファイル保存)と `slack`(エラー通知)を組み合わせる * **特定処理のログ**: `Log::build()` でオンデマンドチャンネルを作成して独立したファイルに書き込む * **リアルタイム監視**: `php artisan pail` でターミナルから確認する * `debug` ログには機密情報が含まれる場合があります。本番環境では `LOG_LEVEL=error` 以上を推奨します。 * ログファイルは定期的にローテーションしてディスクを圧迫しないようにしてください(`daily` チャンネルの `days` 設定)。 * Slack などの外部サービスへの通知はレートリミットに注意してください。重大なエラーのみ通知するようレベルを設定してください。 # メール送信 Source: https://kawax.biz/jp/mail LaravelのMailableクラスを使ったメール送信の基本から、Markdownメール・キュー送信まで解説します。 ## Laravelのメール機能とは Laravelのメール機能は [Symfony Mailer](https://symfony.com/doc/current/mailer.html) をベースに構築されており、SMTP・Mailgun・Postmark・Resend・Amazon SES・sendmailなどのドライバーに対応しています。 以前のLaravelで使われていたSwiftMailerは廃止されています。Laravel 9以降はSymfony Mailerを使います。 メール設定は `config/mail.php` で管理します。`.env` ファイルで使用するドライバーを切り替えることができます。 ```ini theme={null} MAIL_MAILER=smtp MAIL_HOST=smtp.example.com MAIL_PORT=587 MAIL_USERNAME=your-username MAIL_PASSWORD=your-password MAIL_ENCRYPTION=tls MAIL_FROM_ADDRESS="hello@example.com" MAIL_FROM_NAME="${APP_NAME}" ``` ## Mailableクラスの作成 Laravelでは、アプリケーションが送信する各メールを「Mailable」クラスとして表現します。`make:mail` Artisanコマンドでクラスを生成します。 ```shell theme={null} php artisan make:mail OrderShipped ``` 生成されたクラスは `app/Mail/` ディレクトリに配置されます。 ## Mailableの設定 生成されたMailableクラスには `envelope()`、`content()`、`attachments()` の3つのメソッドがあります。 ```php theme={null} [ 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), 'name' => env('MAIL_FROM_NAME', 'Example'), ], ``` ### メール本文の設定 `content()` メソッドでBladeテンプレートを指定します。コンストラクターで `public` プロパティに設定したデータは、自動的にテンプレートで利用できます。 ```php theme={null} public function content(): Content { return new Content( view: 'mail.orders.shipped', ); } ``` 対応するBladeテンプレート (`resources/views/mail/orders/shipped.blade.php`) を作成します。 ```blade theme={null}

    ご注文が発送されました

    注文番号: {{ $order->id }}

    合計金額: ¥{{ number_format($order->total) }}

    ご注文いただきありがとうございました。

    ``` データを `with` パラメーターで明示的に渡すこともできます。この場合はプロパティを `protected` または `private` にします。 ```php theme={null} public function __construct( protected Order $order, ) {} public function content(): Content { return new Content( view: 'mail.orders.shipped', with: [ 'orderId' => $this->order->id, 'total' => $this->order->total, ], ); } ``` ## メールの送信 `Mail` ファサードの `to()` メソッドで宛先を指定し、`send()` でメールを送信します。 ```mermaid theme={null} flowchart TD A["Mail::to()->send(new OrderShipped())"] --> B{"ShouldQueue
    実装?"} B -- "No" --> C["同期送信
    Mailerへ"] B -- "Yes" --> D["キューに登録
    バックグラウンド送信"] D --> C C --> E{"MAILドライバー"} E --> F["smtp"] E --> G["ses / mailgun
    / postmark"] E --> H["log
    (開発用)"] ``` ```php theme={null} order_id); Mail::to($request->user())->send(new OrderShipped($order)); return redirect('/orders'); } } ``` CC・BCCも指定できます。 ```php theme={null} Mail::to($request->user()) ->cc($ccUsers) ->bcc($bccUsers) ->send(new OrderShipped($order)); ``` 特定のメーラーを使って送信するには `mailer()` メソッドを使います。 ```php theme={null} Mail::mailer('postmark') ->to($request->user()) ->send(new OrderShipped($order)); ``` ## キューを使ったメール送信 メール送信はレスポンスタイムに影響するため、キューを使ってバックグラウンドで送信することを推奨します。 ```php theme={null} Mail::to($request->user())->queue(new OrderShipped($order)); ``` キューを使うには、事前にキューの設定とワーカーの起動が必要です。 遅延送信も可能です。 ```php theme={null} Mail::to($request->user()) ->later(now()->plus(minutes: 10), new OrderShipped($order)); ``` Mailableクラスに `ShouldQueue` を実装すると、`send()` を呼び出しても常にキューに入るようになります。 ```php theme={null} use Illuminate\Contracts\Queue\ShouldQueue; class OrderShipped extends Mailable implements ShouldQueue { // ... } ``` ### キュー接続とキュー名をPHP属性で指定する PHP 属性を使ってMailableクラスにキュー接続とキュー名を直接指定できます。`on()->queue()` メソッドチェーンの代わりに、クラスレベルで宣言的に設定できます。 ```php theme={null} use Illuminate\Queue\Attributes\Connection; use Illuminate\Queue\Attributes\Queue; #[Connection('sqs')] #[Queue('emails')] class OrderShipped extends Mailable implements ShouldQueue { // ... } ``` この方法では、このMailableが常にSQS接続の `emails` キューに送信されることをクラス定義で明示できます。 ## Markdown Mailの作成 Markdown形式のメールを使うと、Laravelが提供する美しいレスポンシブなHTMLテンプレートを活用できます。 ### Markdown Mailableの生成 `--markdown` オプションを付けてMailableを生成します。 ```shell theme={null} php artisan make:mail OrderShipped --markdown=mail.orders.shipped ``` `content()` メソッドで `markdown` パラメーターを使います。 ```php theme={null} use Illuminate\Mail\Mailables\Content; public function content(): Content { return new Content( markdown: 'mail.orders.shipped', with: [ 'url' => route('orders.show', $this->order), ], ); } ``` ### Markdownテンプレートの記述 Laravelが提供するBladeコンポーネントを使ってメール本文を作成します。 ```blade theme={null} # ご注文が発送されました ご注文が発送されましたのでお知らせします。 注文を確認する ご利用ありがとうございます。
    {{ config('app.name') }}
    ``` 利用可能なコンポーネントは次のとおりです。 | コンポーネント | 説明 | | - | - | | `` | ボタンリンク (`color` に `primary`・`success`・`error` を指定可能) | | `` | 目立つパネルエリア | | `` | Markdown形式のテーブル | Markdownメールは、同時にプレーンテキスト版も自動生成されます。HTMLメールに対応していないメールクライアントでも読めます。 ## メールのプレビュー ルートでMailableインスタンスを返すと、ブラウザでメールをプレビューできます。開発中の確認に便利です。 ```php theme={null} // routes/web.php Route::get('/mailable', function () { $order = App\Models\Order::first(); return new App\Mail\OrderShipped($order); }); ``` ## メールのテスト `Mail::fake()` を使うと、実際にメールを送信せずに送信内容をアサートできます。 ```php theme={null} use App\Mail\OrderShipped; use Illuminate\Support\Facades\Mail; test('注文出荷時にメールが送信される', function () { Mail::fake(); $order = Order::factory()->create(); $this->post("/orders/{$order->id}/ship"); Mail::assertSent(OrderShipped::class, function ($mail) use ($order) { return $mail->order->id === $order->id; }); }); ``` 送信回数を細かくアサートすることもできます。`assertSentOnce` は Laravel 13 で追加されたメソッドで、対象のMailableがちょうど1回だけ送信されたことを確認します。 ```php theme={null} Mail::fake(); // ... // 2回送信されたことをアサート Mail::assertSentTimes(OrderShipped::class, 2); // ちょうど1回だけ送信されたことをアサート Mail::assertSentOnce(OrderShipped::class); // 送信されたMailableの総数が3件であることをアサート Mail::assertSentCount(3); ``` Mailableをキューで送信する場合は `assertSent` の代わりに `assertQueued` 系のメソッドを使います。`assertQueuedOnce` も同様にちょうど1回だけキューに登録されたことを確認します。 ```php theme={null} Mail::assertQueued(OrderShipped::class); Mail::assertQueuedOnce(OrderShipped::class); Mail::assertNotQueued(OrderShipped::class); Mail::assertNothingQueued(); Mail::assertQueuedCount(3); ``` ## ローカル開発でのメール 本番環境でメールを誤送信しないよう、ローカル開発では [Mailpit](https://github.com/axllent/mailpit) などのツールを使うと便利です。 ```ini theme={null} # .env (Mailpit) MAIL_MAILER=smtp MAIL_HOST=127.0.0.1 MAIL_PORT=1025 ``` `Laragon` や `Laravel Herd` では Mailpit がデフォルトで含まれています。 `log` ドライバーを使うとメールの内容がログファイルに記録されます。メール送信の確認だけであれば最も手軽な方法です。 ```ini theme={null} MAIL_MAILER=log ``` ## まとめ | やりたいこと | 方法 | | - | - | | Mailableを作成する | `php artisan make:mail ClassName` | | 送信者を設定する | `envelope()` の `from` パラメーター | | Bladeテンプレートを使う | `content()` の `view` パラメーター | | Markdownテンプレートを使う | `content()` の `markdown` パラメーター | | メールを送信する | `Mail::to()->send()` | | キューで送信する | `Mail::to()->queue()` | | 遅延送信する | `Mail::to()->later()` | # Laravel MCP Source: https://kawax.biz/jp/mcp LaravelアプリケーションにModel Context Protocol(MCP)サーバーを組み込む方法を学びましょう。AI コーディングエージェントがあなたのアプリケーションと対話できるツール、リソース、プロンプトを定義できます。 ## MCPとは **Model Context Protocol(MCP)** は、AIクライアント(Claude、Cursor、GitHub Copilotなど)とアプリケーションが標準化されたプロトコルで通信するための仕様です。MCPサーバーを実装することで、AIエージェントはあなたのLaravelアプリケーションのデータにアクセスしたり、アクションを実行したりできるようになります。 Laravel MCP は Laravel 13 で追加された公式パッケージです。`laravel/mcp` として提供されており、MCPサーバーの構築に必要な一連の機能を提供します。 MCPサーバーが提供できる機能は主に3つです。 | 機能 | 説明 | | - | - | | **ツール(Tools)** | AIクライアントが呼び出せる関数。検索・更新・外部API連携など | | **リソース(Resources)** | AIクライアントが読み込めるデータやコンテキスト情報 | | **プロンプト(Prompts)** | 再利用可能なプロンプトテンプレート | ## インストール Composerでパッケージをインストールします。 ```shell theme={null} composer require laravel/mcp ``` インストール後、`vendor:publish` Artisanコマンドを実行して `routes/ai.php` ファイルを生成します。 ```shell theme={null} php artisan vendor:publish --tag=ai-routes ``` このコマンドにより `routes/ai.php` ファイルが作成されます。ここにMCPサーバーの登録を記述します。 ## サーバーの作成 `make:mcp-server` Artisanコマンドでサーバークラスを生成します。 ```shell theme={null} php artisan make:mcp-server WeatherServer ``` `app/Mcp/Servers` ディレクトリにサーバークラスが生成されます。 ```php theme={null} middleware(['throttle:mcp']); ``` #### ローカルサーバー ローカルサーバーはArtisanコマンドとして動作します。Claude DesktopなどのローカルAIクライアントとの連携に使います。 ```php theme={null} use App\Mcp\Servers\WeatherServer; use Laravel\Mcp\Facades\Mcp; Mcp::local('weather', WeatherServer::class); ``` ローカルサーバーは通常、MCPクライアントが自動的に起動します。手動で `mcp:start` Artisanコマンドを実行する必要はありません。 ### キャッシュヒント Laravel MCPは、サーバー探索・プリミティブの一覧・リソースの読み込みなど、キャッシュ可能なレスポンスに対してキャッシュヒントを付与します。デフォルトでは、これらのレスポンスは有効期限(TTL)0ミリ秒のプライベートとしてマークされます。 サーバーの `Cacheable` 属性でデフォルトのキャッシュヒントをカスタマイズできます。 ```php theme={null} use Laravel\Mcp\Enums\CacheScope; use Laravel\Mcp\Server\Attributes\Cacheable; #[Cacheable(ttlMs: 60_000, scope: CacheScope::Public)] class WeatherServer extends Server { /** * Get the cache hints for individual MCP methods. * * @return array */ protected function cacheHints(): array { return [ 'tools/list' => new Cacheable(ttlMs: 30_000, scope: CacheScope::Public), ]; } } ``` `CacheScope::Private` は同じ認可コンテキスト内でのみキャッシュされたレスポンスを許可し、`CacheScope::Public` はユーザー間でのレスポンス共有を許可します。キャッシュヒントはあくまで助言であり、実際にキャッシュするかどうかはMCPクライアントやホスト側が判断します。`cacheHints` が返すメソッド単位のヒントは、サーバーの `Cacheable` 属性よりも優先されます。 個別のリソースクラスに `Cacheable` 属性を適用すると、サーバーのキャッシュヒントを上書きできます。 ```php theme={null} #[Cacheable(ttlMs: 300_000, scope: CacheScope::Public)] class WeatherGuidelinesResource extends Resource { // ... } ``` リソースの `Cacheable` 属性は、メソッド単位のヒントとサーバーのデフォルトヒントの両方よりも優先されます。 ## ツール ツールはAIクライアントが呼び出せる関数です。データの取得、外部APIとの連携、データベースの操作などを実装できます。 ### ツールの作成 `make:mcp-tool` Artisanコマンドでツールクラスを生成します。 ```shell theme={null} php artisan make:mcp-tool CurrentWeatherTool ``` 作成したツールをサーバーの `$tools` プロパティに登録します。 ```php theme={null} use App\Mcp\Tools\CurrentWeatherTool; use Laravel\Mcp\Server; class WeatherServer extends Server { protected array $tools = [ CurrentWeatherTool::class, ]; } ``` 基本的なツールクラスの実装例です。 ```php theme={null} get('location'); // 天気データを取得する... return Response::text('The weather is sunny, 22°C.'); } public function schema(JsonSchema $schema): array { return [ 'location' => $schema->string() ->description('The location to get the weather for.') ->required(), ]; } } ``` ### 検索可能なツールカタログ 多数のツールを持つサーバーでは、すべてのツールをAIクライアントに一度に公開するのではなく、一部のツールを検索可能なカタログに配置できます。検索可能なカタログは2つのツールを公開します。`search_tools` はツール名・説明・入力スキーマでカタログを検索し、`execute_tools` は検索結果から1つ以上のツールを呼び出します。 検索可能なカタログを作成するには、サーバーの `$tools` プロパティの配列キーとして `ToolSearch` クラスを使います。 ```php theme={null} |array>> */ protected array $tools = [ CurrentWeatherTool::class, ToolSearch::class => [ HistoricalWeatherTool::class, WeatherAlertsTool::class, ], ]; } ``` この例では、`CurrentWeatherTool` はそのまま公開され、過去の天気や気象警報のツールは検索可能なカタログを通じてのみ利用できます。カタログ内のツールが検索・実行される場合でも、[条件付きツール登録](#条件付き登録)は引き続き有効です。 1回の `execute_tools` 呼び出しで実行できるツールの最大数と最大レスポンスサイズは、`mcp.tool_search.max_tool_calls` と `mcp.tool_search.max_output_bytes` の設定値で制御します。 ### ツールの名前と説明 クラス名からデフォルトの名前とタイトルが自動生成されます。`CurrentWeatherTool` であれば名前は `current-weather`、タイトルは `Current Weather Tool` になります。`Name` と `Title` 属性でカスタマイズできます。 ```php theme={null} use Laravel\Mcp\Server\Attributes\Name; use Laravel\Mcp\Server\Attributes\Title; #[Name('get-optimistic-weather')] #[Title('Get Optimistic Weather Forecast')] class CurrentWeatherTool extends Tool { // ... } ``` ツールの説明(`Description`)は自動生成されません。AIモデルがツールの使い方を理解するために必須なので、必ず意味のある説明を設定してください。 ### 入力スキーマ `schema` メソッドで入力パラメーターのスキーマを定義します。LaravelのJSONスキーマビルダーを使って型や制約を指定できます。 ```php theme={null} public function schema(JsonSchema $schema): array { return [ 'location' => $schema->string() ->description('The location to get the weather for.') ->required(), 'units' => $schema->string() ->enum(['celsius', 'fahrenheit']) ->description('The temperature units to use.') ->default('celsius'), ]; } ``` ### 出力スキーマ `outputSchema` メソッドでレスポンスの構造を定義できます。AIクライアントがレスポンスを解析しやすくなります。 ```php theme={null} public function outputSchema(JsonSchema $schema): array { return [ 'temperature' => $schema->number() ->description('Temperature in Celsius') ->required(), 'conditions' => $schema->string() ->description('Weather conditions') ->required(), 'humidity' => $schema->integer() ->description('Humidity percentage') ->required(), ]; } ``` ### バリデーション `handle` メソッド内でLaravelの標準バリデーション機能を使えます。 ```php theme={null} public function handle(Request $request): Response { $validated = $request->validate([ 'location' => 'required|string|max:100', 'units' => 'in:celsius,fahrenheit', ], [ 'location.required' => 'You must specify a location. For example, "New York City" or "Tokyo".', 'units.in' => 'You must specify either "celsius" or "fahrenheit" for the units.', ]); // バリデーション済みのデータを使って処理を行う... } ``` バリデーション失敗時にAIクライアントはエラーメッセージを参考に再試行します。具体的で実行可能なエラーメッセージを提供してください。 ### 依存性注入 Laravelのサービスコンテナを通じてツールが解決されるため、コンストラクタや `handle` メソッドで依存関係を型ヒントできます。 ```php theme={null} get('location'); $forecast = $weather->getForecastFor($location); return Response::text("Forecast: {$forecast}"); } } ``` ### アノテーション ツールにアノテーションを追加することで、AIクライアントにツールの振る舞いに関する追加情報を提供できます。 ```php theme={null} use Laravel\Mcp\Server\Tools\Annotations\IsIdempotent; use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly; use Laravel\Mcp\Server\Tool; #[IsIdempotent] #[IsReadOnly] class CurrentWeatherTool extends Tool { // ... } ``` 利用可能なアノテーションは次のとおりです。 | アノテーション | 説明 | | - | - | | `#[IsReadOnly]` | ツールが環境を変更しないことを示す | | `#[IsDestructive]` | ツールが破壊的な更新を行う可能性があることを示す | | `#[IsIdempotent]` | 同じ引数で繰り返し呼び出しても副作用がないことを示す | | `#[IsOpenWorld]` | ツールが外部エンティティと対話する可能性があることを示す | ### 条件付き登録 `shouldRegister` メソッドを実装することで、実行時にツールを条件付きで登録できます。 ```php theme={null} public function shouldRegister(Request $request): bool { return $request?->user()?->subscribed() ?? false; } ``` `false` を返すとそのツールはAIクライアントから見えなくなります。 ### レスポンス ツールは `Laravel\Mcp\Response` のインスタンスを返す必要があります。 ```php theme={null} return Response::text('Weather Summary: Sunny, 22°C'); ``` ```php theme={null} return Response::error('Unable to fetch weather data. Please try again.'); ``` ```php theme={null} return Response::image(file_get_contents(storage_path('weather/radar.png')), 'image/png'); return Response::audio(file_get_contents(storage_path('weather/alert.mp3')), 'audio/mp3'); // ストレージから直接読み込む(MIMEタイプは自動検出) return Response::fromStorage('weather/radar.png'); ``` ```php theme={null} public function handle(Request $request): array { return [ Response::text('Weather Summary: Sunny, 22°C'), Response::text("**Detailed Forecast**\n- Morning: 18°C\n- Afternoon: 25°C"), ]; } ``` AIクライアントが解析しやすい構造化データを返します。 ```php theme={null} return Response::structured([ 'temperature' => 22.5, 'conditions' => 'Partly cloudy', 'humidity' => 65, ]); ``` 長時間かかる処理で途中経過をリアルタイム送信します。 ```php theme={null} public function handle(Request $request): Generator { $locations = $request->array('locations'); foreach ($locations as $index => $location) { yield Response::notification('processing/progress', [ 'current' => $index + 1, 'total' => count($locations), 'location' => $location, ]); yield Response::text($this->forecastFor($location)); } } ``` ## プロンプト プロンプトは再利用可能なプロンプトテンプレートです。AIクライアントが言語モデルと対話する際に使う定型的なクエリを標準化した形で提供できます。 ### プロンプトの作成 ```shell theme={null} php artisan make:mcp-prompt DescribeWeatherPrompt ``` サーバーの `$prompts` プロパティに登録します。 ```php theme={null} use App\Mcp\Prompts\DescribeWeatherPrompt; class WeatherServer extends Server { protected array $prompts = [ DescribeWeatherPrompt::class, ]; } ``` ### プロンプトの引数 `arguments` メソッドでプロンプトのパラメーターを定義します。 ```php theme={null} validate([ 'tone' => 'required|string|max:50', ]); $tone = $validated['tone']; // 指定された tone を使ってプロンプトを生成する... } } ``` バリデーション失敗時、AIクライアントはエラーメッセージを参考に再試行します。具体的で実行可能なメッセージを提供してください。 ```php theme={null} $validated = $request->validate([ 'tone' => ['required', 'string', 'max:50'], ], [ 'tone.*' => 'toneを指定してください。例: "formal"、"casual"、"humorous"。', ]); ``` ### 依存性注入 Laravelのサービスコンテナを通じてプロンプトが解決されるため、コンストラクタや `handle` メソッドで依存関係を型ヒントできます。 ```php theme={null} isServiceAvailable(); // ... } ``` ### 条件付き登録 `shouldRegister` メソッドを実装することで、実行時にプロンプトを条件付きで登録できます。 ```php theme={null} user()?->subscribed() ?? false; } } ``` `false` を返すとそのプロンプトはAIクライアントから見えなくなり、呼び出しもできなくなります。 ### プロンプトのレスポンス プロンプトの `handle` メソッドではユーザーメッセージとアシスタントメッセージを返せます。`asAssistant()` でアシスタント側のメッセージとして扱います。 ```php theme={null} string('tone'); $systemMessage = "You are a helpful weather assistant. Please provide a weather description in a {$tone} tone."; $userMessage = 'What is the current weather like in Tokyo?'; return [ Response::text($systemMessage)->asAssistant(), Response::text($userMessage), ]; } } ``` ## リソース リソースはAIクライアントがコンテキストとして読み込めるデータや情報です。ドキュメント、設定情報、動的データなど、AIの応答品質を向上させる情報を提供できます。 ### リソースの作成 ```shell theme={null} php artisan make:mcp-resource WeatherGuidelinesResource ``` サーバーの `$resources` プロパティに登録します。 ```php theme={null} use App\Mcp\Resources\WeatherGuidelinesResource; class WeatherServer extends Server { protected array $resources = [ WeatherGuidelinesResource::class, ]; } ``` ```php theme={null} get('userId'); $fileId = $request->get('fileId'); // ファイルコンテンツを取得して返す... return Response::text("File {$fileId} for user {$userId}"); } } ``` URIからの変数は自動的にリクエストに取り込まれ、`get` メソッドで取得できます。 ### リソースのリクエスト ツールやプロンプトとは異なり、リソースは入力スキーマや引数を定義できません。ただし、`handle` メソッド内でリクエストオブジェクトを通じてリクエスト情報にアクセスできます。 ```php theme={null} guidelines()); } ``` ### リソースのアノテーション リソースにはオーディエンス、優先度、最終更新日などのアノテーションを付けられます。 ```php theme={null} use Laravel\Mcp\Enums\Role; use Laravel\Mcp\Server\Annotations\Audience; use Laravel\Mcp\Server\Annotations\LastModified; use Laravel\Mcp\Server\Annotations\Priority; use Laravel\Mcp\Server\Resource; #[Audience(Role::User)] #[LastModified('2025-01-12T15:00:58Z')] #[Priority(0.9)] class UserDashboardResource extends Resource { // ... } ``` | アノテーション | 型 | 説明 | | - | - | - | | `#[Audience]` | Role または配列 | 対象オーディエンス(`Role::User`、`Role::Assistant`、または両方) | | `#[Priority]` | float | 重要度スコア(0.0〜1.0) | | `#[LastModified]` | string | ISO 8601形式の最終更新日時 | ### リソースの条件付き登録 `shouldRegister` メソッドを実装することで、実行時にリソースを条件付きで登録できます。 ```php theme={null} user()?->subscribed() ?? false; } } ``` `false` を返すとそのリソースはAIクライアントから見えなくなり、アクセスもできなくなります。 ### リソースのレスポンス リソースは `Laravel\Mcp\Response` のインスタンスを返す必要があります。 テキストコンテンツには `text` メソッドを使います。 ```php theme={null} return Response::text($weatherData); ``` #### リソースリンクレスポンス `resourceLink` メソッドでリソースリンクを返せます。埋め込みリソースとは異なり、AIクライアントが独立して取得するURIポインターを返します。 ```php theme={null} return Response::resourceLink( uri: 'file:///data/report.json', name: 'monthly-report', mimeType: 'application/json', ); ``` 登録済みのリソースクラスまたはインスタンスを渡すこともできます。URI、名前、タイトル、説明、MIMEタイプを自動継承します。 ```php theme={null} return Response::resourceLink(new WeatherForecastResource); ``` #### Blobレスポンス バイナリコンテンツを返すには `blob` メソッドを使います。MIMEタイプはリソースの `#[MimeType]` 属性で設定します。 ```php theme={null} return Response::blob(file_get_contents(storage_path('weather/radar.png'))); ``` ```php theme={null} #[MimeType('image/png')] class WeatherGuidelinesResource extends Resource { // ... } ``` #### エラーレスポンス エラーを示す場合は `error` メソッドを使います。 ```php theme={null} return Response::error('指定された場所の天気データを取得できませんでした。'); ``` ## アプリ Laravel MCPは[MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview)をサポートしています。これはModel Context Protocolの拡張機能で、ツールがサポートされたホスト内のサンドボックスiframe上でインタラクティブなHTMLアプリケーションをレンダリングできるようにします。これにより、プレーンテキストのレスポンスを超えた、ダッシュボード、フォーム、ビジュアライゼーション、その他のリッチな体験を構築できます。 MCPアプリは以下の2つのパーツが連携して動作します。 * **アプリリソース** — アプリケーションの自己完結型HTMLを返します。 * **ツール** — `#[RendersApp]` アトリビュートを使ってアプリリソースにリンクされます。ツールが呼び出されると、ホストがリンクされたリソースを取得してレンダリングします。 ### アプリリソースの作成 `make:mcp-app-resource` Artisanコマンドでアプリリソースを作成できます。 ```shell theme={null} php artisan make:mcp-app-resource WeatherDashboardApp ``` このコマンドは2つのファイルを作成します。`app/Mcp/Resources` 内のPHPクラスと `resources/views/mcp` 内のBladeビューです。ビュー名はクラス名から自動的に推測されます。たとえば `WeatherDashboardApp` は `mcp.weather-dashboard-app` にマッピングされます。 ```php theme={null} $this->title(), ]); } } ``` `AppResource` はベースの `Resource` クラスを継承し、MCP Apps仕様で要求される `ui://` URIスキームと `text/html;profile=mcp-app` MIMEタイプを自動的に設定します。他のリソースと同様に、サーバーの `$resources` 配列に登録する必要があります。 生成されたBladeビューは `` コンポーネントを使用します。このコンポーネントはクライアントサイドのMCP SDKがバンドルされた完全なHTMLドキュメントをレンダリングします。 ```blade theme={null}

    ``` `createMcpApp` グローバル関数はバンドルされたSDKが提供します。iframeのサーバーへの接続、ホストテーマの適用、`callServerTool`・`sendMessage`・`openLink`などのヘルパーやイベントコールバックの公開を処理します。完全なクライアントサイドAPIについては[MCP Apps仕様](https://modelcontextprotocol.io/extensions/apps/overview)を参照してください。 ### ツールからアプリをレンダリング アプリリソースを表示するには、`#[RendersApp]` アトリビュートを使ってツールにリンクします。ツールが呼び出されると、Laravel MCPはリソースのURIをツールのメタデータに含め、ホストがサンドボックスiframe内でアプリをレンダリングできるようにします。 ```php theme={null} `AppResource` が登録されると、Laravel MCPは自動的に `io.modelcontextprotocol/ui` ケイパビリティをアドバタイズします。追加のサーバー設定は不要です。 ### アプリツールの可視性 各 `#[RendersApp]` ツールは `visibility` 引数で呼び出し元を制限できます。これは、UIがデータを読み込み・更新するために呼び出すプライベートなアプリ専用ツールを、モデルからは見えないようにする場合に便利です。 ```php theme={null} use Laravel\Mcp\Server\Attributes\RendersApp; use Laravel\Mcp\Server\Ui\Enums\Visibility; #[RendersApp(resource: WeatherDashboardApp::class, visibility: [Visibility::App])] class GetWeatherData extends Tool { // ... } ``` `Visibility` enumには `Model` と `App` の2つのケースがあり、デフォルトは両方です。UIが直接呼び出すバックエンドアクションには `[Visibility::App]` を、UIからツールを利用不可にするには `[Visibility::Model]` を使います。 ### アプリの設定 アプリリソースの `#[AppMeta]` アトリビュートで、iframeのContent Security Policy、ブラウザ権限、ビューの `` に含めるライブラリスクリプトを設定します。 ```php theme={null} use Laravel\Mcp\Server\Attributes\AppMeta; use Laravel\Mcp\Server\Ui\Enums\Library; use Laravel\Mcp\Server\Ui\Enums\Permission; #[AppMeta( connectDomains: ['https://api.weather.com'], permissions: [Permission::Geolocation], libraries: [Library::Tailwind, Library::Alpine], )] class WeatherDashboardApp extends AppResource { // ... } ``` `Library` enumには `Library::Tailwind` や `Library::Alpine` など一般的なフロントエンドライブラリのCDNスクリプトが事前設定されており、CDNオリジンは自動的にCSPにマージされます。`Permission` enumは `Camera`・`Microphone`・`Geolocation`・`ClipboardWrite` などのブラウザ権限をカバーします。 動的な設定が必要な場合は、`Laravel\Mcp\Server\Ui` 名前空間の `AppMeta`・`Csp`・`Permissions` フルエントビルダーを使ってリソースの `appMeta` メソッドをオーバーライドします。 ### Boostを使ったアプリ開発 Laravel MCPにはMCP Appsを構築するための専用[Boost](/jp/boost)スキルリファレンスが含まれています。Laravel Boostがインストールされていれば、AIコーディングエージェントが `mcp-development` スキルを呼び出し、アプリリソース、Bladeビュー、リンクされたツールを自動生成できます。 プロトコルの完全なリファレンス(クライアントサイドAPIやスキーマの詳細を含む)については、公式の[MCP Appsドキュメント](https://modelcontextprotocol.io/extensions/apps/overview)を参照してください。 ## メタデータ MCP仕様の `_meta` フィールドをツール、リソース、プロンプトのレスポンスに付加できます。 ```php theme={null} // レスポンスコンテンツへのメタデータ return Response::text('The weather is sunny.') ->withMeta(['source' => 'weather-api', 'cached' => true]); ``` レスポンスエンベロープ全体にメタデータを付ける場合は `Response::make` を使います。 ```php theme={null} return Response::make( Response::text('The weather is sunny.') )->withMeta(['request_id' => '12345']); ``` ツール・リソース・プロンプトのクラス自体にメタデータを付けるには `$meta` プロパティを定義します。 ```php theme={null} class CurrentWeatherTool extends Tool { protected ?array $meta = [ 'version' => '2.0', 'author' => 'Weather Team', ]; } ``` ## アイコン MCP クライアントはサーバーとそのプリミティブのアイコンを表示できます。`Icon` 属性を使ってサーバー、ツール、リソース、プロンプトにアイコンを宣言できます。 ```php theme={null} use Laravel\Mcp\Enums\IconTheme; use Laravel\Mcp\Server\Attributes\Icon; #[Icon('mcp/server.png', mimeType: 'image/png', sizes: ['48x48'])] #[Icon('mcp/server-dark.svg', theme: IconTheme::Dark)] class WeatherServer extends Server { // ... } ``` `Icon` 属性は繰り返し使用可能なため、異なるサイズやライト・ダークテーマのバリアントを提供するために複数のアイコンを宣言できます。 または、`icons` メソッドをオーバーライドしてプログラムでアイコンを定義することもできます。これはアイコンがランタイムの条件に依存する場合に便利です。 ```php theme={null} use Laravel\Mcp\Schema\Icon; class CurrentWeatherTool extends Tool { /** * ツールのアイコンを取得します。 * * @return array */ public function icons(): array { return [ Icon::from('mcp/tool.png', mimeType: 'image/png'), ]; } } ``` 属性と `icons` メソッドで定義されたアイコンは自動的に結合されます。アイコンパスは以下のように解決されます。 * `https:` や `data:` などの URI スキームを持つパスはそのまま使用されます。 * 相対パスは Laravel の `asset` ヘルパーを使って URL に解決されます。 ## 認証 WebサーバーはLaravelの標準的なミドルウェアで認証できます。 ### Sanctum [Laravel Sanctum](https://laravel.com/docs/sanctum) を使ったトークン認証です。MCPクライアントは `Authorization: Bearer ` ヘッダーを送信します。 ```php theme={null} use App\Mcp\Servers\WeatherServer; use Laravel\Mcp\Facades\Mcp; Mcp::web('/mcp/weather', WeatherServer::class) ->middleware('auth:sanctum'); ``` ### OAuth 2.1 [Laravel Passport](https://laravel.com/docs/passport) を使ったOAuth認証です。より堅牢なセキュリティが必要な場合に適しています。 ```php theme={null} use App\Mcp\Servers\WeatherServer; use Laravel\Mcp\Facades\Mcp; Mcp::oauthRoutes(); Mcp::web('/mcp/weather', WeatherServer::class) ->middleware('auth:api'); ``` OAuth認証を使う場合は、Passportの認可ビューを公開してサービスプロバイダーに設定します。 ```shell theme={null} php artisan vendor:publish --tag=mcp-views ``` ```php theme={null} // AppServiceProvider::boot() use Laravel\Passport\Passport; Passport::authorizationView(function ($parameters) { return view('mcp.authorize', $parameters); }); ``` ## 認可 `$request->user()` で認証済みユーザーを取得し、ツールやリソース内で認可チェックを行えます。 ```php theme={null} public function handle(Request $request): Response { if (! $request->user()->can('read-weather')) { return Response::error('Permission denied.'); } // 処理を続行... } ``` ## MCP クライアント Laravel MCP はサーバーの構築だけでなく、他の MCP サーバーへ接続するためのクライアントも提供しています。クライアントを使うことで、外部 MCP サーバーが公開するツールを発見・呼び出しできます。[AI エージェント](/jp/ai-sdk#mcp-ツール) に外部 MCP サーバーの機能を提供する際にとくに有用です。 ### サーバーへの接続 HTTP でアクセスできる MCP サーバーには `Client::web` メソッドを使い、サーバーの URL を渡します。 ```php theme={null} use Laravel\Mcp\Client; $client = Client::web('https://mcp.example.com'); ``` コマンドとして起動するローカル MCP サーバーへは `Client::local` メソッドを使い、コマンドと引数を渡します。 ```php theme={null} use Laravel\Mcp\Client; $client = Client::local('php', ['artisan', 'mcp:start']); ``` クライアントは遅延接続(lazy connect)し、ツールの一覧取得や呼び出しを初めて行うときに自動的に接続を確立します。接続を手動で管理する場合は `connect`、`connected`、`ping`、`disconnect` メソッドを使います。 ```php theme={null} $client->connect(); $client->ping(); if ($client->connected()) { // ... } $client->disconnect(); ``` `withTimeout` メソッドでリクエストタイムアウトをカスタマイズできます。 ```php theme={null} $client = Client::web('https://mcp.example.com')->withTimeout(30); ``` ### 名前付きクライアント クライアントを毎回構築するかわりに、再利用可能な名前付きクライアントを登録できます。通常は `Mcp` ファサードを使い、サービスプロバイダーの `boot` メソッド内で行います。 ```php theme={null} use Laravel\Mcp\Client; use Laravel\Mcp\Facades\Mcp; Mcp::registerClient('github', fn () => Client::web('https://mcp.example.com')); ``` 登録後は名前でクライアントを解決できます。 ```php theme={null} use Laravel\Mcp\Facades\Mcp; $client = Mcp::client('github'); ``` 名前付きクライアントはリクエストごとに一度だけ解決され、リクエストライフサイクルの終了時に自動的に切断されます。 ### クライアント認証 Bearer トークンで保護されている Web MCP サーバーへ接続するには `withToken` メソッドを使います。トークン文字列または遅延解決するクロージャを渡せます。 ```php theme={null} use Illuminate\Support\Facades\Auth; use Laravel\Mcp\Client; $client = Client::web('https://mcp.example.com')->withToken($token); $client = Client::web('https://mcp.example.com')->withToken( fn () => Auth::user()->mcpToken(), ); ``` [OAuth 2.1](#oauth-2-1) で保護されているサーバーには `withOAuth` メソッドを使います。 ```php theme={null} use Laravel\Mcp\Client; use Laravel\Mcp\Facades\Mcp; Mcp::registerClient('github', fn () => Client::web('https://mcp.example.com')->withOAuth( clientId: config('services.github_mcp.client_id'), clientSecret: config('services.github_mcp.client_secret'), )); ``` MCP サーバーが[動的クライアント登録](https://datatracker.ietf.org/doc/html/rfc7591)をサポートしている場合、`clientId` と `clientSecret` は省略できます。クライアントが自動登録します。 次に、`routes/ai.php` ファイルで名前付きクライアントの OAuth ルートを `oAuthRoutesFor` メソッドを使って登録します。渡すクロージャは認可コードとアクセストークンが交換された後にクライアント名と `TokenSet` を受け取ります。 ```php theme={null} use Illuminate\Support\Facades\Auth; use Laravel\Mcp\Client\OAuth\TokenSet; use Laravel\Mcp\Facades\Mcp; Mcp::oAuthRoutesFor('github', function (string $client, TokenSet $token) { Auth::user()->update([ 'github_mcp_token' => $token->accessToken, ]); return redirect('/dashboard'); }); ``` これにより2つの名前付きルートが登録されます。ユーザーを認可サーバーへリダイレクトする connect ルート(`mcp.oauth.{client}.connect`)と、認可コードを交換してハンドラーを呼び出す callback ルート(`mcp.oauth.{client}.callback`)です。どちらも `web` ミドルウェアグループを使います(`middleware` 引数で上書き可能)。 認可フローを開始するには、ユーザーを connect ルートへリダイレクトします。 ```php theme={null} return redirect()->route('mcp.oauth.github.connect'); ``` ### ツール `tools` メソッドで MCP サーバーが公開するツールを取得します。名前をキーにしたコレクションとして返されます。 ```php theme={null} use Laravel\Mcp\Facades\Mcp; $tools = Mcp::client('github')->tools(); foreach ($tools as $tool) { $tool->name; $tool->title; $tool->description; $tool->inputSchema; } ``` クライアントは自動的にページネーションを処理してすべてのツールを取得します。`limit` 引数で取得数を制限できます。 ```php theme={null} $tools = Mcp::client('github')->tools(limit: 10); ``` ツールを呼び出すには `callTool` メソッドを使い、ツール名と引数の配列を渡します。返される `ToolResult` インスタンスでレスポンスを取得します。 ```php theme={null} use Laravel\Mcp\Facades\Mcp; $result = Mcp::client('github')->callTool('current-weather', [ 'location' => 'New York', ]); $result->text(); // レスポンスのテキストコンテンツ (string) $result; // text() と同等 $result->isError; // ツールがエラーを報告したか $result->structuredContent; // 構造化コンテンツ(存在する場合) ``` 一覧取得したツールインスタンスから直接呼び出すこともできます。 ```php theme={null} $tools = Mcp::client('github')->tools(); $result = $tools['current-weather']->call([ 'location' => 'New York', ]); ``` [Laravel AI SDK](/jp/ai-sdk) でエージェントを構築している場合は、MCP クライアントのツールをエージェントに直接渡すことで、モデルがプロンプトへの応答中にそれらを呼び出せるようになります。詳細は AI SDK の [MCP ツール](/jp/ai-sdk#mcp-ツール) セクションを参照してください。 ### プロンプト `prompts` メソッドで MCP サーバーが公開するプロンプトを取得します。名前をキーにしたコレクションとして返されます。 ```php theme={null} use Laravel\Mcp\Facades\Mcp; $prompts = Mcp::client('github')->prompts(); foreach ($prompts as $prompt) { $prompt->name; $prompt->title; $prompt->description; $prompt->arguments; } ``` クライアントは自動的にページネーションを処理してすべてのプロンプトを取得します。`limit` 引数で取得数を制限できます。 ```php theme={null} $prompts = Mcp::client('github')->prompts(limit: 10); ``` プロンプトを取得するには `getPrompt` メソッドを使い、プロンプト名と引数の配列を渡します。返される `PromptResult` インスタンスで生成されたメッセージを取得できます。 ```php theme={null} use Laravel\Mcp\Facades\Mcp; $result = Mcp::client('github')->getPrompt('describe-weather', [ 'location' => 'New York', ]); $result->text(); // メッセージのテキストコンテンツ (string) $result; // text() と同等 $result->messages; // プロンプトが返したメッセージ(生データ) $result->description; // プロンプトの説明(存在する場合) ``` ### リソース `resources` メソッドで MCP サーバーが公開するリソースを取得します。URI をキーにしたコレクションとして返されます。 ```php theme={null} use Laravel\Mcp\Facades\Mcp; $resources = Mcp::client('github')->resources(); foreach ($resources as $resource) { $resource->uri; $resource->name; $resource->title; $resource->description; $resource->mimeType; $resource->size; } ``` クライアントは自動的にページネーションを処理してすべてのリソースを取得します。`limit` 引数で取得数を制限できます。 ```php theme={null} $resources = Mcp::client('github')->resources(limit: 10); ``` リソースを読み込むには `readResource` メソッドを使い、リソースの URI を渡します。返される `ResourceReadResult` インスタンスでリソースのコンテンツを取得できます。 ```php theme={null} use Laravel\Mcp\Facades\Mcp; $result = Mcp::client('github')->readResource('weather://guidelines'); $result->content(); // リソースのコンテンツ(base64 の blob は自動でデコード) (string) $result; // content() と同等 $result->mimeType(); // リソースの MIME タイプ(存在する場合) $result->contents; // リソースが返したコンテンツ(生データ) ``` ## テスト ### MCP Inspector MCPサーバーの動作確認には、インタラクティブなデバッグツール「MCP Inspector」を使います。 ```shell theme={null} # Webサーバー php artisan mcp:inspector mcp/weather # ローカルサーバー(名前が "weather" の場合) php artisan mcp:inspector weather ``` コマンドを実行するとMCP Inspectorが起動し、クライアント設定をコピーできます。認証ミドルウェアを設定している場合は、Authorizationヘッダーを含めて接続してください。 ### ユニットテスト ツール・リソース・プロンプトに対してユニットテストを書けます。 ```php Pest theme={null} test('tool', function () { $response = WeatherServer::tool(CurrentWeatherTool::class, [ 'location' => 'Tokyo', 'units' => 'celsius', ]); $response ->assertOk() ->assertSee('The current weather in Tokyo is 22°C and sunny.'); }); ``` ```php PHPUnit theme={null} public function test_tool(): void { $response = WeatherServer::tool(CurrentWeatherTool::class, [ 'location' => 'Tokyo', 'units' => 'celsius', ]); $response ->assertOk() ->assertSee('The current weather in Tokyo is 22°C and sunny.'); } ``` プロンプトとリソースも同様にテストできます。 ```php theme={null} $response = WeatherServer::prompt(DescribeWeatherPrompt::class, ['tone' => 'casual']); $response = WeatherServer::resource(WeatherGuidelinesResource::class); ``` 認証済みユーザーとして実行するには `actingAs` を使います。 ```php theme={null} $response = WeatherServer::actingAs($user)->tool(CurrentWeatherTool::class, [...]); ``` 主なアサーションメソッドは次のとおりです。 ```php theme={null} $response->assertOk(); // エラーがないことを確認 $response->assertSee('...'); // 特定のテキストが含まれることを確認 ``` エラーの有無を検証するには `assertHasErrors` / `assertHasNoErrors` を使います。 ```php theme={null} $response->assertHasErrors(); $response->assertHasErrors([ 'Something went wrong.', ]); $response->assertHasNoErrors(); ``` ツール・リソース・プロンプトの名前やタイトル、説明を検証できます。 ```php theme={null} $response->assertName('current-weather'); $response->assertTitle('Current Weather Tool'); $response->assertDescription('Fetches the current weather forecast for a specified location.'); ``` ストリーミングレスポンスのノーティフィケーションを検証するには `assertSentNotification` と `assertNotificationCount` を使います。 ```php theme={null} $response->assertSentNotification('processing/progress', [ 'step' => 1, 'total' => 5, ]); $response->assertSentNotification('processing/progress', [ 'step' => 2, 'total' => 5, ]); $response->assertNotificationCount(5); ``` レスポンスの内容をデバッグするには `dd` または `dump` メソッドを使います。 ```php theme={null} $response->dd(); $response->dump(); ``` # 通知(Notifications) Source: https://kawax.biz/jp/notifications Laravelの通知システムを使ってメール・データベース・Slackなど複数チャンネルに一括送信する方法を解説します。 ## Notificationとは Laravelの通知(Notification)システムは、メール・SMS・Slack・データベースなど複数の配信チャンネルに対して、統一されたAPIで通知を送る仕組みです。 **MailとNotificationの違い** | 比較 | Mail | Notification | | - | - | - | | 主な用途 | リッチなHTMLメール | 短い情報通知 | | 配信チャンネル | メールのみ | メール・DB・Slack・SMSなど複数 | | テンプレート | 完全に自由 | シンプルなメッセージ形式 | 請求書の支払い完了通知のように、複数のチャンネルに同じ通知を送りたい場合はNotificationが適しています。 ## Notificationクラスの作成 `make:notification` Artisanコマンドでクラスを生成します。 ```shell theme={null} php artisan make:notification InvoicePaid ``` 生成されたクラスは `app/Notifications/` ディレクトリに配置されます。クラスには `via()` メソッドと、各チャンネル向けのメッセージ生成メソッドが含まれます。 ## 通知の送信方法 ### Notifiableトレイトを使う `App\Models\User` にはデフォルトで `Notifiable` トレイトが含まれています。`notify()` メソッドで通知を送ります。 ```php theme={null} use App\Notifications\InvoicePaid; $user->notify(new InvoicePaid($invoice)); ``` `Notifiable` トレイトは `User` モデルに限らず、任意のモデルに追加できます。 ### Notificationファサードを使う 複数のユーザーに同時に送信するには `Notification` ファサードを使います。 ```php theme={null} use Illuminate\Support\Facades\Notification; Notification::send($users, new InvoicePaid($invoice)); ``` すぐに送信する (キューをスキップする) には `sendNow()` を使います。 ```php theme={null} Notification::sendNow($developers, new DeploymentCompleted($deployment)); ``` ## 配信チャンネルの指定 `via()` メソッドで使用するチャンネルを配列で返します。 ```mermaid theme={null} flowchart TD A["$user->notify(new InvoicePaid())"] --> B["via() で
    チャンネルを決定"] B --> C{"チャンネル選択"} C --> D["mail
    toMail()"] C --> E["database
    toArray()"] C --> F["broadcast
    toBroadcast()"] C --> G["vonage
    toVonage()"] C --> H["slack
    toSlack()"] ``` ```php theme={null} public function via(object $notifiable): array { return ['mail', 'database']; } ``` ユーザーの設定に応じてチャンネルを切り替えることもできます。 ```php theme={null} public function via(object $notifiable): array { return $notifiable->prefers_sms ? ['vonage'] : ['mail', 'database']; } ``` 利用可能な主なチャンネルは次のとおりです。 | チャンネル | キー | 説明 | | - | - | - | | メール | `mail` | メールで通知を送る | | データベース | `database` | DBに保存してUI上で表示する | | ブロードキャスト | `broadcast` | リアルタイム通知(WebSocket) | | SMS | `vonage` | Vonage(旧Nexmo)経由でSMS送信 | | Slack | `slack` | Slackチャンネルに投稿 | ## メールチャンネルでの通知 `toMail()` メソッドで `MailMessage` インスタンスを返します。 ```php theme={null} use Illuminate\Notifications\Messages\MailMessage; public function toMail(object $notifiable): MailMessage { $url = url('/invoice/'.$this->invoice->id); return (new MailMessage) ->greeting('こんにちは!') ->line('請求書のお支払いを確認しました。') ->action('請求書を確認する', $url) ->line('ご利用いただきありがとうございます。'); } ``` `MailMessage` の主なメソッドは次のとおりです。 | メソッド | 説明 | | - | - | | `greeting()` | 冒頭の挨拶文 | | `line()` | 本文の1行 | | `action()` | ボタンリンク | | `subject()` | 件名 | | `from()` | 送信者アドレス | | `mailer()` | 使用するメーラー | エラーを通知する場合は `error()` メソッドを追加すると、ボタンが赤くなります。 ```php theme={null} return (new MailMessage) ->error() ->subject('お支払いに失敗しました') ->line('請求書のお支払い処理に失敗しました。'); ``` ### Markdown通知メール Markdown形式のリッチなメールを使うこともできます。`--markdown` オプションでクラスを生成します。 ```shell theme={null} php artisan make:notification InvoicePaid --markdown=mail.invoice.paid ``` `toMail()` の代わりに `toMarkdownMail()` を使います。 ```php theme={null} use Illuminate\Notifications\Messages\MailMessage; public function toMail(object $notifiable): MailMessage { return (new MailMessage) ->markdown('mail.invoice.paid', [ 'url' => url('/invoice/'.$this->invoice->id), 'invoice' => $this->invoice, ]); } ``` ## データベースチャンネルでの通知 データベースチャンネルを使うと、通知をDBに保存してアプリのUI上で表示できます。 ### テーブルの準備 まず `notifications` テーブルを作成します。 ```shell theme={null} php artisan make:notifications-table php artisan migrate ``` ### toArrayメソッドの定義 `toArray()` メソッドで保存するデータを配列で返します。 ```php theme={null} public function toArray(object $notifiable): array { return [ 'invoice_id' => $this->invoice->id, 'amount' => $this->invoice->amount, 'message' => '請求書のお支払いを確認しました。', ]; } ``` このデータは `notifications` テーブルの `data` カラムにJSON形式で保存されます。 ### 通知の取得 `Notifiable` トレイトが提供する `notifications` リレーションで通知を取得できます。 ```php theme={null} $user = App\Models\User::find(1); foreach ($user->notifications as $notification) { echo $notification->type; echo $notification->data['message']; } ``` 未読の通知だけを取得するには `unreadNotifications` を使います。 ```php theme={null} foreach ($user->unreadNotifications as $notification) { echo $notification->data['message']; } ``` ### 既読にする `markAsRead()` で通知を既読にします。 ```php theme={null} // 個別に既読にする foreach ($user->unreadNotifications as $notification) { $notification->markAsRead(); } // まとめて既読にする $user->unreadNotifications->markAsRead(); // クエリで一括更新する $user->unreadNotifications()->update(['read_at' => now()]); ``` ## 通知のキュー処理 通知の送信に時間がかかる場合は、`ShouldQueue` インターフェースと `Queueable` トレイトを追加してキュー処理にします。`make:notification` で生成したクラスには最初からインポートされています。 ```php theme={null} notify(new InvoicePaid($invoice)); ``` 遅延送信も可能です。 ```php theme={null} $user->notify( (new InvoicePaid($invoice))->delay(now()->plus(minutes: 10)) ); ``` ## 複数チャンネルへの同時送信 `via()` メソッドで複数のチャンネルを返し、それぞれのメソッドを定義するだけで、同じ通知を複数チャンネルに同時送信できます。 ```php theme={null} subject('請求書のお支払いを確認しました') ->line('請求書 #'.$this->invoice->id.' のお支払いを確認しました。') ->action('請求書を確認する', url('/invoice/'.$this->invoice->id)); } public function toArray(object $notifiable): array { return [ 'invoice_id' => $this->invoice->id, 'amount' => $this->invoice->amount, ]; } } ``` ## オンデマンド通知 アプリにアカウントを持たないユーザーにも通知を送りたい場合は、`Notification::route()` を使います。 ```php theme={null} use Illuminate\Support\Facades\Notification; Notification::route('mail', 'guest@example.com') ->route('vonage', '5555551212') ->notify(new InvoicePaid($invoice)); ``` ## 通知のテスト `Notification::fake()` を使うと、実際に通知を送信せずにアサートできます。 ```php theme={null} use App\Notifications\InvoicePaid; use Illuminate\Support\Facades\Notification; test('請求書支払い時にユーザーへ通知される', function () { Notification::fake(); $user = User::factory()->create(); $invoice = Invoice::factory()->for($user)->create(); $this->post("/invoices/{$invoice->id}/pay"); Notification::assertSentTo($user, InvoicePaid::class, function ($notification) use ($invoice) { return $notification->invoice->id === $invoice->id; }); }); ``` `assertSentToOnce`(Laravel 13で追加)を使うと、指定したユーザーに対して通知がちょうど1回だけ送信されたことをアサートできます。 ```php theme={null} Notification::fake(); // ... // ユーザーに2回通知が送られたことをアサート Notification::assertSentTimes($user, InvoicePaid::class, 2); // ユーザーにちょうど1回だけ通知が送られたことをアサート Notification::assertSentToOnce($user, InvoicePaid::class); ``` オンデマンド通知(`Notification::route()`)についても、`assertSentOnDemandOnce` でちょうど1回だけ送信されたことをアサートできます。 ```php theme={null} Notification::assertSentOnDemand(InvoicePaid::class); Notification::assertSentOnDemandOnce(InvoicePaid::class); ``` ## まとめ | やりたいこと | 方法 | | - | - | | Notificationを作成する | `php artisan make:notification ClassName` | | ユーザーに通知する | `$user->notify(new MyNotification())` | | 複数ユーザーに通知する | `Notification::send($users, new MyNotification())` | | メールで送る | `via()` で `mail` を返し `toMail()` を定義する | | DBに保存する | `via()` で `database` を返し `toArray()` を定義する | | キューで非同期送信する | `ShouldQueue` を実装する | | 複数チャンネルに送る | `via()` で複数のキーを返す | # Laravel Octane Source: https://kawax.biz/jp/octane FrankenPHP、Swoole、RoadRunner を使ってLaravelアプリケーションを高速化する ## Octane とは [Laravel Octane](https://github.com/laravel/octane) は、高性能なアプリケーションサーバーを使って Laravel アプリケーションのパフォーマンスを劇的に向上させるパッケージです。 通常の PHP-FPM では、リクエストのたびにアプリケーションが起動・終了します。Octane ではアプリケーションを一度だけ起動してメモリに保持し、続くリクエストを高速に処理します。 ```mermaid theme={null} sequenceDiagram participant Client participant Nginx participant Octane as Octane
    (常駐プロセス) participant App as Laravel
    アプリ Client->>Nginx: リクエスト Nginx->>Octane: プロキシ Octane->>App: リクエスト処理(メモリ内) App->>Nginx: レスポンス Nginx->>Client: レスポンス ``` PHP-FPM との最大の違いは、アプリケーションのブート処理(サービスプロバイダの登録など)がリクエストごとに繰り返されない点です。1 回起動すればあとは超高速でリクエストを処理できます。 ## 対応サーバー Octane は以下の 3 つのサーバーをサポートしています。 ```mermaid theme={null} flowchart LR A["サーバー選択"] --> B{"並行タスクや
    Swoole機能が必要?"} B -->|"Yes"| C["Swoole
    (PECL拡張)"] B -->|"No"| D{"HTTP/3・Brotli
    圧縮が必要?"} D -->|"Yes"| E["FrankenPHP
    (推奨)"] D -->|"No"| F["RoadRunner
    (Go製バイナリ)"] ``` | サーバー | 言語 | インストール | 並行タスク | 備考 | | - | - | - | - | - | | FrankenPHP | Go | 自動(バイナリ) | — | 推奨。HTTP/3・Brotli 対応 | | RoadRunner | Go | 自動(バイナリ) | — | シンプルで設定が容易 | | Swoole | PHP 拡張 | PECL で手動 | ✅ | 並行タスク・Octane キャッシュが使える | Laravel Cloud では FrankenPHP を使った Octane 運用が推奨されており、フルマネージドでサポートされています。 ## インストールと設定 ### パッケージのインストール ```bash theme={null} composer require laravel/octane ``` ### Octane の初期設定 ```bash theme={null} php artisan octane:install ``` 実行するとサーバーを選択するプロンプトが表示されます。選択すると `config/octane.php` が生成されます。 ### FrankenPHP FrankenPHP を選択すると Octane が自動でバイナリをダウンロードします。追加の手順は不要です。 ### RoadRunner RoadRunner を選択すると Octane が自動でバイナリをダウンロードします。 ### Swoole Swoole は PHP 拡張として PECL でインストールします。 ```bash theme={null} pecl install swoole ``` Open Swoole を使う場合は以下のコマンドでインストールします。 ```bash theme={null} pecl install openswoole ``` ## 起動方法 ### サーバーの起動 ```bash theme={null} php artisan octane:start ``` デフォルトではポート `8000` で起動します。`http://localhost:8000` でアクセスできます。 ### サーバーの指定 ```bash theme={null} php artisan octane:start --server=frankenphp php artisan octane:start --server=roadrunner php artisan octane:start --server=swoole ``` ### ワーカー数の指定 ```bash theme={null} php artisan octane:start --workers=4 ``` Swoole でタスクワーカーも指定する場合は以下のとおりです。 ```bash theme={null} php artisan octane:start --workers=4 --task-workers=6 ``` ### ファイル変更の監視 開発中はファイルを変更してもサーバーを再起動しないと反映されません。`--watch` フラグを使うと自動で再起動します。 ```bash theme={null} php artisan octane:start --watch ``` `--watch` を使うには [Node.js](https://nodejs.org) と Chokidar が必要です。 ```bash theme={null} npm install --save-dev chokidar ``` ### その他の管理コマンド ```bash theme={null} # ワーカーのリロード(デプロイ後に実行) php artisan octane:reload # サーバーの停止 php artisan octane:stop # サーバーの状態確認 php artisan octane:status ``` ## 依存性注入の注意点 Octane ではアプリケーションがメモリに保持されるため、**サービスプロバイダの `register` や `boot` はサーバー起動時に 1 回しか実行されません**。同じアプリケーションインスタンスがリクエスト間で使い回されるため、コンテナやリクエストをオブジェクトのコンストラクタに注入する場合は注意が必要です。 ```mermaid theme={null} flowchart LR A["サーバー起動
    (1回のみ)"] --> B["ServiceProvider
    register / boot"] B --> C["アプリケーション
    インスタンス(メモリ)"] C --> D["リクエスト1"] C --> E["リクエスト2"] C --> F["リクエスト3..."] ``` ### コンテナのインジェクション シングルトンのコンストラクタにコンテナを直接注入すると、古いコンテナが使い回されます。 ```php theme={null} // ❌ 問題のある例 — 古いコンテナが使い回される $this->app->singleton(Service::class, function (Application $app) { return new Service($app); }); // ✅ 安全な例 — クロージャで毎回最新のコンテナを取得 $this->app->singleton(Service::class, function () { return new Service(fn () => Container::getInstance()); }); ``` グローバルヘルパー `app()` と `Container::getInstance()` は常に最新のコンテナを返すため安全です。 ### リクエストのインジェクション ```php theme={null} // ❌ 問題のある例 — 古いリクエストが使い回される $this->app->singleton(Service::class, function (Application $app) { return new Service($app['request']); }); // ✅ 安全な例 — クロージャで毎回最新のリクエストを取得 $this->app->singleton(Service::class, function (Application $app) { return new Service(fn () => $app['request']); }); // ✅ 最も推奨 — 必要な値だけメソッドに渡す $service->method($request->input('name')); ``` コントローラーメソッドやルートクロージャで `Illuminate\Http\Request` をタイプヒントするのは安全です。グローバルヘルパー `request()` も常に現在のリクエストを返します。 ### 設定リポジトリのインジェクション ```php theme={null} // ❌ 問題のある例 — 設定値が変わっても古いリポジトリが使われる $this->app->singleton(Service::class, function (Application $app) { return new Service($app->make('config')); }); // ✅ 安全な例 $this->app->singleton(Service::class, function () { return new Service(fn () => Container::getInstance()->make('config')); }); ``` グローバルヘルパー `config()` は常に最新の設定リポジトリを返すため安全です。 ## メモリリーク対策 Octane はアプリケーションをメモリに保持するため、静的プロパティなどにデータを蓄積するとメモリリークが発生します。 ```php theme={null} // ❌ メモリリークの例 — リクエストごとに $data が増え続ける class Service { public static array $data = []; } public function index(Request $request): array { Service::$data[] = Str::random(10); return []; } ``` ### max-requests でワーカーをリサイクル ワーカーが一定数のリクエストを処理したら自動的に再起動させることでメモリリークを軽減できます。デフォルトは 500 リクエストです。 ```bash theme={null} php artisan octane:start --max-requests=250 ``` ### 最大実行時間の設定 `config/octane.php` でリクエストの最大実行時間を設定できます。デフォルトは 30 秒です。 ```php theme={null} 'max_execution_time' => 30, ``` `max_execution_time` を変更した場合は Octane サーバーを再起動してください。 ## 並行タスク(Swoole 限定) Swoole を使用している場合、`Octane::concurrently()` で複数の処理を並行実行できます。 ```php theme={null} use App\Models\User; use App\Models\Server; use Laravel\Octane\Facades\Octane; [$users, $servers] = Octane::concurrently([ fn () => User::all(), fn () => Server::all(), ]); ``` 並行タスクは Swoole の「タスクワーカー」として別プロセスで実行されます。タスクワーカー数は `--task-workers` で指定します。 ```bash theme={null} php artisan octane:start --workers=4 --task-workers=6 ``` `concurrently` に渡せるタスクは最大 1024 個です(Swoole の制限)。 ## Tick と Interval(Swoole 限定) Swoole では指定した間隔で定期実行する処理を登録できます。サービスプロバイダの `boot` メソッドで登録します。 ```php theme={null} use Laravel\Octane\Facades\Octane; // 10 秒ごとに実行 Octane::tick('delayed-ticker', fn () => ray('Ticking...')) ->seconds(10); // サーバー起動直後にも即実行 Octane::tick('immediate-ticker', fn () => ray('Ticking...')) ->seconds(10) ->immediate(); ``` ## Octane キャッシュ(Swoole 限定) Swoole の [Swoole テーブル](https://www.swoole.co.uk/docs/modules/swoole-table) を使った超高速インメモリキャッシュです。毎秒最大 200 万回の読み書きが可能です。 ```php theme={null} use Illuminate\Support\Facades\Cache; Cache::store('octane')->put('framework', 'Laravel', 30); ``` ### インターバルキャッシュ 指定した間隔で自動更新されるキャッシュです。 ```php theme={null} use Illuminate\Support\Str; Cache::store('octane')->interval('random', function () { return Str::random(10); }, seconds: 5); ``` Octane キャッシュのデータはサーバーを再起動するとすべて消去されます。 ## Swoole テーブル(Swoole 限定) 全ワーカーからアクセス可能な任意のインメモリテーブルを定義できます。`config/octane.php` の `tables` で設定します。 ```php theme={null} 'tables' => [ 'example:1000' => [ 'name' => 'string:1000', 'votes' => 'int', ], ], ``` テーブルへのアクセスは `Octane::table()` メソッドを使います。 ```php theme={null} use Laravel\Octane\Facades\Octane; Octane::table('example')->set('uuid', [ 'name' => 'Nuno Maduro', 'votes' => 1000, ]); $row = Octane::table('example')->get('uuid'); ``` Swoole テーブルで使えるカラム型は `string`、`int`、`float` のみです。データはサーバー再起動で消えます。 ## 本番環境での運用 ### Nginx + Octane の構成 本番環境では Nginx の背後で Octane を動かすのが一般的です。Nginx が静的ファイルの配信と SSL 終端を担い、動的リクエストを Octane にプロキシします。 ```nginx theme={null} map $http_upgrade $connection_upgrade { default upgrade; '' close; } server { listen 80; server_name example.com; root /home/forge/example.com/public; location /index.php { try_files /not_exists @octane; } location / { try_files $uri $uri/ @octane; } location @octane { set $suffix ""; if ($uri = /index.php) { set $suffix ?$query_string; } proxy_http_version 1.1; proxy_set_header Host $http_host; proxy_set_header Scheme $scheme; proxy_set_header SERVER_PORT $server_port; proxy_set_header REMOTE_ADDR $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_pass http://127.0.0.1:8000$suffix; } } ``` ### Supervisor で常駐させる 本番環境では Supervisor を使って Octane を常時起動しておきます。 ```ini theme={null} [program:octane] process_name=%(program_name)s_%(process_num)02d command=php /home/forge/example.com/artisan octane:start --server=frankenphp --host=127.0.0.1 --port=8000 autostart=true autorestart=true user=forge redirect_stderr=true stdout_logfile=/home/forge/example.com/storage/logs/octane.log stopwaitsecs=3600 ``` ### HTTPS の有効化 Octane 経由で HTTPS リンクを生成するには `.env` に以下を設定します。 ```ini theme={null} OCTANE_HTTPS=true ``` ### Laravel Cloud との連携 [Laravel Cloud](https://cloud.laravel.com) では FrankenPHP を使った Octane をフルマネージドでサポートしています。Nginx や Supervisor の設定は不要で、たった 2 ステップで Octane を有効化できます。 Octane をインストールします。`octane:install` コマンドは実行してもかまいませんが必須ではありません。 ```bash theme={null} composer require laravel/octane ``` 環境の App コンピュートクラスタの設定を開き、**「Use Octane as runtime」** をオンにして保存・デプロイします。Laravel Cloud が自動的に FrankenPHP + Octane でアプリケーションをビルド・起動します。 詳細は [Laravel Cloud のドキュメント](https://cloud.laravel.com/docs/compute#laravel-octane) を参照してください。 ### デプロイ後のワーカーリロード デプロイ後は新しいコードをメモリに読み込むため、ワーカーをリロードします。 ```bash theme={null} php artisan octane:reload ``` ## `php artisan dev` でローカルのサーバーを指定する Octane をインストールすると、`php artisan dev` の `server` プロセスも Octane が使う構成になることがあります。Laravel Cloud の本番環境では Octane を使い、ローカルでは通常の `php artisan serve` を使いたい場合は、`AppServiceProvider` の `register` メソッドで `server` コマンドを登録し直します。 ```php theme={null} use Illuminate\Foundation\DevCommands; class AppServiceProvider extends ServiceProvider { public function register(): void { DevCommands::artisan('serve', 'server'); } } ``` これで `php artisan dev` を実行したとき、ローカルの `server` プロセスは `php artisan serve` で起動します。Laravel Cloud 側では Octane をランタイムとして有効にしたまま運用できます。 # Laravel Passport(OAuth2サーバー実装) Source: https://kawax.biz/jp/passport Laravel PassportでOAuth2サーバーを実装する方法を解説します。Sanctumとの使い分け、インストール、クライアント管理、スコープとトークン運用までを扱います。 ## Passportとは Laravel Passport は、Laravel アプリを OAuth2 認可サーバーとして動かすための公式パッケージです。 サードパーティアプリ連携や、厳密な OAuth2 フローが必要な API で使います。 ```mermaid theme={null} sequenceDiagram participant User as ユーザー participant Client as OAuthクライアント participant App as Laravelアプリ
    (Passport) participant API as 保護されたAPI User->>Client: 連携を開始 Client->>App: 認可リクエスト App->>User: 同意画面を表示 User->>App: 許可 App-->>Client: 認可コード Client->>App: 認可コードをアクセストークンへ交換 App-->>Client: アクセストークン Client->>API: Bearerトークン付きでAPI呼び出し API-->>Client: レスポンス ``` ## Passport vs Sanctum OAuth2 が必須なら Passport を選びます。 単純な API トークン認証や SPA / モバイル認証が目的なら Sanctum を選びます。 | 観点 | Passport | Sanctum | | - | - | - | | 目的 | OAuth2サーバー実装 | シンプルなAPI認証 | | 向いているケース | 外部アプリ連携、OAuth2標準準拠、[MCPサーバー](/jp/mcp#oauth-2-1) | 自社SPA、モバイル、個人用トークン | | 複雑さ | 高い | 低い | AIクライアントからアクセスされる[MCPサーバー](/jp/mcp)を構築する場合はPassportを使うことが公式に推奨されています。MCPクライアントは通常OAuthでの認証を前提としているためです。 ## インストール Laravel 13 の公式推奨は `install:api --passport` です。 ```shell theme={null} php artisan install:api --passport ``` 既存プロジェクトで手動導入する場合は、次のコマンドでもセットアップできます。 ```shell theme={null} composer require laravel/passport php artisan passport:install ``` 初回デプロイ時は鍵生成だけ実行したいケースもあります。 ```shell theme={null} php artisan passport:keys ``` ## 設定 ### Userモデル `User` モデルに `HasApiTokens` トレイトと `OAuthenticatable` インターフェースを追加します。 ```php theme={null} use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Passport\Contracts\OAuthenticatable; use Laravel\Passport\HasApiTokens; class User extends Authenticatable implements OAuthenticatable { use HasApiTokens, HasFactory, Notifiable; } ``` ### authガード `config/auth.php` の `api` ガードで `passport` ドライバーを使います。 ```php theme={null} 'guards' => [ 'api' => [ 'driver' => 'passport', 'provider' => 'users', ], ], ``` ### サービスプロバイダー設定 `AppServiceProvider` の `boot()` でスコープ定義とトークン有効期限を設定できます。 ```php theme={null} use Carbon\CarbonInterval; use Laravel\Passport\Passport; public function boot(): void { Passport::tokensCan([ 'orders:read' => '注文の参照', 'orders:create' => '注文の作成', ]); Passport::defaultScopes(['orders:read']); Passport::tokensExpireIn(CarbonInterval::days(15)); Passport::refreshTokensExpireIn(CarbonInterval::days(30)); Passport::personalAccessTokensExpireIn(CarbonInterval::months(6)); } ``` ## クライアント管理 ### 認可コードグラント用クライアント ```shell theme={null} php artisan passport:client ``` このクライアントは、ユーザー同意画面を伴う OAuth2 の標準フローで使います。 ### クライアントクレデンシャルグラント用クライアント ```shell theme={null} php artisan passport:client --client ``` マシン間通信のエンドポイントでは `EnsureClientIsResourceOwner` ミドルウェアを使います。 ```php theme={null} use Laravel\Passport\Http\Middleware\EnsureClientIsResourceOwner; Route::get('/orders', function () { // ... })->middleware(EnsureClientIsResourceOwner::using('orders:read')); ``` ## トークン管理 ### スコープ付与 ```php theme={null} $accessToken = $user->createToken( 'dashboard-token', ['orders:read', 'orders:create'] )->accessToken; ``` ### スコープチェック ```php theme={null} use Laravel\Passport\Http\Middleware\CheckToken; Route::get('/orders', function () { // ... })->middleware(['auth:api', CheckToken::using('orders:read')]); ``` ### 失効 ```php theme={null} use Laravel\Passport\Passport; $token = Passport::token()->find($tokenId); $token?->revoke(); ``` ## APIルートの保護 ユーザーアクセストークンで保護する API には `auth:api` を付けます。 ```php theme={null} Route::middleware('auth:api')->group(function () { Route::get('/user', fn (Request $request) => $request->user()); Route::get('/orders', [OrderController::class, 'index']); }); ``` クライアントクレデンシャルグラントのルートは `auth:api` ではなく `EnsureClientIsResourceOwner` を使ってください。 ## Personal Access Token OAuth2 の完全フローを使わず、ユーザー自身が API トークンを発行する用途に向いています。 ```shell theme={null} php artisan passport:client --personal ``` ```php theme={null} $token = $request->user()->createToken('cli-token', ['orders:read'])->accessToken; ``` Personal Access Token が主用途なら、Laravel公式でも Sanctum を検討することが推奨されています。 ## 関連リンク * [Laravel公式ドキュメント: Passport](https://laravel.com/docs/13.x/passport) * [Laravel公式ドキュメント: Sanctum](https://laravel.com/docs/13.x/sanctum) # Laravel Pennant Source: https://kawax.biz/jp/pennant Laravel Pennantを使った機能フラグ(Feature Flags)の管理方法を解説します。フィーチャーの定義から確認、スコープ設定、テストまで幅広くカバーします。 ## Laravel Pennantとは [Laravel Pennant](https://github.com/laravel/pennant) は、シンプルで軽量な機能フラグ(Feature Flag)パッケージです。機能フラグを使うと、新機能を段階的にロールアウトしたり、A/Bテストを実施したり、トランクベース開発を補完したりできます。 ### 機能フラグとは ```mermaid theme={null} flowchart TD A["リクエスト"] --> B{"Feature::active('new-api')"} B -->|true| C["新しいAPI処理"] B -->|false| D["従来のAPI処理"] C --> E["レスポンス"] D --> E ``` 機能フラグを使うことで、コードのデプロイとリリースを分離できます。コードは本番環境にデプロイしておきながら、機能のON/OFFを設定で制御できます。 *** ## インストール Composerを使ってPennantをインストールします。 ```bash theme={null} composer require laravel/pennant ``` `vendor:publish` Artisanコマンドでファイルを公開します。 ```bash theme={null} php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider" ``` これにより `config/pennant.php` と `database/migrations` にマイグレーションファイルが生成されます。 Pennantがフィーチャーフラグの値を保存する `features` テーブルを作成します。 ```bash theme={null} php artisan migrate ``` *** ## 設定 `config/pennant.php` で使用するストレージドライバーを設定できます。Pennantは2種類のドライバーをサポートしています。 | ドライバー | 説明 | | - | - | | `database` | リレーショナルDBに値を永続保存(デフォルト) | | `array` | インメモリに保存(テストや一時的な利用向け) | ```php theme={null} // config/pennant.php 'default' => env('PENNANT_STORE', 'database'), ``` *** ## フィーチャーの定義 ### クロージャーベースの定義 フィーチャーは `Feature` ファサードの `define` メソッドで定義します。通常はサービスプロバイダーの `boot` メソッドで定義します。クロージャーには「スコープ」(通常は認証済みユーザー)が渡されます。 ```php theme={null} match (true) { $user->isInternalTeamMember() => true, $user->isHighTrafficCustomer() => false, default => Lottery::odds(1 / 100), }); } } ``` このフィーチャーのロジックは以下の通りです。 * 社内チームメンバーは必ずON * 高トラフィック顧客はOFF * それ以外は1%の確率でON フィーチャーが初めてチェックされたとき、クロージャーの結果がストレージドライバーに保存されます。次回以降は保存された値が使われます。 定義がLotteryを返すだけの場合、クロージャーを省略できます。 ```php theme={null} Feature::define('site-redesign', Lottery::odds(1, 1000)); ``` ### クラスベースの定義 Pennantはクラスベースのフィーチャー定義もサポートしています。クラスベースの場合、サービスプロバイダーへの登録は不要です。 ```bash theme={null} php artisan pennant:feature NewApi ``` 生成されたクラスは `app/Features` ディレクトリに配置されます。`resolve` メソッドを実装するだけです。 ```php theme={null} isInternalTeamMember() => true, $user->isHighTrafficCustomer() => false, default => Lottery::odds(1 / 100), }; } } ``` #### 保存名のカスタマイズ デフォルトでは完全修飾クラス名が保存されます。`Name` アトリビュートで名前をカスタマイズできます。 ```php theme={null} use Laravel\Pennant\Attributes\Name; #[Name('new-api')] class NewApi { // ... } ``` #### フィーチャーチェックの傍受 (`before` メソッド) クラスベースのフィーチャーには `before` メソッドを定義できます。このメソッドはストレージから値を取得する前にインメモリで実行され、`null` 以外の値を返すとその値が使われます。 ```php theme={null} class NewApi { public function before(User $user): mixed { if (Config::get('features.new-api.disabled')) { return $user->isInternalTeamMember(); } } public function resolve(User $user): mixed { // ... } } ``` `before` メソッドはバグ発生時に機能を緊急無効化したり、特定日時にロールアウトをスケジュールしたりする際に役立ちます。 *** ## フィーチャーの確認 ### `Feature::active()` / `Feature::inactive()` `active` メソッドでフィーチャーがアクティブかどうかを確認できます。デフォルトでは現在認証済みのユーザーに対してチェックが行われます。 ```php theme={null} use Laravel\Pennant\Feature; if (Feature::active('new-api')) { // 新しいAPIを使う処理 } ``` クラスベースのフィーチャーの場合はクラス名を渡します。 ```php theme={null} use App\Features\NewApi; if (Feature::active(NewApi::class)) { // ... } ``` その他の便利なメソッドも用意されています。 ```php theme={null} // すべてのフィーチャーがアクティブか Feature::allAreActive(['new-api', 'site-redesign']); // いずれかのフィーチャーがアクティブか Feature::someAreActive(['new-api', 'site-redesign']); // フィーチャーが非アクティブか Feature::inactive('new-api'); // すべてのフィーチャーが非アクティブか Feature::allAreInactive(['new-api', 'site-redesign']); // いずれかのフィーチャーが非アクティブか Feature::someAreInactive(['new-api', 'site-redesign']); ``` ### 条件付き実行 (`when` / `unless`) `when` メソッドを使うと、フィーチャーがアクティブな場合のみクロージャーを実行できます。 ```php theme={null} return Feature::when(NewApi::class, fn () => $this->resolveNewApiResponse($request), fn () => $this->resolveLegacyApiResponse($request), ); ``` `unless` は `when` の逆で、フィーチャーが非アクティブな場合に最初のクロージャーを実行します。 ```php theme={null} return Feature::unless(NewApi::class, fn () => $this->resolveLegacyApiResponse($request), fn () => $this->resolveNewApiResponse($request), ); ``` ### `HasFeatures` トレイト `HasFeatures` トレイトを `User` モデルに追加すると、モデルから直接フィーチャーをチェックできます。 ```php theme={null} use Laravel\Pennant\Concerns\HasFeatures; class User extends Authenticatable { use HasFeatures; } ``` ```php theme={null} if ($user->features()->active('new-api')) { // ... } // 値の取得 $value = $user->features()->value('purchase-button'); // 条件付き実行 $user->features()->when('new-api', fn () => /* ... */, fn () => /* ... */, ); ``` ### Bladeディレクティブ Bladeテンプレートでは `@feature` ディレクティブを使えます。 ```blade theme={null} @feature('site-redesign') {{-- 'site-redesign' がアクティブの場合 --}} @else {{-- 'site-redesign' が非アクティブの場合 --}} @endfeature @featureany(['site-redesign', 'beta']) {{-- どちらかがアクティブの場合 --}} @endfeatureany ``` ### ミドルウェア `EnsureFeaturesAreActive` ミドルウェアを使うと、ルートへのアクセスにフィーチャーが必要であることを指定できます。フィーチャーが非アクティブな場合は `400 Bad Request` が返されます。 ```php theme={null} use Laravel\Pennant\Middleware\EnsureFeaturesAreActive; Route::get('/api/servers', function () { // ... })->middleware(EnsureFeaturesAreActive::using('new-api', 'servers-api')); ``` レスポンスをカスタマイズするには `whenInactive` メソッドを使います。 ```php theme={null} EnsureFeaturesAreActive::whenInactive( function (Request $request, array $features) { return new Response(status: 403); } ); ``` ### インメモリキャッシュ Pennantは1リクエスト内でフィーチャーの結果をインメモリにキャッシュします。同じフィーチャーフラグを複数回チェックしても追加のDBクエリは発生しません。 キャッシュを手動でクリアするには `flushCache` メソッドを使います。 ```php theme={null} Feature::flushCache(); ``` *** ## スコープ ### スコープの指定 デフォルトでは認証済みユーザーがスコープになりますが、`for` メソッドで任意のスコープを指定できます。 ```php theme={null} // 特定ユーザーに対してチェック Feature::for($user)->active('new-api'); // チームに対してチェック Feature::for($user->team)->active('billing-v2'); ``` チームごとにフィーチャーを管理する例です。 ```php theme={null} Feature::define('billing-v2', function (Team $team) { if ($team->created_at->isAfter(new Carbon('1st Jan, 2023'))) { return true; } if ($team->created_at->isAfter(new Carbon('1st Jan, 2019'))) { return Lottery::odds(1 / 100); } return Lottery::odds(1 / 1000); }); ``` ### デフォルトスコープのカスタマイズ `Feature::resolveScopeUsing` でデフォルトスコープをカスタマイズできます。 ```php theme={null} Feature::resolveScopeUsing(fn ($driver) => Auth::user()?->team); ``` 設定後は `for` を省略するとデフォルトスコープが使われます。 ```php theme={null} Feature::active('billing-v2'); // 上記は以下と同等 Feature::for($user->team)->active('billing-v2'); ``` ### Nullable Scope スコープが `null` の場合(未認証ルート、Artisanコマンドなど)、フィーチャー定義がnullに対応していないと自動的に `false` が返されます。nullを扱う場合はnullable型で定義してください。 ```php theme={null} Feature::define('new-api', fn (User|null $user) => match (true) { $user === null => true, $user->isInternalTeamMember() => true, $user->isHighTrafficCustomer() => false, default => Lottery::odds(1 / 100), }); ``` *** ## リッチフィーチャー値 フィーチャーはboolean以外の値も返せます。例えばA/Bテストでボタンの色を制御する場合です。 ```php theme={null} Feature::define('purchase-button', fn (User $user) => Arr::random([ 'blue-sapphire', 'seafoam-green', 'tart-orange', ])); ``` 値を取得するには `value` メソッドを使います。 ```php theme={null} $color = Feature::value('purchase-button'); ``` Bladeでは値を使った条件分岐もできます。 ```blade theme={null} @feature('purchase-button', 'blue-sapphire') {{-- blue-sapphire がアクティブ --}} @elsefeature('purchase-button', 'seafoam-green') {{-- seafoam-green がアクティブ --}} @elsefeature('purchase-button', 'tart-orange') {{-- tart-orange がアクティブ --}} @endfeature ``` リッチ値を使う場合、`false` 以外のすべての値がアクティブとみなされます。 `when` メソッドにリッチ値が渡される場合、最初のクロージャーに値が渡されます。 ```php theme={null} Feature::when('purchase-button', fn ($color) => /* $color に値が入る */, fn () => /* 非アクティブ時 */, ); ``` *** ## 複数フィーチャーの取得 `values` メソッドで複数のフィーチャーの値を一度に取得できます。 ```php theme={null} Feature::values(['billing-v2', 'purchase-button']); // [ // 'billing-v2' => false, // 'purchase-button' => 'blue-sapphire', // ] ``` `all` メソッドで定義済みのすべてのフィーチャーの値を取得できます。 ```php theme={null} Feature::all(); ``` クラスベースのフィーチャーを `all` の結果に含めるには、サービスプロバイダーで `discover` を呼び出します。 ```php theme={null} Feature::discover(); ``` これにより `app/Features` ディレクトリのすべてのフィーチャークラスが登録されます。 *** ## Eager Loading ループ内でフィーチャーチェックを行う場合、パフォーマンスの問題が発生することがあります。`load` メソッドを使って事前に値を取得しておくことで解決できます。 ```php theme={null} // NG: ループごとにDBクエリが発生 foreach ($users as $user) { if (Feature::for($user)->active('notifications-beta')) { $user->notify(new RegistrationSuccess); } } // OK: 事前に一括取得 Feature::for($users)->load(['notifications-beta']); foreach ($users as $user) { if (Feature::for($user)->active('notifications-beta')) { $user->notify(new RegistrationSuccess); } } ``` 未取得の値のみを取得するには `loadMissing` を使います。 ```php theme={null} Feature::for($users)->loadMissing([ 'new-api', 'purchase-button', 'notifications-beta', ]); ``` *** ## 値の更新 ### 手動での更新 `activate` / `deactivate` メソッドでフィーチャーのON/OFFを切り替えられます。 ```php theme={null} // デフォルトスコープでアクティブにする Feature::activate('new-api'); // 特定スコープで非アクティブにする Feature::for($user->team)->deactivate('billing-v2'); // リッチ値を設定する Feature::activate('purchase-button', 'seafoam-green'); ``` 保存された値を忘れさせるには `forget` メソッドを使います。次回チェック時に定義から再評価されます。 ```php theme={null} Feature::forget('purchase-button'); ``` ### 一括更新 `activateForEveryone` / `deactivateForEveryone` でストレージ内のすべてのスコープに一括適用できます。 ```php theme={null} Feature::activateForEveryone('new-api'); Feature::activateForEveryone('purchase-button', 'seafoam-green'); Feature::deactivateForEveryone('new-api'); ``` ### フィーチャーのパージ フィーチャーをアプリケーションから削除した場合や定義を変更した場合、ストレージから値を削除(パージ)できます。 ```php theme={null} // 単一フィーチャーをパージ Feature::purge('new-api'); // 複数フィーチャーをパージ Feature::purge(['new-api', 'purchase-button']); // すべてのフィーチャーをパージ Feature::purge(); ``` Artisanコマンドでもパージできます。デプロイパイプラインに組み込むと便利です。 ```bash theme={null} php artisan pennant:purge new-api # 複数指定 php artisan pennant:purge new-api purchase-button # 指定フィーチャー以外をすべてパージ php artisan pennant:purge --except=new-api --except=purchase-button # サービスプロバイダーに登録済みのフィーチャー以外をパージ php artisan pennant:purge --except-registered ``` *** ## テスト ### フィーチャーの再定義 テストでは `Feature::define` でフィーチャーを再定義することで、返り値を制御できます。 ```php tab=Pest theme={null} use Laravel\Pennant\Feature; test('it can control feature values', function () { Feature::define('purchase-button', 'seafoam-green'); expect(Feature::value('purchase-button'))->toBe('seafoam-green'); }); ``` ```php tab=PHPUnit theme={null} use Laravel\Pennant\Feature; public function test_it_can_control_feature_values(): void { Feature::define('purchase-button', 'seafoam-green'); $this->assertSame('seafoam-green', Feature::value('purchase-button')); } ``` クラスベースのフィーチャーも同様に扱えます。 ```php tab=Pest theme={null} test('it can control feature values', function () { Feature::define(NewApi::class, true); expect(Feature::value(NewApi::class))->toBeTrue(); }); ``` ```php tab=PHPUnit theme={null} use App\Features\NewApi; public function test_it_can_control_feature_values(): void { Feature::define(NewApi::class, true); $this->assertTrue(Feature::value(NewApi::class)); } ``` ### テスト用ストアの設定 テスト中に使用するストアを `phpunit.xml` の環境変数で指定できます。 ```xml theme={null} ``` *** ## カスタムドライバー 既存のドライバーが要件に合わない場合、カスタムドライバーを作成できます。`Laravel\Pennant\Contracts\Driver` インターフェースを実装します。 ```php theme={null} make('redis'), $app->make('events'), []); }); ``` 登録後は `config/pennant.php` でドライバーを指定できます。 ```php theme={null} 'stores' => [ 'redis' => [ 'driver' => 'redis', 'connection' => null, ], ], ``` *** ## まとめ | やりたいこと | 方法 | | - | - | | フィーチャーをインストール | `composer require laravel/pennant` | | フィーチャーを定義 | `Feature::define('name', fn ($user) => ...)` | | フィーチャーを確認 | `Feature::active('name')` | | Bladeで確認 | `@feature('name') ... @endfeature` | | 値を更新 | `Feature::activate('name')` / `deactivate` | | 全スコープに適用 | `Feature::activateForEveryone('name')` | | テストで制御 | `Feature::define('name', true)` で再定義 | | ストレージからパージ | `Feature::purge('name')` | ## 次のステップ アプリケーションの例外処理とレポートの仕組みを学びます。 アプリケーションのパフォーマンス監視ダッシュボードを導入します。 # Laravel Pint Source: https://kawax.biz/jp/pint PHP CS Fixer上に構築されたコードスタイル自動修正ツール。チームで一貫したコーディングスタイルを維持する方法を解説します。 ## Laravel Pintとは [Laravel Pint](https://github.com/laravel/pint) は、PHP CS Fixer 上に構築されたコードスタイル自動修正ツールです。設定なしですぐに使えるよう設計されており、Laravelのコーディングスタイルに従ってコードを自動的に整形します。 チーム開発でよくある「コードスタイルのレビューコメント」を自動化することで、本質的なコードレビューに集中できるようになります。 ```mermaid theme={null} flowchart LR A["PHPファイル"] --> B["pint 実行"] B --> C{スタイル
    エラーあり?} C -- あり --> D["自動修正"] C -- なし --> E["変更なし"] D --> F["整形済みファイル"] E --> F ``` ## インストール 新しく作成したLaravelアプリケーションにはPintが自動でインストールされています。古いバージョンのプロジェクトには Composer でインストールしてください。 ```shell theme={null} composer require laravel/pint --dev ``` ## 実行方法 ### 基本的な使い方 プロジェクト内のすべての `.php` ファイルを自動修正します。 ```shell theme={null} ./vendor/bin/pint ``` 特定のファイルやディレクトリだけを対象にすることもできます。 ```shell theme={null} ./vendor/bin/pint app/Models ./vendor/bin/pint app/Models/User.php ``` デフォルトでは、PintはBladeテンプレートを整形しません。`--blade`を指定すると、`pint.json`を変更せず、その実行時だけ `.blade.php` ファイルも整形できます。 ```shell theme={null} ./vendor/bin/pint --blade ``` このオプションは `Pint/laravel_blade` ルールを有効にします。Pintは `prettier-plugin-blade` と `prettier-plugin-tailwindcss` を組み合わせた Prettier を使用するため、Node.js が必要です。初回実行時に不足している Prettier の依存関係がある場合は、インストールを促します。 ### オプション一覧 | オプション | 説明 | | - | - | | `--blade` | 実行時だけBladeテンプレートを整形する | | `--test` | ファイルを変更せずスタイルエラーを検出するだけ。エラーがあれば非ゼロの終了コードを返す | | `--dirty` | Gitで未コミットの変更があるファイルだけを対象にする | | `--diff=[branch]` | 指定ブランチとの差分があるファイルだけを対象にする | | `--repair` | スタイルエラーを修正しつつ、修正があれば非ゼロの終了コードを返す | | `--parallel` | 並列実行モード(実験的)でパフォーマンスを向上させる | | `--max-processes=4` | `--parallel` と組み合わせて最大プロセス数を指定する | | `-v` | 変更内容の詳細を表示する | | `--config` | 使用する `pint.json` のパスを指定する | | `--preset` | 使用するプリセットを指定する | ```shell theme={null} # ファイルを変更せずチェックのみ ./vendor/bin/pint --test # 未コミットのファイルだけ対象 ./vendor/bin/pint --dirty # 並列実行 ./vendor/bin/pint --parallel ``` ## 設定 プロジェクトルートに `pint.json` を作成することで動作をカスタマイズできます。 ```json theme={null} { "preset": "laravel" } ``` 設定ファイルのパスを明示的に指定することもできます。 ```shell theme={null} ./vendor/bin/pint --config vendor/my-company/coding-style/pint.json ``` ### プリセット プリセットはルールのセットです。デフォルトは `laravel` プリセットで、Laravelプロジェクトに最適なルールが適用されます。 | プリセット | 説明 | | - | - | | `laravel` | Laravelの推奨コーディングスタイル(デフォルト) | | `psr12` | PSR-12 コーディング標準 | | `per` | PER Coding Style | | `symfony` | Symfony のコーディングスタイル | | `empty` | ルールなし。個別にルールを定義して使う | ```shell theme={null} # コマンドラインでプリセットを指定 ./vendor/bin/pint --preset psr12 ``` ### ルールのカスタマイズ `pint.json` でルールを個別に有効・無効にできます。利用できるルールは [PHP CS Fixer Configurator](https://mlocati.github.io/php-cs-fixer-configurator) を参照してください。 ```json theme={null} { "preset": "laravel", "rules": { "simplified_null_return": true, "array_indentation": false, "new_with_parentheses": { "anonymous_class": true, "named_class": true } } } ``` ### ファイル・フォルダの除外 特定のフォルダをチェック対象から除外できます。 ```json theme={null} { "exclude": [ "my-specific/folder" ] } ``` ファイル名のパターンで除外する場合は `notName` を使います。 ```json theme={null} { "notName": [ "*-my-file.php" ] } ``` 特定のファイルパスで除外する場合は `notPath` を使います。 ```json theme={null} { "notPath": [ "path/to/excluded-file.php" ] } ``` ## 推奨設定 実際の開発現場で効果的な `pint.json` の設定例を紹介します。 ```json theme={null} { "preset": "laravel", "rules": { "no_unused_imports": false, "strict_comparison": true, "declare_strict_types": true } } ``` 各ルールを追加する理由は次のとおりです。 | ルール | 効果 | | - | - | | `no_unused_imports` | `false` にすると未使用の `use` 文を削除せず残す。Pintのデフォルトは `true`(削除)なので通常のLaravelプロジェクトではこの項目は不要 | | `strict_comparison` | `==` を `===` に、`!=` を `!==` に変換して予期しない型変換のバグを防ぐ | | `declare_strict_types` | ファイル先頭に `declare(strict_types=1);` を自動追加して型の安全性を高める | `strict_comparison` と `declare_strict_types` は型に厳格なコードを強制するため、既存プロジェクトに追加する際は初回の修正量が多くなることがあります。新規プロジェクトでは最初から導入することをお勧めします。 **`no_unused_imports` はパッケージ開発向けの設定です。** Pintのデフォルトは `true`(未使用の `use` 文を削除)なので、通常のLaravelプロジェクトではこの項目自体不要です。パッケージ開発ではトレイトやインターフェースのインポートで機能をオン/オフするパターンがあるため、`false` を追加しておき各パッケージの状況に合わせて `true` に変更する運用が便利です。 ## composer.json への scripts 設定 `composer.json` に `pint` 用のスクリプトを登録すると、`composer pint` だけで実行できるようになります。 ```json theme={null} { "scripts": { "pint": "./vendor/bin/pint", "pint:test": "./vendor/bin/pint --test" } } ``` 登録後は次のように実行できます。 ```shell theme={null} # コードを自動修正 composer pint # チェックのみ(修正しない) composer pint:test ``` CI環境では `composer pint:test` を使うとファイルを変更せずスタイル違反を検出できます。`--test` オプションはエラーがあれば非ゼロの終了コードを返すので、CI のパスチェックに使えます。 ## GitHub Actions での自動実行 GitHub Actions を使ってプッシュのたびにコードスタイルを自動修正・コミットできます。 GitHubリポジトリの **Settings > Actions > General > Workflow permissions** で「Read and write permissions」を有効にします。 `.github/workflows/lint.yml` を作成します。 ```yaml theme={null} name: Fix Code Style on: [push] jobs: lint: runs-on: ubuntu-latest strategy: fail-fast: true matrix: php: [8.4] steps: - name: Checkout code uses: actions/checkout@v5 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} tools: pint - name: Run Pint run: pint - name: Commit linted files uses: stefanzweifel/git-auto-commit-action@v6 ``` このワークフローは、プッシュのたびに Pint を実行し、スタイル違反を修正したファイルを自動的にコミットします。 Pull Request のレビュー前に自動修正が入るため、コードスタイルに関するレビューコメントが減ります。チームで導入する際は最初に全ファイルをローカルで修正してからワークフローを追加すると、余分なコミットを防げます。 # プロセス Source: https://kawax.biz/jp/processes LaravelのProcessファサードを使って外部シェルコマンドを実行し、同期・非同期・並行処理を簡潔に扱う方法を解説します。 ## はじめに LaravelのProcessファサードは、[Symfony Processコンポーネント](https://symfony.com/doc/current/components/process.html)の薄いラッパーです。 シェルコマンドをLaravelアプリケーションから呼び出し、その結果を扱うための簡潔なAPIを提供します。 ```mermaid theme={null} flowchart LR A["Laravelアプリ"] --> B["Process
    ファサード"] B --> C["シェルコマンド"] C --> D["ProcessResult
    (output / exitCode)"] D --> A ``` ## プロセスの起動 `Process::run()` を使うと、コマンドを同期的に実行して結果を取得できます。 ```php theme={null} use Illuminate\Support\Facades\Process; $result = Process::run('ls -la'); return $result->output(); ``` `ProcessResult` インスタンスには、結果を確認するための各種メソッドが用意されています。 ```php theme={null} $result = Process::run('ls -la'); $result->command(); // 実行したコマンド $result->successful(); // 成功したか(終了コード 0) $result->failed(); // 失敗したか(終了コード 0 以外) $result->output(); // 標準出力(stdout) $result->errorOutput(); // 標準エラー出力(stderr) $result->exitCode(); // 終了コード ``` ### 失敗時に例外をスローする 終了コードが 0 より大きい場合に `Illuminate\Process\Exceptions\ProcessFailedException` をスローさせるには、`throw()` または `throwIf()` を使います。 ```php theme={null} $result = Process::run('ls -la')->throw(); $result = Process::run('ls -la')->throwIf($condition); ``` ## プロセスオプション ### 作業ディレクトリ `path()` で作業ディレクトリを指定できます。省略すると現在のPHPスクリプトの作業ディレクトリが使われます。 ```php theme={null} $result = Process::path(__DIR__)->run('ls -la'); ``` ### 標準入力 `input()` でプロセスの標準入力にデータを渡せます。 ```php theme={null} $result = Process::input('Hello World')->run('cat'); ``` ### タイムアウト デフォルトは60秒です。`timeout()` で変更できます。タイムアウトするとプロセスが中断され、`ProcessTimedOutException` がスローされます。 ```php theme={null} $result = Process::timeout(120)->run('bash import.sh'); ``` `CarbonInterval` ヘルパー関数も使えます。 ```php theme={null} use function Illuminate\Support\minutes; $result = Process::timeout(minutes(2))->run('bash import.sh'); ``` タイムアウトを無効にするには `forever()` を使います。 ```php theme={null} $result = Process::forever()->run('bash import.sh'); ``` アイドルタイムアウト(出力がない状態が続く秒数)を設定することもできます。 ```php theme={null} $result = Process::timeout(60)->idleTimeout(30)->run('bash import.sh'); ``` ### 環境変数 `env()` で環境変数を追加または上書きできます。システムの環境変数は自動的に継承されます。 ```php theme={null} $result = Process::forever() ->env(['IMPORT_PATH' => __DIR__]) ->run('bash import.sh'); ``` 継承した変数を除外するには `false` を指定します。 ```php theme={null} $result = Process::forever() ->env(['LOAD_PATH' => false]) ->run('bash import.sh'); ``` ### 出力の無効化 大量の出力が不要な場合は `quietly()` でメモリ消費を抑えられます。 ```php theme={null} $result = Process::quietly()->run('bash import.sh'); ``` ### リアルタイム出力 `run()` の第2引数にクロージャを渡すと、出力をリアルタイムに受け取れます。 ```php theme={null} $result = Process::run('ls -la', function (string $type, string $output) { echo $output; }); ``` ## パイプライン `Process::pipe()` を使うと、あるコマンドの出力を次のコマンドの入力として渡せます。 ```php theme={null} use Illuminate\Process\Pipe; use Illuminate\Support\Facades\Process; $result = Process::pipe(function (Pipe $pipe) { $pipe->command('cat example.txt'); $pipe->command('grep -i "laravel"'); }); if ($result->successful()) { // ... } ``` コマンド文字列の配列を渡すこともできます。 ```php theme={null} $result = Process::pipe([ 'cat example.txt', 'grep -i "laravel"', ]); ``` 各プロセスに `as()` でキーを付けると、出力クロージャでどのプロセスからの出力かを判別できます。 ```php theme={null} $result = Process::pipe(function (Pipe $pipe) { $pipe->as('first')->command('cat example.txt'); $pipe->as('second')->command('grep -i "laravel"'); }, function (string $type, string $output, string $key) { // $key が 'first' または 'second' }); ``` ## 非同期プロセス `Process::start()` を使うと、プロセスを非同期で起動できます。プロセスが実行中の間、アプリケーションは他の処理を続けられます。 ```php theme={null} $process = Process::timeout(120)->start('bash import.sh'); while ($process->running()) { // 他の処理 } $result = $process->wait(); ``` ### プロセスIDとシグナル `id()` で実行中プロセスのOSプロセスIDを取得できます。 ```php theme={null} $process = Process::start('bash import.sh'); return $process->id(); ``` `signal()` でプロセスにシグナルを送れます。 ```php theme={null} $process->signal(SIGUSR2); ``` ### 非同期プロセスの出力 実行中に `latestOutput()` と `latestErrorOutput()` を使うと、前回取得以降の差分出力を取得できます。 ```php theme={null} $process = Process::timeout(120)->start('bash import.sh'); while ($process->running()) { echo $process->latestOutput(); echo $process->latestErrorOutput(); sleep(1); } ``` 特定の出力が現れるまで待機するには `waitUntil()` を使います。 ```php theme={null} $process = Process::start('bash import.sh'); $process->waitUntil(function (string $type, string $output) { return $output === 'Ready...'; }); ``` ### 非同期プロセスのタイムアウト確認 ループ内で `ensureNotTimedOut()` を呼ぶと、タイムアウトしていた場合に例外がスローされます。 ```php theme={null} $process = Process::timeout(120)->start('bash import.sh'); while ($process->running()) { $process->ensureNotTimedOut(); sleep(1); } ``` ## 並行プロセス `Process::pool()` を使うと、複数のプロセスを並行して実行できます。 ```php theme={null} use Illuminate\Process\Pool; use Illuminate\Support\Facades\Process; $pool = Process::pool(function (Pool $pool) { $pool->path(__DIR__)->command('bash import-1.sh'); $pool->path(__DIR__)->command('bash import-2.sh'); $pool->path(__DIR__)->command('bash import-3.sh'); })->start(function (string $type, string $output, int $key) { // ... }); while ($pool->running()->isNotEmpty()) { // ... } $results = $pool->wait(); ``` `concurrently()` を使うと、プールをすぐに開始して結果を待つ処理を1行で書けます。 ```php theme={null} [$first, $second, $third] = Process::concurrently(function (Pool $pool) { $pool->path(__DIR__)->command('ls -la'); $pool->path(app_path())->command('ls -la'); $pool->path(storage_path())->command('ls -la'); }); echo $first->output(); ``` ### プロセスへの名前付け `as()` で各プロセスに文字列キーを付けると、結果の取得が明確になります。 ```php theme={null} $pool = Process::pool(function (Pool $pool) { $pool->as('first')->command('bash import-1.sh'); $pool->as('second')->command('bash import-2.sh'); $pool->as('third')->command('bash import-3.sh'); })->start(function (string $type, string $output, string $key) { // ... }); $results = $pool->wait(); return $results['first']->output(); ``` プール全体にシグナルを送ることもできます。 ```php theme={null} $pool->signal(SIGUSR2); ``` ## テスト ### プロセスのフェイク `Process::fake()` を使うと、実際のシェルコマンドを実行せずにテストできます。 ```php theme={null} use Illuminate\Support\Facades\Process; Process::fake(); $response = $this->get('/import'); Process::assertRan('bash import.sh'); ``` 出力や終了コードを指定することもできます。 ```php theme={null} Process::fake([ '*' => Process::result( output: 'Test output', errorOutput: 'Test error output', exitCode: 1, ), ]); ``` ### 特定のコマンドをフェイクする ワイルドカードやコマンド文字列をキーにして個別にフェイクを設定できます。 ```php theme={null} Process::fake([ 'cat *' => Process::result(output: 'file contents'), 'ls -la' => Process::result(output: 'file listing'), ]); ``` ### シーケンスのフェイク 同じコマンドを複数回呼び出す場合、返す結果の順序を指定できます。 ```php theme={null} Process::fake([ 'ls *' => [ Process::result('first time'), Process::result('second time'), ], ]); ``` ### アサーション | メソッド | 説明 | | - | - | | `Process::assertRan('command')` | コマンドが実行されたことを検証 | | `Process::assertNotRan('command')` | コマンドが実行されなかったことを検証 | | `Process::assertRan(fn)` | クロージャで実行内容を詳細に検証 | | `Process::assertRanTimes('command', 3)` | コマンドが指定回数実行されたことを検証 | ### 予期しないプロセスの防止 `Process::preventStrayProcesses()` を呼ぶと、フェイクが設定されていないコマンドが実行された場合に例外がスローされます。 ```php theme={null} Process::preventStrayProcesses(); Process::fake([ 'ls *' => Process::result('file listing'), ]); // 例外: 'bash import.sh' はフェイクが設定されていない Process::run('bash import.sh'); ``` ## 関連ページ カスタムコマンドを作成してプロセスを呼び出す 非同期処理との比較 # Laravel Prompts Source: https://kawax.biz/jp/prompts laravel/promptsパッケージを使ってArtisanコマンドに美しい対話型UIを追加する方法を解説します。テキスト入力・選択・検索・autocomplete・タスクログ・ストリーミング出力など多彩な関数を紹介します。 ## はじめに [Laravel Prompts](https://github.com/laravel/prompts) は、コマンドラインアプリケーションに美しく使いやすい対話型フォームを追加するためのPHPパッケージです。 プレースホルダーテキストやバリデーションといったブラウザのフォームに近い体験を提供します。 Artisanコマンドのコード内で直接呼び出せるため、ユーザーへの問い合わせをシンプルかつ直感的に記述できます。 Laravel PromptsはmacOS・Linux・Windows(WSL)をサポートしています。 非対応環境では自動的にフォールバック動作に切り替わります。 ## インストール Laravel PromptsはLaravel本体に同梱されているため、追加インストールは不要です。 他のPHPプロジェクトで使いたい場合はComposerでインストールします。 ```shell theme={null} composer require laravel/prompts ``` ## 基本的なプロンプト関数 ### text — テキスト入力 `text()` でユーザーに文字列入力を求めます。 ```php theme={null} use function Laravel\Prompts\text; $name = text('What is your name?'); ``` プレースホルダー・デフォルト値・ヒントを設定できます。 ```php theme={null} $name = text( label: 'What is your name?', placeholder: 'E.g. Taylor Otwell', default: $user?->name, hint: 'This will be displayed on your profile.' ); ``` `required` を指定すると入力を必須にできます。バリデーションメッセージも変更可能です。 ```php theme={null} $name = text( label: 'What is your name?', required: 'Your name is required.' ); ``` `validate` クロージャで追加のバリデーションを行えます。エラーメッセージを返すか、合格時に `null` を返します。 ```php theme={null} $name = text( label: 'What is your name?', validate: fn (string $value) => match (true) { strlen($value) < 3 => 'The name must be at least 3 characters.', strlen($value) > 255 => 'The name must not exceed 255 characters.', default => null } ); ``` Laravelのバリデーションルールを配列で指定することもできます。 ```php theme={null} $name = text( label: 'What is your name?', validate: ['name' => 'required|max:255|unique:users'] ); ``` ### textarea — 複数行テキスト入力 `textarea()` で複数行の入力を受け付けます。 ```php theme={null} use function Laravel\Prompts\textarea; $story = textarea('Tell me a story.'); ``` ### number — 数値入力 `number()` で数値を受け付けます。上下矢印キーで値を増減できます。 ```php theme={null} use function Laravel\Prompts\number; $copies = number( label: 'How many copies would you like?', default: 1, validate: ['copies' => 'required|integer|min:1|max:100'] ); ``` ### password — パスワード入力 `password()` はテキスト入力と同様ですが、入力内容が画面に表示されません。 ```php theme={null} use function Laravel\Prompts\password; $password = password('What is your password?'); ``` ```php theme={null} $password = password( label: 'What is your password?', placeholder: 'password', hint: 'Minimum 8 characters.', validate: fn (string $value) => strlen($value) < 8 ? 'The password must be at least 8 characters.' : null ); ``` ### confirm — Yes/No確認 `confirm()` でユーザーに二択の確認を求めます。`true` または `false` を返します。 ```php theme={null} use function Laravel\Prompts\confirm; $confirmed = confirm('Do you accept the terms?'); ``` デフォルト値やラベルテキストをカスタマイズできます。 ```php theme={null} $confirmed = confirm( label: 'Do you accept the terms?', default: false, yes: 'I accept', no: 'I decline', hint: 'The terms must be accepted to continue.' ); ``` ### select — 選択リスト `select()` でユーザーに一覧から1つ選ばせます。 ```php theme={null} use function Laravel\Prompts\select; $role = select( label: 'What role should the user have?', options: ['Member', 'Contributor', 'Owner'], default: 'Owner' ); ``` 連想配列を使うと、表示ラベルではなくキーを返り値にできます。 ```php theme={null} $role = select( label: 'What role should the user have?', options: [ 'member' => 'Member', 'contributor' => 'Contributor', 'owner' => 'Owner', ], default: 'owner' ); ``` `scroll` でスクロール前に表示する選択肢数を変更できます(デフォルト5件)。 ```php theme={null} $role = select( label: 'Which category would you like to assign?', options: Category::pluck('name', 'id'), scroll: 10 ); ``` ### multiselect — 複数選択 `multiselect()` で複数の選択肢を同時に選ばせます。 ```php theme={null} use function Laravel\Prompts\multiselect; $permissions = multiselect( label: 'What permissions should be assigned?', options: ['Read', 'Create', 'Update', 'Delete'] ); ``` `required` を指定すると少なくとも1つの選択を必須にできます。 ```php theme={null} $permissions = multiselect( label: 'What permissions should be assigned?', options: ['Read', 'Create', 'Update', 'Delete'], required: 'At least one permission must be selected.' ); ``` ### suggest — オートコンプリート付き入力 `suggest()` は候補を提示しながら自由入力も受け付けます。 ```php theme={null} use function Laravel\Prompts\suggest; $name = suggest( label: 'What is your name?', options: ['Taylor', 'Dayle'] ); ``` クロージャを渡すと、入力内容に応じて動的に候補を絞り込めます。 ```php theme={null} $name = suggest( label: 'What is your name?', options: fn (string $value) => collect(['Taylor', 'Tobi', 'Dries']) ->filter(fn ($name) => str_starts_with($name, $value)) ->values() ->all() ); ``` ### search — 動的検索 `search()` は入力するたびに候補リストを更新します。クロージャが返す配列が候補になります。 ```php theme={null} use function Laravel\Prompts\search; $userId = search( label: 'Search for the user that should receive the mail', options: fn (string $value) => strlen($value) > 0 ? User::whereLike('name', "%{$value}%")->pluck('name', 'id')->all() : [] ); ``` ### multisearch — 動的複数選択 `multisearch()` は動的検索で複数選択ができます。 ```php theme={null} use function Laravel\Prompts\multisearch; $userIds = multisearch( label: 'Search for the users that should receive the mail', options: fn (string $value) => strlen($value) > 0 ? User::whereLike('name', "%{$value}%")->pluck('name', 'id')->all() : [] ); ``` ### pause — 一時停止 `pause()` でユーザーにEnterキーの押下を促して処理を一時停止できます。 ```php theme={null} use function Laravel\Prompts\pause; pause('Press ENTER to continue.'); ``` ### autocomplete — インライン補完 `autocomplete()` はゴーストテキストとして候補を表示するインライン補完関数です。`suggest()` と異なり、ユーザーが入力するたびに一致する候補がゴーストテキストとして表示され、`Tab` キーまたは右矢印キーで補完を確定できます。 ```php theme={null} use function Laravel\Prompts\autocomplete; $name = autocomplete( label: 'What is your name?', options: ['Taylor', 'Dayle', 'Jess', 'Nuno', 'Tim'] ); ``` プレースホルダー・デフォルト値・ヒントを設定できます。 ```php theme={null} $name = autocomplete( label: 'What is your name?', options: ['Taylor', 'Dayle', 'Jess', 'Nuno', 'Tim'], placeholder: 'E.g. Taylor', default: $user?->name, hint: 'Use Tab to accept, up/down to cycle.' ); ``` クロージャを渡すと入力内容に応じて動的に候補を生成できます。 ```php theme={null} $file = autocomplete( label: 'Which file?', options: fn (string $value) => collect($files) ->filter(fn ($file) => str_starts_with(strtolower($file), strtolower($value))) ->values() ->all(), ); ``` ## バリデーション すべてのプロンプト関数は `validate` 引数でバリデーションを設定できます。 ```php theme={null} $name = text( label: 'What is your name?', validate: fn (string $value) => match (true) { strlen($value) < 3 => 'The name must be at least 3 characters.', strlen($value) > 255 => 'The name must not exceed 255 characters.', default => null } ); ``` クロージャが文字列を返すとエラーとして表示し、再入力を促します。`null` を返すとバリデーション成功です。 Laravelのバリデーションルールを配列形式で使うこともできます。 ```php theme={null} $name = text( label: 'What is your name?', validate: ['name' => 'required|max:255|unique:users'] ); ``` 入力をバリデーション前に変換するには `transform` 引数を使います。 ```php theme={null} $name = text( label: 'What is your name?', transform: fn (string $value) => trim($value), validate: fn (string $value) => match (true) { strlen($value) === 0 => 'The name must not be empty.', default => null } ); ``` ## フォーム `form()` を使うと複数のプロンプトをひとまとめにして、完了前にまとめてキャンセルすることができます。 ```php theme={null} use function Laravel\Prompts\form; $responses = form() ->text('What is your name?', required: true, name: 'name') ->password('What is your password?', validate: ['password' => 'min:8'], name: 'password') ->confirm('Do you accept the terms?') ->submit(); $name = $responses['name']; $password = $responses['password']; $confirmed = $responses[2]; ``` ## 情報出力 テキストメッセージをスタイル付きで出力する関数が用意されています。 ```php theme={null} use function Laravel\Prompts\info; use function Laravel\Prompts\warning; use function Laravel\Prompts\error; use function Laravel\Prompts\alert; use function Laravel\Prompts\note; note('Prepare for launch.'); info('User created successfully.'); warning('This action cannot be undone.'); error('Something went wrong.'); alert('Critical failure detected!'); ``` ## コールアウト `callout()` はラベルとコンテンツを枠で囲んで表示します。デプロイサマリー・エラー詳細・ステータス更新など、重要な情報を目立たせるのに適しています。 ```php theme={null} use function Laravel\Prompts\callout; callout( label: 'Environment Configured', content: 'Your application is running in production mode with 4 workers.', ); ``` `type` 引数に `'warning'` または `'error'` を指定すると、ビジュアルスタイルを変更できます。 ```php theme={null} callout( label: 'Deprecation Notice', content: 'The `--prefer-stable` flag will be removed in v4.0.', type: 'warning', ); callout( label: 'Database Connection Failed', content: 'Could not connect to MySQL on 127.0.0.1:3306.', type: 'error', ); ``` `info` 引数でフッター行を追加できます。IDやタイムスタンプなどのメタデータ表示に便利です。 ```php theme={null} callout( label: 'Deployment Summary', content: 'Your application was deployed to production.', info: 'deploy-id: d4f8a2c', ); ``` ### リッチコンテンツ 文字列の代わりに配列を渡すと、構造化されたリッチなコールアウトを作れます。`Element` クラスには見出し・箇条書きリスト・番号付きリスト・キーバリューリスト・リンクを作るファクトリーメソッドがあります。 ```php theme={null} use Laravel\Prompts\Elements\Element; use function Laravel\Prompts\callout; callout('Deployment Summary', [ 'Your application was deployed to production at 2024-03-15 14:32 UTC.', Element::heading('What Changed'), Element::bulletedList([ 'Migrated 3 pending database migrations', 'Cleared and rebuilt route cache', 'Restarted 4 queue workers', ]), Element::heading('Next Steps'), Element::numberedList([ 'Verify the health check endpoint at /up', 'Monitor error rates for the next 15 minutes', 'Confirm background jobs are processing', ]), ]); ``` `Element::keyValueList` でラベル付きデータを表示できます。 ```php theme={null} callout('Database Connection Failed', [ 'Could not connect to the database server.', Element::keyValueList([ 'Host' => '127.0.0.1', 'Port' => '3306', 'Database' => 'forge', 'Status' => 'Connection refused', ]), ], type: 'error'); ``` `Element::link` は [OSC 8](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda) に対応したターミナルでクリック可能なハイパーリンクを生成します。URL のみ、または URL とカスタムラベルを渡せます。 ```php theme={null} callout('Server Health Check', [ 'Multiple services are reporting degraded performance.', Element::heading('Affected Services'), 'Look here: '.Element::link('https://example.com/health', 'Health Dashboard'), Element::link('https://example.com/health'), ]); ``` ラベルを省略した場合は、URL がリンクテキストとして表示されます。 ## テーブル表示 `table()` でデータをテーブル形式で表示できます。 ```php theme={null} use function Laravel\Prompts\table; table( headers: ['Name', 'Email'], rows: User::all(['name', 'email'])->toArray() ); ``` ## スピン(ローディング表示) `spin()` はクロージャの実行中にローディングインジケーターを表示します。 ```php theme={null} use function Laravel\Prompts\spin; $response = spin( message: 'Fetching response...', callback: fn () => Http::get('http://example.com') ); ``` `spin()` を使うには `pcntl` PHP拡張が必要です。使用できない環境ではスピナーは表示されません。 ## 進捗バー `progress()` で繰り返し処理の進捗を視覚的に表示できます。 ```php theme={null} use function Laravel\Prompts\progress; $users = progress( label: 'Updating users', steps: User::all(), callback: fn ($user) => $this->performTask($user), hint: 'This may take some time.' ); ``` 手動で進捗バーを制御することもできます。 ```php theme={null} $progress = progress(label: 'Uploading files', steps: count($files)); $progress->start(); foreach ($files as $file) { $this->uploadFile($file); $progress->advance(); } $progress->finish(); ``` ## タスク `task()` はコールバック実行中にスピナーとスクロール可能なライブ出力エリアを表示します。依存関係のインストールやデプロイスクリプトなど長時間実行するプロセスをラップするのに最適で、何が起きているかリアルタイムで確認できます。 ```php theme={null} use function Laravel\Prompts\task; task( label: 'Installing dependencies', callback: function ($logger) { // 長時間実行する処理... } ); ``` コールバックは `Logger` インスタンスを受け取り、ログ行やステータスメッセージをリアルタイムで表示できます。 `task()` を使うには `pcntl` PHP拡張が必要です。使用できない環境ではスタティック表示にフォールバックします。 ### ログ行の出力 `line` メソッドでスクロール出力エリアに1行ずつログを書き込みます。 ```php theme={null} task( label: 'Installing dependencies', callback: function ($logger) { $logger->line('Resolving packages...'); $logger->line('Downloading laravel/framework'); } ); ``` ### ステータスメッセージ `success`・`warning`・`error` を使うと、スクロールログエリアの上部に固定されたハイライト付きメッセージを表示できます。 ```php theme={null} task( label: 'Deploying application', callback: function ($logger) { $logger->line('Pulling latest changes...'); $logger->success('Changes pulled!'); $logger->line('Running migrations...'); $logger->warning('No new migrations to run.'); $logger->line('Clearing cache...'); $logger->success('Cache cleared!'); } ); ``` ### ラベルの更新 `label` メソッドで実行中にタスクのラベルを更新できます。`subLabel` メソッドはラベルの下に薄く表示されるサブラベルを設定します。空文字列を渡すとサブラベルを消去できます。`subLabel` 引数で初期サブラベルを指定することもできます。 ```php theme={null} task( label: 'Deploying', callback: function ($logger) { $logger->subLabel('Building assets...'); // ... $logger->subLabel('Running migrations...'); // ... $logger->subLabel(''); }, subLabel: 'Preparing...' ); ``` ### テキストのストリーミング AIが生成するレスポンスのように段階的に出力が生成される処理では、`partial` メソッドでテキストをひとつひとつストリーミングできます。ストリームが完了したら `commitPartial` を呼んで確定します。 ```php theme={null} task( label: 'Generating response...', callback: function ($logger) { foreach ($words as $word) { $logger->partial($word . ' '); } $logger->commitPartial(); } ); ``` ### 出力上限とサマリーの保持 デフォルトでは最大10行のスクロール出力を表示します。`limit` 引数でカスタマイズできます。タスク完了後もステータスメッセージを画面に残したい場合は `keepSummary: true` を渡します。 ```php theme={null} task( label: 'Deploying', callback: function ($logger) { $logger->success('Assets built'); $logger->success('Migrations complete'); }, limit: 20, keepSummary: true, ); ``` ## ストリーム `stream()` はテキストを段階的にターミナルに表示します。AIが生成するコンテンツやチャンクで届くデータの表示に最適です。 ```php theme={null} use function Laravel\Prompts\stream; $stream = stream(); foreach ($words as $word) { $stream->append($word . ' '); usleep(25_000); // チャンク間の遅延をシミュレート... } $stream->close(); ``` `append` メソッドはフェードインエフェクトでテキストをストリームに追加します。すべてのコンテンツをストリームし終えたら `close` を呼んで出力を確定しカーソルを復元します。 ## ターミナル操作 ### ターミナルタイトルの設定 ```php theme={null} use function Laravel\Prompts\title; title('My Application'); ``` 空文字列を渡すとデフォルトのタイトルにリセットできます。 ```php theme={null} title(''); ``` ### ターミナルのクリア ```php theme={null} use function Laravel\Prompts\clear; clear(); ``` ## ターミナルの考慮事項 **ターミナル幅**: ラベル・選択肢・バリデーションメッセージがターミナルの列数を超える場合、自動的に切り詰められます。80列のターミナルを想定する場合、最大74文字を目安にしてください。 **ターミナルの高さ**: `scroll` 引数を受け付けるプロンプトでは、バリデーションメッセージ用のスペースを含めてターミナルの高さに収まるよう自動的に値が調整されます。 ## フォールバック 非対応の環境(Windows non-WSLなど)では自動的にフォールバックします。 デフォルトではLaravelの `$this->ask()` や `$this->choice()` などの組み込みメソッドが代わりに使われます。 ## テスト Laravel PromptsはPestやPHPUnitのテストと連携できます。 ```php theme={null} use Laravel\Prompts\Prompt; Prompt::fake(['Taylor', true]); $name = text('What is your name?'); $confirmed = confirm('Do you accept the terms?'); Prompt::assertOutputContains('What is your name?'); ``` LaravelのArtisanテストヘルパーを使うと、情報出力関数に対してもアサーションを記述できます。 ```php theme={null} // Pest test('report generation', function () { $this->artisan('report:generate') ->expectsPromptsInfo('Welcome to the application!') ->expectsPromptsWarning('This action cannot be undone') ->expectsPromptsError('Something went wrong') ->expectsPromptsAlert('Important notice!') ->expectsPromptsTable( headers: ['Name', 'Email'], rows: [ ['Taylor Otwell', 'taylor@example.com'], ] ) ->assertExitCode(0); }); ``` ```php theme={null} // PHPUnit public function test_report_generation(): void { $this->artisan('report:generate') ->expectsPromptsInfo('Welcome to the application!') ->expectsPromptsWarning('This action cannot be undone') ->expectsPromptsTable( headers: ['Name', 'Email'], rows: [['Taylor Otwell', 'taylor@example.com']] ) ->assertExitCode(0); } ``` ## 関連ページ Artisanコマンド内でPromptsを活用する # Laravel Pulse Source: https://kawax.biz/jp/pulse Laravel Pulseを使ったアプリケーション監視ダッシュボードの導入と設定方法を解説します。レコーダー、サンプリング、カスタムカードの作成まで幅広くカバーします。 ## Laravel Pulseとは [Laravel Pulse](https://github.com/laravel/pulse) は、アプリケーションのパフォーマンスと使用状況をひと目で把握できる監視ダッシュボードです。 スロージョブや遅いエンドポイントのボトルネックを特定したり、最もアクティブなユーザーを調べたりすることができます。 ### TelescopeとPulseの違い | 特徴 | Laravel Pulse | Laravel Telescope | | - | - | - | | 目的 | アプリ全体の傾向・集計データを監視 | 個別リクエスト・イベントの詳細なデバッグ | | データ | 集計済みメトリクス | 個別イベントのログ | | 向いている場面 | 本番環境のパフォーマンス監視 | 開発・ステージング環境でのデバッグ | 個別イベントの詳細なデバッグには [Laravel Telescope](/jp/blog/telescope-introduction) を使用してください。 ### データフロー図 ```mermaid theme={null} flowchart LR A["Laravelアプリ"] -->|イベント検知| B["レコーダー
    (Recorders)"] B -->|エントリ記録| C["データベース
    (Pulseテーブル)"] C -->|集計・表示| D["ダッシュボード
    /pulse"] E["pulse:check
    デーモン"] -->|サーバー情報| C ``` *** ## インストール Pulseのファーストパーティストレージ実装は、MySQL、MariaDB、またはPostgreSQLデータベースが必要です。別のデータベースエンジンを使用している場合は、Pulseデータ専用にMySQL、MariaDB、またはPostgreSQLデータベースを用意してください。 Composerを使ってPulseをインストールします。 ```shell theme={null} composer require laravel/pulse ``` `vendor:publish` Artisanコマンドでファイルを公開します。 ```shell theme={null} php artisan vendor:publish --provider="Laravel\Pulse\PulseServiceProvider" ``` Pulseのデータを保存するテーブルを作成します。 ```shell theme={null} php artisan migrate ``` マイグレーション完了後、`/pulse` ルートでダッシュボードにアクセスできます。 ### 設定ファイルの公開 設定ファイルを別途公開してカスタマイズすることもできます。 ```shell theme={null} php artisan vendor:publish --tag=pulse-config ``` *** ## ダッシュボードのアクセス ### 認証設定 デフォルトでは `local` 環境のみダッシュボードにアクセスできます。 本番環境では `viewPulse` 認証ゲートをカスタマイズしてアクセス制御を設定してください。 ```php theme={null} use App\Models\User; use Illuminate\Support\Facades\Gate; /** * Bootstrap any application services. */ public function boot(): void { Gate::define('viewPulse', function (User $user) { return $user->isAdmin(); }); } ``` `app/Providers/AppServiceProvider.php` の `boot` メソッドに上記のコードを追加します。 ### ダッシュボードのカスタマイズ ダッシュボードのビューを公開してカードやレイアウトをカスタマイズできます。 ```shell theme={null} php artisan vendor:publish --tag=pulse-dashboard ``` 公開後、`resources/views/vendor/pulse/dashboard.blade.php` を編集します。 ダッシュボードは [Livewire](https://livewire.laravel.com/) で動いているため、JavaScriptのビルドなしでカスタマイズできます。 ```blade theme={null} {{-- 全幅表示にする --}} ... {{-- カラム数を変更する --}} ... ``` 各カードは `cols` と `rows` プロパティでサイズと位置を調整できます。 ```blade theme={null} ``` *** ## レコーダー レコーダーはアプリケーションのイベントをキャプチャしてPulseデータベースに記録します。 設定は `config/pulse.php` の `recorders` セクションで管理します。 ### Requests / Slow Requests `Requests` レコーダーはアプリケーションへのリクエスト情報をキャプチャします。 スロールートの閾値(デフォルト: 1000ms)、サンプリングレート、無視するパスを設定できます。 ```php theme={null} Recorders\SlowRequests::class => [ // ... 'threshold' => [ '#^/admin/#' => 5000, 'default' => env('PULSE_SLOW_REQUESTS_THRESHOLD', 1000), ], ], ``` ### Slow Jobs `SlowJobs` レコーダーは閾値を超えた遅いジョブをキャプチャします(デフォルト: 1000ms)。 ジョブごとに異なる閾値を設定できます。 ```php theme={null} Recorders\SlowJobs::class => [ // ... 'threshold' => [ '#^App\\Jobs\\GenerateYearlyReports$#' => 5000, 'default' => env('PULSE_SLOW_JOBS_THRESHOLD', 1000), ], ], ``` ### Exceptions `Exceptions` レコーダーはアプリケーションで発生したレポート可能な例外をキャプチャします。 例外クラスと発生場所でグループ化されます。 ### Cache `CacheInteractions` レコーダーはキャッシュのヒットとミスをキャプチャします。 類似したキーをグループ化する正規表現も設定できます。 ```php theme={null} Recorders\CacheInteractions::class => [ // ... 'groups' => [ '/:\d+/' => ':*', ], ], ``` ### Queues `Queues` レコーダーはキュー内のジョブのスループット(キュー済み、処理中、処理完了、リリース、失敗)をキャプチャします。 ### Servers `Servers` レコーダーはアプリケーションを動かすサーバーのCPU、メモリ、ストレージ使用量をキャプチャします。 このレコーダーを使うには、監視対象の各サーバーで `pulse:check` コマンドを常時起動する必要があります。 ```shell theme={null} php artisan pulse:check ``` サーバー名はデフォルトでPHPの `gethostname()` の値が使われます。カスタマイズするには環境変数を設定します。 ```ini theme={null} PULSE_SERVER_NAME=load-balancer ``` ### Users `UserRequests` と `UserJobs` レコーダーは、リクエストやジョブを送信したユーザー情報をキャプチャして「Application Usage」カードに表示します。 ### Redis Ingest 高トラフィック環境では、エントリをRedisストリームに送信してからデータベースに取り込む方法が利用できます。 ```ini theme={null} PULSE_INGEST_DRIVER=redis ``` Redisインジェストを使う場合は、`pulse:work` コマンドでストリームを監視する必要があります。 ```shell theme={null} php artisan pulse:work ``` *** ## サンプリング 高トラフィック環境では、全イベントをキャプチャするとデータベースに数百万行が蓄積される可能性があります。 **サンプリング**を有効にすると、一部のイベントのみ記録し、ダッシュボードで近似値として表示します。 ```php theme={null} // config/pulse.php Recorders\UserRequests::class => [ 'sample_rate' => 0.1, // 10%のリクエストのみ記録 ], ``` ダッシュボードでは近似値の前に `~` が表示されます。 メトリクスのエントリ数が多いほど、精度を維持しながらサンプリングレートを下げられます。 *** ## 環境変数 主要な設定は環境変数で制御できます。 | 環境変数 | 説明 | デフォルト | | - | - | - | | `PULSE_ENABLED` | Pulseの有効/無効 | `true` | | `PULSE_DB_CONNECTION` | Pulseが使うDBコネクション | アプリのデフォルト | | `PULSE_INGEST_DRIVER` | インジェストドライバー (`redis` など) | `storage` | | `PULSE_SERVER_NAME` | サーバー識別名 | `gethostname()` の値 | | `PULSE_SLOW_REQUESTS_THRESHOLD` | スローリクエストの閾値(ms) | `1000` | | `PULSE_SLOW_JOBS_THRESHOLD` | スロージョブの閾値(ms) | `1000` | | `PULSE_SLOW_QUERIES_THRESHOLD` | スロークエリの閾値(ms) | `1000` | *** ## カスタムカード 独自のPulseカードを作成して、アプリケーション固有のデータを表示できます。 カードは [Livewire](https://livewire.laravel.com/) コンポーネントとして実装します。 ```php theme={null} namespace App\Livewire\Pulse; use Laravel\Pulse\Livewire\Card; use Livewire\Attributes\Lazy; #[Lazy] class TopSellers extends Card { public function render() { return view('livewire.pulse.top-sellers'); } } ``` カードのビューでは、Pulseが提供するBladeコンポーネントを使って統一感のある見た目を実現できます。 ```blade theme={null} ... ... ``` カスタムデータのキャプチャには `Pulse::record` メソッドを使います。 ```php theme={null} use Laravel\Pulse\Facades\Pulse; Pulse::record('user_sale', $user->id, $sale->amount) ->sum() ->count(); ``` 作成したコンポーネントはダッシュボードビューに組み込みます。 ```blade theme={null} ... ``` *** ## まとめ | やりたいこと | 方法 | | - | - | | Pulseをインストール | `composer require laravel/pulse` | | ダッシュボードにアクセス | `/pulse` ルート | | 本番環境でアクセス制御 | `viewPulse` ゲートを定義 | | サーバー監視を有効化 | `php artisan pulse:check` を常時起動 | | サンプリングを設定 | `sample_rate` オプションで比率を指定 | ## 次のステップ アプリケーションの例外処理とレポートの仕組みを学びます。 Laravelのログシステムの設定と活用方法を解説します。 # キューとジョブ Source: https://kawax.biz/jp/queues Laravelのキュー(Queue)とジョブ(Job)を使って、メール送信や画像処理などの重い処理を非同期で実行する方法を解説します。 ## キューとは Webアプリケーションでは、メール送信・画像のリサイズ・外部APIへの問い合わせなど、完了まで数秒かかる処理が発生することがあります。 これをHTTPリクエストの中で同期的に行うと、ユーザーはレスポンスが返るまで待ち続けなければなりません。 Laravelのキューを使うと、こうした重い処理を**バックグラウンドで非同期に実行**できます。 リクエストはすぐにレスポンスを返し、実際の処理はワーカープロセスが別途こなしてくれます。 キューはデータベース・Redis・Amazon SQSなど複数のバックエンドに対応しています。 開発環境では `sync` ドライバを使うと、キューを使わずジョブを即時実行できます。 ```mermaid theme={null} flowchart LR A["ジョブ作成
    dispatch()"] --> B["キュードライバー
    (Redis/DBなど)"] B --> C["ワーカー
    queue:work"] C --> D{"実行成功?"} D -->|"Yes"| E["完了"] D -->|"No"| F{"リトライ可能?"} F -->|"Yes"| B F -->|"No"| G["失敗記録
    failed_jobs"] ``` ## キューの設定 ### config/queue.php キューの設定は `config/queue.php` に集約されています。 `QUEUE_CONNECTION` 環境変数で使用するドライバを切り替えます。 ```php theme={null} // config/queue.php 'default' => env('QUEUE_CONNECTION', 'database'), ``` ### .env の設定 ```ini theme={null} # ドライバの選択 QUEUE_CONNECTION=database # Redisを使う場合 # QUEUE_CONNECTION=redis # REDIS_HOST=127.0.0.1 # REDIS_PORT=6379 ``` ### データベースドライバの準備 `database` ドライバを使う場合、ジョブを保存するテーブルが必要です。 Laravel 11以降の新規プロジェクトには最初からマイグレーションが含まれていますが、 含まれていない場合は次のコマンドで作成します。 ```shell theme={null} php artisan make:queue-table php artisan migrate ``` ### Redisドライバの準備 `redis` ドライバを使う場合は `config/database.php` にRedis接続設定を追加し、 Composerでドライバをインストールします。 ```shell theme={null} composer require predis/predis ``` ### SQS Overflow Storage Amazon SQS はメッセージペイロードの最大サイズに制限があります。 ペイロードが大きくなるジョブを扱う場合は、超過分をキャッシュストアに保存し、SQSにはポインターのみを送る設定を追加します。 ```php theme={null} 'sqs' => [ // ... 'overflow' => [ 'enabled' => env('SQS_OVERFLOW_ENABLED', false), 'store' => env('SQS_OVERFLOW_STORE'), 'always' => false, 'delete_after_processing' => true, 'flush_on_clear' => env('SQS_OVERFLOW_FLUSH_ON_CLEAR', false), ], ], ``` * `enabled` を有効にすると、**1MB以上**のペイロードを指定したキャッシュストアへ保存します。 * `always` を `true` にすると、サイズに関係なくすべてのSQSペイロードをキャッシュストアに保存します。 * `delete_after_processing` はジョブ成功後に保存済みペイロードを削除します(デフォルト `true`)。 * `flush_on_clear` を `true` にすると `queue:clear` 実行時にoverflow用ストアを `flush` します。通常のキャッシュを消さないため、専用ストアと組み合わせて使ってください。 ## ジョブクラスの作成 ### make:job コマンド `make:job` Artisanコマンドでジョブクラスのひな型を生成します。 ```shell theme={null} php artisan make:job SendWelcomeEmail ``` `app/Jobs/SendWelcomeEmail.php` が生成されます。 ### ジョブクラスの構造 ```php theme={null} user->email)->send(new WelcomeMail($this->user)); } } ``` `ShouldQueue` インターフェースを実装することで、このジョブがキューで非同期処理されることをLaravelに伝えます。 `Queueable` トレイトがジョブのキュー操作に必要なメソッドを提供します。 コンストラクタにEloquentモデルを渡すと、Laravelは自動的にIDだけをシリアライズします。 実行時にデータベースから最新データを再取得するため、キューのペイロードが軽くなります。 ## ジョブのディスパッチ ### dispatch() コントローラやサービスからジョブをキューに送り出すには `dispatch()` を使います。 ```php theme={null} use App\Jobs\SendWelcomeEmail; // ルートやコントローラの中で public function register(Request $request): RedirectResponse { $user = User::create($request->validated()); // ジョブをキューに積む SendWelcomeEmail::dispatch($user); return redirect('/dashboard'); } ``` ### 遅延ディスパッチ `delay()` メソッドでジョブの実行を指定時間後に遅らせられます。 ```php theme={null} // 5分後に実行 SendWelcomeEmail::dispatch($user)->delay(now()->addMinutes(5)); ``` ### dispatchAfterResponse() `dispatchAfterResponse()` を使うと、HTTPレスポンスをユーザーに返した**直後**にジョブを実行します。 `sync` ドライバでも動作するため、専用のワーカーが不要な軽量な用途に向いています。 ```php theme={null} SendWelcomeEmail::dispatchAfterResponse($user); ``` ### 特定のキューへのディスパッチ ```php theme={null} SendWelcomeEmail::dispatch($user)->onQueue('emails'); ``` ### Queue Routing 特定のジョブクラスをデフォルトで決まった接続・キューに振り向けるには、ServiceProviderの `boot()` で `Queue::route()` を使います。ジョブクラスごとに `onQueue()` / `onConnection()` を書く代わりに一元管理できます。 ```php theme={null} use App\Concerns\RequiresVideo; use App\Jobs\ProcessPodcast; use App\Jobs\ProcessVideo; use Illuminate\Support\Facades\Queue; public function boot(): void { Queue::route(ProcessPodcast::class, connection: 'redis', queue: 'podcasts'); Queue::route(RequiresVideo::class, queue: 'video'); } ``` インターフェース・トレイト・親クラスも指定できます。それらを実装・使用・継承するすべてのジョブに自動で適用されます。 複数ジョブをまとめてルーティングする場合は配列を渡します。 ```php theme={null} Queue::route([ ProcessPodcast::class => ['redis', 'podcasts'], // connection と queue ProcessVideo::class => 'videos', // queue のみ(デフォルト接続を使用) ]); ``` Queue Routing はジョブ側の `onQueue()` / `onConnection()` で上書きできます。 ### キューの転送(`Queue::forward()`) `Queue::forward()` を使うと、あるキューから別のキュー・接続へジョブを転送できます。個々のジョブや呼び出し元のコードを変更せずに、キュー基盤(インフラ)を切り替えたいときに便利です。 ```php theme={null} use Illuminate\Support\Facades\Queue; Queue::forward('reports', 'reports.fifo', 'sqs'); Queue::forward('payments', connection: 'sqs'); Queue::forward('updates', 'notifications'); ``` 複数のキューをまとめて転送するには配列を渡します。 ```php theme={null} Queue::forward([ 'reports' => 'reports.fifo', 'emails' => 'emails.fifo', ], connection: 'sqs'); ``` ジョブ側で明示的に接続を指定している場合は、転送設定よりもジョブの指定が優先されます。 ### 同期実行(テスト・開発用) `dispatchSync()` を使うと、キューを経由せずに即時実行します。 ```php theme={null} SendWelcomeEmail::dispatchSync($user); ``` ### バルクディスパッチ 多数の独立したジョブを一度にディスパッチする場合、`Bus` ファサードの `bulk()` メソッドを使用できます。バッチ処理のような追跡やコールバックが不要なケースに最適です。 `Bus::bulk()` は、ジョブを設定されたキュー接続とキュー名でグループ化し、各グループをまとめてキューにプッシュするため効率的です。 ```php theme={null} use App\Jobs\ProcessUser; use Illuminate\Support\Facades\Bus; Bus::bulk( $users->map(fn ($user) => new ProcessUser($user)) ); ``` `Bus::bulk()` はジョブを**バッチとしてまとめて**キューに送信します。バッチ処理(`Bus::batch()`)とは異なり、進捗追跡や完了コールバックは提供されません。独立した大量のジョブをシンプルに一括送信したい場合に適しています。 ### ジョブチェーン ジョブをチェーンすると、複数のジョブを順番に実行できます。チェーン内のジョブが失敗すると、後続のジョブは実行されません。 ```php theme={null} use App\Jobs\OptimizePodcast; use App\Jobs\ProcessPodcast; use App\Jobs\ReleasePodcast; use Illuminate\Support\Facades\Bus; Bus::chain([ new ProcessPodcast($podcast), new OptimizePodcast($podcast), new ReleasePodcast($podcast), ])->dispatch(); ``` チェーン全体が完了したときや、チェーン内のジョブが失敗したときに実行するコールバックも追加できます。 ```php theme={null} Bus::chain([ new ProcessPodcast($podcast), new ReleasePodcast($podcast), ])->catch(function (Throwable $e) { // チェーン内のジョブが失敗したときの処理 })->dispatch(); ``` ### ジョブバッチ バッチを使うと、複数のジョブをまとめてディスパッチし、全体の進捗を追跡できます。まず `job_batches` テーブル用のマイグレーションを作成します。 ```shell theme={null} php artisan make:batches-table php artisan migrate ``` ジョブクラスで `Batchable` トレイトを使用します。 ```php theme={null} batch()->cancelled()) { return; } // 連絡先のチャンクをインポートする... } } ``` `Bus::batch()` でバッチをディスパッチし、完了・失敗・終了時のコールバックを登録できます。 ```php theme={null} use App\Jobs\ImportContacts; use Illuminate\Bus\Batch; use Illuminate\Support\Facades\Bus; use Throwable; $batch = Bus::batch([ new ImportContacts($chunkA), new ImportContacts($chunkB), new ImportContacts($chunkC), ])->then(function (Batch $batch) { // すべてのジョブが正常に完了したとき })->catch(function (Batch $batch, Throwable $e) { // ジョブが失敗したとき })->finally(function (Batch $batch) { // バッチの実行が完了したとき })->dispatch(); ``` バッチIDを使ってバッチの状態を確認できます。 ```php theme={null} $batch = Bus::findBatch($batchId); $batch->totalJobs; // ジョブの総数 $batch->pendingJobs; // 待機中のジョブ数 $batch->failedJobs; // 失敗したジョブ数 $batch->progress(); // 進捗率(0〜100) ``` ## ジョブの処理 ### queue:work コマンド キューワーカーを起動してジョブを処理します。 ```shell theme={null} php artisan queue:work ``` 特定のドライバやキューを指定することもできます。 ```shell theme={null} # Redisのemailsキューだけを処理 php artisan queue:work redis --queue=emails # databaseドライバを使う php artisan queue:work database ``` `queue:work` は起動したまま動き続けます。コードを変更したときは `queue:restart` でワーカーを再起動してください。 本番環境ではSupervisorなどのプロセスマネージャーで管理するのが一般的です。 ## キューワーカーの監視オプション よく使うオプションを組み合わせてワーカーを細かく制御できます。 ```shell theme={null} php artisan queue:work --tries=3 --timeout=60 --sleep=3 ``` | オプション | 説明 | | - | - | | `--tries=N` | ジョブの最大試行回数。超えると失敗ジョブとして記録される | | `--timeout=N` | ジョブ1件の最大実行秒数。超えるとワーカーを強制終了する | | `--sleep=N` | キューが空のときに次のポーリングまで待機する秒数(デフォルト: 3) | | `--max-jobs=N` | N件処理したらワーカーを終了する | | `--max-time=N` | N秒経過したらワーカーを終了する | | `--queue=A,B` | 優先順位付きでキューを処理する(Aを優先) | ### ジョブクラスにリトライ設定を書く コマンドラインオプションより、ジョブクラス自体に設定を書く方が管理しやすい場合があります。 ```php theme={null} use Illuminate\Foundation\Queue\Queueable; use Illuminate\Queue\Attributes\Tries; use Illuminate\Queue\Attributes\Timeout; #[Tries(3)] #[Timeout(60)] class SendWelcomeEmail implements ShouldQueue { use Queueable; // ... } ``` ### クラッシュを例外としてカウントする デフォルトでは、メモリ不足などでワーカープロセスがクラッシュまたは強制終了して終わった試行は、ジョブの最大例外数(`MaxExceptions`)にカウントされません。これらの試行も例外としてカウントしたい場合は、ジョブクラスに `CountCrashesAsExceptions` 属性を追加します。 ```php theme={null} use Illuminate\Queue\Attributes\CountCrashesAsExceptions; use Illuminate\Queue\Attributes\MaxExceptions; use Illuminate\Queue\Attributes\Tries; #[Tries(25)] #[MaxExceptions(3)] #[CountCrashesAsExceptions] class ProcessPodcast implements ShouldQueue { // ... } ``` この属性があると、ワーカーはジョブの処理中にアプリケーションのキャッシュへマーカーを保存します。次にジョブが試行されたときにマーカーが残っていれば、前回の試行が例外としてカウントされます。 ## ジョブミドルウェアによる実行制御 ジョブミドルウェアを使うと、レート制限や多重実行防止といった横断的なロジックを `handle()` メソッドから切り離し、`middleware()` メソッドで宣言的に指定できます。ロジックをジョブ本体に書かないので、複数のジョブで同じ制御を再利用しやすくなります。 ```php theme={null} /** * ジョブが通過するミドルウェアを返す */ public function middleware(): array { return [new RateLimited('backups')]; } ``` 独自のジョブミドルウェアは `make:job-middleware` Artisanコマンドで生成できます。ジョブミドルウェアは[キューイベントリスナー](/jp/events)・[メーラブル](/jp/mail)・[通知](/jp/notifications)にも指定可能です。 ### レート制限(RateLimited) `RateLimiter` ファサードの `for` メソッドでレート制限を定義し、`Illuminate\Queue\Middleware\RateLimited` ミドルウェアをジョブに適用します。 ```php theme={null} use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Support\Facades\RateLimiter; // AppServiceProvider::boot() で定義 RateLimiter::for('backups', function (object $job) { return $job->user->vipCustomer() ? Limit::none() : Limit::perHour(1)->by($job->user->id); }); ``` ```php theme={null} use Illuminate\Queue\Middleware\RateLimited; public function middleware(): array { return [new RateLimited('backups')]; } ``` 制限を超えたジョブは、レート制限の残り時間に応じて自動的にキューへリリースされます。`releaseAfter()` で再試行までの秒数を固定したり、`dontRelease()` でリトライさせずそのまま終了させることもできます。 ```php theme={null} public function middleware(): array { return [(new RateLimited('backups'))->releaseAfter(60)]; // または、リトライしない場合 // return [(new RateLimited('backups'))->dontRelease()]; } ``` リリースされたジョブも試行回数(`attempts`)はカウントされます。`#[Tries]` や [`retryUntil()`](#ジョブクラスにリトライ設定を書く) を適切に設定してください。 Redisを使っている場合は、より高速な `Illuminate\Queue\Middleware\RateLimitedWithRedis` が利用できます。 ```php theme={null} use Illuminate\Queue\Middleware\RateLimitedWithRedis; public function middleware(): array { return [new RateLimitedWithRedis('backups')]; } ``` ### 多重実行の防止(WithoutOverlapping) `Illuminate\Queue\Middleware\WithoutOverlapping` は、任意のキーを基準に同じジョブが同時に複数実行されるのを防ぎます。特定のリソースを1件ずつしか更新したくない場合に有効です。 ```php theme={null} use Illuminate\Queue\Middleware\WithoutOverlapping; public function middleware(): array { // 同じユーザーIDの信用スコア更新ジョブが重複しないようにする return [new WithoutOverlapping($this->user->id)]; } ``` 重複したジョブはキューへリリースされ、`releaseAfter()` で再試行間隔を、`dontRelease()` で即座に削除するかを指定できます。ロックは[アトミックロック](/jp/cache#アトミック操作(ロック))機能を利用しているため、`expireAfter()` でロックの有効期限を明示しておくと、ジョブが予期せず失敗・タイムアウトした際もロックが残り続けません。 ```php theme={null} public function middleware(): array { return [ (new WithoutOverlapping($this->order->id)) ->releaseAfter(60) ->expireAfter(180), ]; } ``` デフォルトでは同一ジョブクラス内でのみ重複を防ぎます。異なるジョブクラス間でもロックキーを共有したい場合は `shared()` メソッドを使います。 ### 例外の連続発生を抑制(ThrottlesExceptions) `Illuminate\Queue\Middleware\ThrottlesExceptions` は、外部APIなど不安定なサービスと連携するジョブで、一定回数例外が発生したら以降の実行を一時停止する仕組みです。[時間ベースの試行制限](#ジョブクラスにリトライ設定を書く)(`retryUntil()`)と組み合わせて使うのが一般的です。 ```php theme={null} use DateTime; use Illuminate\Queue\Middleware\ThrottlesExceptions; public function middleware(): array { // 10回連続で例外が発生したら5分間ジョブの実行を止める return [new ThrottlesExceptions(10, 5 * 60)]; } public function retryUntil(): DateTime { return now()->addMinutes(30); } ``` `when()` で特定の例外だけをスロットリング対象にしたり、`deleteWhen()` で特定の例外が発生したらジョブを削除するといった細かい制御も可能です。 ```php theme={null} use App\Exceptions\CustomerDeletedException; use Illuminate\Http\Client\HttpClientException; use Illuminate\Queue\Middleware\ThrottlesExceptions; public function middleware(): array { return [ (new ThrottlesExceptions(10, 10 * 60)) ->when(fn ($e) => $e instanceof HttpClientException) ->deleteWhen(CustomerDeletedException::class), ]; } ``` Redisを使っている場合は `Illuminate\Queue\Middleware\ThrottlesExceptionsWithRedis` を利用すると、より効率的にスロットリングを行えます。 ## ジョブのリリース(Release ミドルウェア) 特定の条件を満たすときにジョブを実行せずキューに戻したい場合、`Release` ミドルウェアを使うと簡潔に書けます。 ```php theme={null} use Illuminate\Queue\Middleware\Release; /** * ジョブが通過するミドルウェアを返す */ public function middleware(): array { return [ // $condition が true のとき60秒後にリリース Release::when($this->order->isPending(), releaseAfter: 60), ]; } ``` `Release::unless()` は条件が `false` のときにリリースします。 ```php theme={null} return [ // 注文が支払い済みでないとき60秒後にリリース Release::unless($this->order->isPaid(), releaseAfter: 60), ]; ``` クロージャを使うと、より複雑な条件を記述できます。 ```php theme={null} return [ Release::when(function (): bool { return ! $this->order->isPaid(); }, releaseAfter: 60), ]; ``` ジョブをリリースしてもジョブの試行回数はカウントアップされます。`#[Tries]` や `$tries` プロパティを適切に設定してください。 ## 失敗したジョブの処理 ### failed\_jobs テーブルの準備 ジョブが最大試行回数を超えると、`failed_jobs` テーブルに記録されます。 テーブルがない場合は次のコマンドで作成します。 ```shell theme={null} php artisan make:queue-failed-table php artisan migrate ``` ### 失敗時のクリーンアップ ジョブに `failed()` メソッドを定義すると、失敗したときの後処理を記述できます。 ```php theme={null} use Throwable; public function failed(?Throwable $exception): void { // 管理者にSlack通知を送るなど // Notification::route('slack', config('app.slack_webhook')) // ->notify(new JobFailedNotification($this, $exception)); } ``` ### 例外によるリトライ停止 例外の種類によってはリトライさせず即座に失敗させたい場合があります。`bootstrap/app.php` の `withExceptions()` 内で `dontRetry` を使って対象の例外クラスを指定します。 ```php theme={null} use App\Exceptions\InvalidPodcastSourceException; use Illuminate\Foundation\Configuration\Exceptions; ->withExceptions(function (Exceptions $exceptions): void { $exceptions->dontRetry([ InvalidPodcastSourceException::class, ]); }) ``` より細かい制御が必要な場合は `dontRetryWhen` にクロージャを渡します。クロージャが `true` を返すとジョブは即座に失敗としてマークされ、リトライされません。 ```php theme={null} use App\Exceptions\PodcastProcessingException; use Illuminate\Foundation\Configuration\Exceptions; ->withExceptions(function (Exceptions $exceptions): void { $exceptions->dontRetryWhen(function (PodcastProcessingException $e) { return $e->reason() === 'Subscription expired'; }); }) ``` バリデーションエラーや決済失敗(サブスクリプション期限切れなど)のように、何度リトライしても結果が変わらない例外はこの方法で即座に失敗させると効率的です。 ### 失敗したジョブの一覧確認 ```shell theme={null} php artisan queue:failed ``` ### 失敗したジョブのリトライ ```shell theme={null} # 特定のジョブIDを指定してリトライ php artisan queue:retry ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece # すべての失敗ジョブをリトライ php artisan queue:retry all ``` ### 失敗したジョブの削除 ```shell theme={null} # 特定のジョブを削除 php artisan queue:forget ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece # すべての失敗ジョブを削除 php artisan queue:flush ``` ## よく使うキュードライバ ### database ドライバ 追加のミドルウェアなしに使い始められるシンプルなドライバです。 `jobs` テーブルにジョブを保存し、ワーカーがポーリングして処理します。 * **長所**: セットアップが簡単、既存のRDBMSをそのまま使える * **短所**: データベースへの負荷が高いため、大量のジョブには不向き ```ini theme={null} QUEUE_CONNECTION=database ``` ### redis ドライバ 本番環境で最もよく使われる高速なドライバです。 インメモリで動作するためデータベースよりもスループットが高く、大量のジョブを処理できます。 * **長所**: 高速、スケーラブル * **短所**: Redisサーバーの用意が必要 ```ini theme={null} QUEUE_CONNECTION=redis REDIS_HOST=127.0.0.1 REDIS_PORT=6379 ``` Redisキューを本番運用する場合は [Laravel Horizon](https://laravel.com/docs/horizon) の導入を検討してください。 美しいダッシュボードでジョブの状況をリアルタイムに監視できます。 ## Supervisorによる本番運用 本番環境では、`queue:work` プロセスが何らかの理由で停止したときに自動で再起動する仕組みが必要です。 Linux環境では **Supervisor** を使うのが一般的です。 ```ini theme={null} # /etc/supervisor/conf.d/laravel-worker.conf [program:laravel-worker] process_name=%(program_name)s_%(process_num)02d command=php /var/www/your-app/artisan queue:work --sleep=3 --tries=3 --max-time=3600 autostart=true autorestart=true stopasgroup=true killasgroup=true user=www-data numprocs=2 redirect_stderr=true stdout_logfile=/var/www/your-app/storage/logs/worker.log stopwaitsecs=3600 ``` `numprocs=2` で2つのワーカープロセスを並列起動します。 設定後に Supervisor を再読み込みします。 ```shell theme={null} sudo supervisorctl reread sudo supervisorctl update sudo supervisorctl start laravel-worker:* ``` ## 実践例: メール送信をキューで処理する ```shell theme={null} php artisan make:job SendOrderConfirmation ``` ```php theme={null} order->user->email) ->send(new OrderConfirmed($this->order)); } } ``` ```php theme={null} use App\Jobs\SendOrderConfirmation; public function store(Request $request): RedirectResponse { $order = Order::create($request->validated()); SendOrderConfirmation::dispatch($order); return redirect()->route('orders.show', $order) ->with('success', '注文を受け付けました。'); } ``` ```shell theme={null} php artisan queue:work --tries=3 --timeout=30 ``` ## まとめ * メール・SMS送信 * 画像・動画のリサイズや変換 * 外部APIへのリクエスト * レポートの生成やCSVエクスポート * Webhook の送信 `.env` で `QUEUE_CONNECTION=sync` にすると、ジョブはキューを経由せず即時実行されます。 ワーカーを起動しなくても動作確認できるため、開発中は便利です。 ```ini theme={null} QUEUE_CONNECTION=sync ``` ```shell theme={null} # ワーカー起動 php artisan queue:work # ワーカー再起動(デプロイ後) php artisan queue:restart # 失敗ジョブ一覧 php artisan queue:failed # 失敗ジョブをすべてリトライ php artisan queue:retry all # 失敗ジョブをすべて削除 php artisan queue:flush ``` # Redis Source: https://kawax.biz/jp/redis LaravelアプリケーションでRedisを使ったキャッシュ・セッション・キュー・Pub/Subの設定と操作方法 Redis はインメモリのキー・バリューストアで、Laravel の多くの機能のバックエンドとして使われます。 設定から操作・Pub/Sub まで、Laravel での Redis の使い方を解説します。 Redis をキャッシュドライバとして使う Redis をキュードライバとして使う Pub/Sub でリアルタイム通信 ## Redis とは [Redis](https://redis.io) はオープンソースの高速なキー・バリューストアです。 文字列・ハッシュ・リスト・セット・ソート済みセットなど多様なデータ構造をサポートし、データ構造サーバーとも呼ばれます。 Laravel では以下の用途で Redis が使われます。 ```mermaid theme={null} flowchart LR A["Laravel
    アプリケーション"] --> B["Redis"] subgraph uses ["主な用途"] C["キャッシュ
    Cache"] D["セッション
    Session"] E["キュー
    Queue"] F["ブロードキャスト
    Broadcasting"] end B --> C B --> D B --> E B --> F ``` ## クライアントの選択 Laravel は **PhpRedis**(PHP 拡張)と **Predis**(PHP パッケージ)の 2 つのクライアントをサポートしています。 | 項目 | PhpRedis | Predis | | - | - | - | | 実装 | C 言語による PHP 拡張 | 純粋な PHP パッケージ | | インストール | PECL 拡張が必要 | `composer require` で完結 | | パフォーマンス | 高速 | PhpRedis より若干低速 | | Laravel Sail | デフォルトで導入済み | 別途インストールが必要 | | 推奨環境 | 本番環境 | 開発環境・インストールが難しい環境 | Laravel 13 ではデフォルトクライアントが PhpRedis です。本番環境では PhpRedis の使用を推奨します。 Laravel Sail を使っている場合は PhpRedis がすでにインストールされています。 ## 設定 ### config/database.php Redis の設定は `config/database.php` の `redis` 配列で管理します。 ```php theme={null} 'redis' => [ 'client' => env('REDIS_CLIENT', 'phpredis'), 'options' => [ 'cluster' => env('REDIS_CLUSTER', 'redis'), 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), ], 'default' => [ 'url' => env('REDIS_URL'), 'host' => env('REDIS_HOST', '127.0.0.1'), 'username' => env('REDIS_USERNAME'), 'password' => env('REDIS_PASSWORD'), 'port' => env('REDIS_PORT', '6379'), 'database' => env('REDIS_DB', '0'), ], 'cache' => [ 'url' => env('REDIS_URL'), 'host' => env('REDIS_HOST', '127.0.0.1'), 'username' => env('REDIS_USERNAME'), 'password' => env('REDIS_PASSWORD'), 'port' => env('REDIS_PORT', '6379'), 'database' => env('REDIS_CACHE_DB', '1'), ], ], ``` ### 環境変数 `.env` で接続先を設定します。 ```ini theme={null} REDIS_CLIENT=phpredis REDIS_HOST=127.0.0.1 REDIS_PORT=6379 REDIS_PASSWORD=null REDIS_DB=0 REDIS_CACHE_DB=1 ``` URL 形式での指定も可能です。 ```php theme={null} 'default' => [ 'url' => 'tcp://127.0.0.1:6379?database=0', ], 'cache' => [ 'url' => 'tls://user:password@127.0.0.1:6380?database=1', ], ``` ### TLS / SSL 接続 TLS 暗号化を使う場合は `scheme` オプションを指定します。 ```php theme={null} 'default' => [ 'scheme' => 'tls', 'url' => env('REDIS_URL'), 'host' => env('REDIS_HOST', '127.0.0.1'), 'username' => env('REDIS_USERNAME'), 'password' => env('REDIS_PASSWORD'), 'port' => env('REDIS_PORT', '6379'), 'database' => env('REDIS_DB', '0'), ], ``` ### クラスタリング 複数の Redis サーバーをクラスターとして使う場合は `clusters` キーを使います。 ```php theme={null} 'redis' => [ 'client' => env('REDIS_CLIENT', 'phpredis'), 'options' => [ 'cluster' => env('REDIS_CLUSTER', 'redis'), 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), ], 'clusters' => [ 'default' => [ [ 'url' => env('REDIS_URL'), 'host' => env('REDIS_HOST', '127.0.0.1'), 'username' => env('REDIS_USERNAME'), 'password' => env('REDIS_PASSWORD'), 'port' => env('REDIS_PORT', '6379'), 'database' => env('REDIS_DB', '0'), ], ], ], ], ``` デフォルトでは `options.cluster` が `redis` に設定されており、ネイティブ Redis クラスタリングが使われます。 フェイルオーバーを自動で処理するため、本番環境での推奨設定です。 Predis でクライアントサイドシャーディングを使う場合は `options.cluster` を削除します。 ただしクライアントサイドシャーディングはフェイルオーバーを処理しないため、キャッシュなど一時的なデータにのみ適しています。 ### Predis の設定 Predis を使うには `predis/predis` パッケージをインストールし、`REDIS_CLIENT` を `predis` に変更します。 ```shell theme={null} composer require predis/predis ``` ```ini theme={null} REDIS_CLIENT=predis ``` Predis 固有の[接続パラメーター](https://github.com/nrk/predis/wiki/Connection-Parameters)を追加できます。 ```php theme={null} 'default' => [ 'url' => env('REDIS_URL'), 'host' => env('REDIS_HOST', '127.0.0.1'), 'username' => env('REDIS_USERNAME'), 'password' => env('REDIS_PASSWORD'), 'port' => env('REDIS_PORT', '6379'), 'database' => env('REDIS_DB', '0'), 'read_write_timeout' => 60, ], ``` #### Predis のリトライ設定 Predis 3.4.0 以降では組み込みのリトライ・バックオフ設定が利用できます。`max_retries` オプションでリトライ回数を、`retry` オプションでバックオフ戦略を設定します。`retry` オプションには `NoBackoff`、`EqualBackoff`、`ExponentialBackoff` のいずれかのクラス名をキーとした配列を指定します。 ```php theme={null} use Predis\Retry\Strategy\ExponentialBackoff; 'default' => [ 'url' => env('REDIS_URL'), // ... 'retry' => [ ExponentialBackoff::class => [ env('REDIS_BACKOFF_BASE', 100), env('REDIS_BACKOFF_CAP', 1000), true, // ジッターを有効化 ], ], 'max_retries' => env('REDIS_MAX_RETRIES', 3), ], ``` Redis クラスターで Predis を使う場合は、クラスター設定の `parameters` オプションでリトライを設定します。 ```php theme={null} use Predis\Retry\Strategy\NoBackoff; 'clusters' => [ 'default' => [ // ... ], ], 'options' => [ 'cluster' => env('REDIS_CLUSTER', 'redis'), 'parameters' => [ 'retry' => [ NoBackoff::class => [], ], 'max_retries' => env('REDIS_MAX_RETRIES', 3), ], ], ``` ### PhpRedis の設定 PhpRedis は PECL 経由でインストールします(Laravel Sail では導入済み)。 PhpRedis は以下の追加オプションをサポートします: `name`, `persistent`, `persistent_id`, `prefix`, `read_timeout`, `retry_interval`, `max_retries`, `backoff_algorithm`, `backoff_base`, `backoff_cap`, `timeout`, `context` ```php theme={null} 'default' => [ 'url' => env('REDIS_URL'), 'host' => env('REDIS_HOST', '127.0.0.1'), 'username' => env('REDIS_USERNAME'), 'password' => env('REDIS_PASSWORD'), 'port' => env('REDIS_PORT', '6379'), 'database' => env('REDIS_DB', '0'), 'read_timeout' => 60, 'context' => [ // 'auth' => ['username', 'secret'], // 'stream' => ['verify_peer' => false], ], ], ``` #### リトライとバックオフ設定 接続失敗時のリトライ動作を設定します。 サポートされるバックオフアルゴリズム: `default`, `decorrelated_jitter`, `equal_jitter`, `exponential`, `uniform`, `constant` ```php theme={null} 'default' => [ 'url' => env('REDIS_URL'), 'host' => env('REDIS_HOST', '127.0.0.1'), 'username' => env('REDIS_USERNAME'), 'password' => env('REDIS_PASSWORD'), 'port' => env('REDIS_PORT', '6379'), 'database' => env('REDIS_DB', '0'), 'max_retries' => env('REDIS_MAX_RETRIES', 3), 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), ], ``` #### Unix ソケット接続 同一サーバー上の Redis に接続する場合、Unix ソケットを使うと TCP オーバーヘッドを削減できます。 ```ini theme={null} REDIS_HOST=/run/redis/redis.sock REDIS_PORT=0 ``` #### シリアライズと圧縮 PhpRedis では保存データのシリアライズと圧縮アルゴリズムを設定できます。 ```php theme={null} 'redis' => [ 'client' => env('REDIS_CLIENT', 'phpredis'), 'options' => [ 'cluster' => env('REDIS_CLUSTER', 'redis'), 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), 'serializer' => Redis::SERIALIZER_MSGPACK, 'compression' => Redis::COMPRESSION_LZ4, ], ], ``` サポートされるシリアライザー: | 定数 | 説明 | | - | - | | `Redis::SERIALIZER_NONE` | シリアライズなし(デフォルト) | | `Redis::SERIALIZER_PHP` | PHP のシリアライズ | | `Redis::SERIALIZER_JSON` | JSON | | `Redis::SERIALIZER_IGBINARY` | igbinary | | `Redis::SERIALIZER_MSGPACK` | MessagePack | サポートされる圧縮アルゴリズム: | 定数 | 説明 | | - | - | | `Redis::COMPRESSION_NONE` | 圧縮なし(デフォルト) | | `Redis::COMPRESSION_LZF` | LZF | | `Redis::COMPRESSION_ZSTD` | Zstandard | | `Redis::COMPRESSION_LZ4` | LZ4 | ## Redis との操作 ### Redis ファサード `Redis` ファサードを使ってあらゆる Redis コマンドを実行できます。 ファサードはマジックメソッドでコマンドを Redis サーバーに転送します。 ```php theme={null} Redis::get('user:profile:'.$id) ]); } } ``` 引数を受け取るコマンドは、そのままメソッドの引数として渡します。 ```php theme={null} use Illuminate\Support\Facades\Redis; Redis::set('name', 'Taylor'); $values = Redis::lrange('names', 5, 10); ``` `command` メソッドを使ってコマンド名と引数を明示的に指定することもできます。 ```php theme={null} $values = Redis::command('lrange', ['name', 5, 10]); ``` ### 複数接続の使い分け `config/database.php` で複数の Redis 接続を定義し、`connection()` メソッドで切り替えられます。 ```php theme={null} // 名前付き接続を取得 $redis = Redis::connection('connection-name'); // デフォルト接続を取得 $redis = Redis::connection(); ``` ### トランザクション `transaction()` メソッドは Redis の `MULTI`/`EXEC` コマンドをラップします。 クロージャ内のすべてのコマンドがアトミックに実行されます。 ```php theme={null} use Redis; use Illuminate\Support\Facades; Facades\Redis::transaction(function (Redis $redis) { $redis->incr('user_visits', 1); $redis->incr('total_visits', 1); }); ``` トランザクション内では Redis から値を取得できません。 クロージャ全体が実行されてから `EXEC` でまとめて実行されるためです。 ### Lua スクリプト `eval()` メソッドで Lua スクリプトをアトミックに実行できます。 スクリプト内で Redis の値を参照・更新できる点でトランザクションより柔軟です。 ```php theme={null} $value = Redis::eval(<<<'LUA' local counter = redis.call("incr", KEYS[1]) if counter > 5 then redis.call("incr", KEYS[2]) end return counter LUA, 2, 'first-counter', 'second-counter'); ``` 引数の順序: Lua スクリプト → キー数 → キー名... → 追加引数... `KEYS[1]`, `KEYS[2]` がキー名、`ARGV[1]` 以降が追加引数として渡されます。 ### パイプライン 大量のコマンドを一度に送信してネットワークのラウンドトリップを削減できます。 ```php theme={null} use Redis; use Illuminate\Support\Facades; Facades\Redis::pipeline(function (Redis $pipe) { for ($i = 0; $i < 1000; $i++) { $pipe->set("key:$i", $i); } }); ``` ```mermaid theme={null} sequenceDiagram participant App as Laravel
    アプリ participant Redis Note over App,Redis: 通常(コマンドごとに往復) App->>Redis: SET key:0 0 Redis-->>App: OK App->>Redis: SET key:1 1 Redis-->>App: OK Note over App,Redis: パイプライン(まとめて送信) App->>Redis: SET key:0 0 / SET key:1 1 / ... Redis-->>App: OK / OK / ... ``` パイプラインはコマンドをまとめて送信するだけで、アトミックではありません。 アトミックな操作が必要な場合はトランザクションまたは Lua スクリプトを使ってください。 ## Pub / Sub Redis の `publish` / `subscribe` コマンドを使ってチャンネル経由でメッセージをやりとりできます。 ### サブスクライバー `subscribe()` は長時間動作するプロセスなので、Artisan コマンド内で呼び出します。 ```php theme={null} 'Adam Wathan' ])); }); ``` ### ワイルドカードサブスクリプション `psubscribe()` でパターンマッチングのサブスクリプションができます。 ```php theme={null} // すべてのチャンネルを購読 Redis::psubscribe(['*'], function (string $message, string $channel) { echo $message; }); // users.* パターンのチャンネルを購読 Redis::psubscribe(['users.*'], function (string $message, string $channel) { echo $message; }); ``` ## まとめ * **本番環境**: PhpRedis(PECL 拡張、高パフォーマンス) * **開発環境・インストールが難しい場合**: Predis(Composer パッケージ) * **Laravel Sail**: PhpRedis がデフォルトで導入済み * 複数の Redis 接続は `config/database.php` で定義し、用途ごとに使い分ける(`default` / `cache` など) * クラスター環境では `clusters` キーを使う * 本番環境では TLS 接続と認証情報の設定を検討する | 操作 | 用途 | | - | - | | ファサードメソッド | 通常の Redis コマンド | | `transaction()` | 複数コマンドのアトミック実行(値の読み取りは不可) | | `eval()` | Lua スクリプト(読み取りを伴うアトミック操作) | | `pipeline()` | 大量コマンドの高速送信(非アトミック) | | `subscribe()` / `publish()` | チャンネルメッセージング(Pub/Sub) | # Laravel Reverb Source: https://kawax.biz/jp/reverb LaravelアプリケーションにリアルタイムWebSocket通信を提供する公式サーバーパッケージ このページはReverb サーバー自体のセットアップと運用に焦点を当てています。 ブロードキャストイベントの作成やLaravel Echoの使い方は[ブロードキャスト](/jp/broadcasting)を参照してください。 ## Reverbとは [Laravel Reverb](https://github.com/laravel/reverb) は、Laravelが公式に提供するセルフホスト型WebSocketサーバーです。 外部サービスへの依存なしに、高速でスケーラブルなリアルタイム通信をLaravelアプリケーションに追加できます。 Reverbは[ブロードキャスト](/jp/broadcasting)基盤の上で動作し、サーバーサイドのイベントをWebSocket経由でブラウザへ届ける役割を担います。 ```mermaid theme={null} flowchart LR A["ブラウザ
    Laravel Echo"] -->|"WebSocket接続"| B["Laravel Reverb
    サーバー"] B -->|"認可・ルーティング"| C["チャンネル
    管理"] C -->|"イベント配信"| A D["Laravelアプリ
    イベント発火"] -->|"ブロードキャスト
    (キュー経由)"| B ``` ## インストール `install:broadcasting` Artisanコマンドを使うと、Reverb を含むすべての依存関係を一括でインストールできます。 ```shell theme={null} php artisan install:broadcasting ``` コマンド実行時にReverbを選択するか、`--reverb` オプションを付けて実行すると自動的にReverbがセットアップされます。 ```shell theme={null} php artisan install:broadcasting --reverb ``` このコマンドは以下を行います。 * Composer パッケージ (`laravel/reverb`) のインストール * NPM パッケージ (`laravel-echo`、`pusher-js`) のインストール * `.env` への環境変数の追加 * `config/reverb.php` の生成 手動でインストールする場合は、Composerでパッケージを追加してから `reverb:install` を実行します。 ```shell theme={null} composer require laravel/reverb php artisan reverb:install ``` ## 設定 ### アプリケーション認証情報 クライアントとサーバーの接続確立に使用する認証情報を環境変数で設定します。 ```ini theme={null} REVERB_APP_ID=my-app-id REVERB_APP_KEY=my-app-key REVERB_APP_SECRET=my-app-secret ``` これらの値は `config/reverb.php` の `apps` セクションで参照されます。 ### 許可するオリジン (Allowed Origins) クライアントリクエストを許可するオリジンを `config/reverb.php` の `allowed_origins` で制限できます。 ```php theme={null} 'apps' => [ [ 'app_id' => 'my-app-id', 'allowed_origins' => ['laravel.com'], // ... ] ] ``` すべてのオリジンを許可する場合は `*` を指定します。 ### 複数アプリ対応 1つのReverbサーバーで複数のアプリケーションに対応できます。 `config/reverb.php` の `apps` 配列に複数のエントリを追加します。 ```php theme={null} 'apps' => [ [ 'app_id' => 'my-app-one', // ... ], [ 'app_id' => 'my-app-two', // ... ], ], ``` ### SSL設定 本番環境では、NginxなどのWebサーバーがSSL終端を担当し、Reverbへリクエストをプロキシするのが一般的です。 ローカル開発環境でSecure WebSocket (`wss://`) を使いたい場合は、Laravel HerdやValetの証明書を活用できます。 ```shell theme={null} php artisan reverb:start --host="0.0.0.0" --port=8080 --hostname="laravel.test" ``` 証明書を手動で指定する場合は `config/reverb.php` の `tls` オプションを設定します。 ```php theme={null} 'options' => [ 'tls' => [ 'local_cert' => '/path/to/cert.pem' ], ], ``` ## サーバーの起動 `reverb:start` Artisanコマンドでサーバーを起動します。 ```shell theme={null} php artisan reverb:start ``` デフォルトでは `0.0.0.0:8080` で起動します。 `--host` / `--port` オプションでカスタムアドレスを指定できます。 ```shell theme={null} php artisan reverb:start --host=127.0.0.1 --port=9000 ``` 環境変数でも指定できます。 ```ini theme={null} REVERB_SERVER_HOST=0.0.0.0 REVERB_SERVER_PORT=8080 ``` `REVERB_SERVER_HOST` / `REVERB_SERVER_PORT` はサーバー自体のリスンアドレスです。 `REVERB_HOST` / `REVERB_PORT` はLaravelアプリがブロードキャストメッセージを送信する先のアドレスです。 本番環境ではこれらが異なる場合があります。 ### デバッグモード パフォーマンスのため、Reverbはデフォルトでデバッグ情報を出力しません。 接続やメッセージの流れを確認したい場合は `--debug` オプションを使います。 ```shell theme={null} php artisan reverb:start --debug ``` ### サーバーの再起動 Reverbは常駐プロセスのため、コードの変更を反映するには再起動が必要です。 `reverb:restart` コマンドは、すべての接続を graceful に終了してからサーバーを停止します。 ```shell theme={null} php artisan reverb:restart ``` Supervisorなどのプロセスマネージャーを使っている場合、停止後に自動的に再起動されます。 ## モニタリング Reverbは[Laravel Pulse](https://laravel.com/docs/pulse) との連携をサポートしています。 接続数やメッセージ数をリアルタイムでダッシュボードに表示できます。 まず `config/pulse.php` にReverbのレコーダーを追加します。 ```php theme={null} use Laravel\Reverb\Pulse\Recorders\ReverbConnections; use Laravel\Reverb\Pulse\Recorders\ReverbMessages; 'recorders' => [ ReverbConnections::class => [ 'sample_rate' => 1, ], ReverbMessages::class => [ 'sample_rate' => 1, ], // ... ], ``` 次に、Pulseダッシュボードのテンプレートにカードを追加します。 ```blade theme={null} ... ``` 接続状況を正しく記録するため、Reverbサーバーで `pulse:check` デーモンを起動してください。 水平スケーリング構成の場合、`pulse:check` は1台のサーバーでのみ実行します。 ## 本番環境での運用 ### ファイルオープン制限 WebSocket接続は1接続につき1ファイルディスクリプターを消費します。 OSレベルの制限を確認し、必要に応じて上限を引き上げます。 ```shell theme={null} ulimit -n ``` `/etc/security/limits.conf` で上限を変更できます。 ```ini theme={null} # /etc/security/limits.conf forge soft nofile 10000 forge hard nofile 10000 ``` ### イベントループ (ext-uv) ReverbはデフォルトでPHPの `stream_select` を使いますが、これは最大1,024ファイルの制限があります。 1,000以上の同時接続を扱う場合は `ext-uv` をインストールして制限を解放します。 ```shell theme={null} pecl install uv ``` `ext-uv` が利用可能な場合、Reverbは自動的にそちらを使用します。 ### Nginxリバースプロキシ 本番環境ではReverbを直接公開せず、NginxなどのWebサーバーでプロキシします。 以下はNginxの設定例です。 ```nginx theme={null} server { ... location / { proxy_http_version 1.1; proxy_set_header Host $http_host; proxy_set_header Scheme $scheme; proxy_set_header SERVER_PORT $server_port; proxy_set_header REMOTE_ADDR $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; proxy_pass http://0.0.0.0:8080; } ... } ``` ReverbはWebSocket接続を `/app` で、APIリクエストを `/apps` で受け付けます。 Webサーバーの設定で両方のURIへのアクセスを許可してください。 同時接続数を増やすには、`nginx.conf` の `worker_rlimit_nofile` と `worker_connections` を調整します。 ```nginx theme={null} user forge; worker_processes auto; pid /run/nginx.pid; include /etc/nginx/modules-enabled/*.conf; worker_rlimit_nofile 10000; events { worker_connections 10000; multi_accept on; } ``` ### プロセス管理 (Supervisor) 本番環境ではSupervisorでReverbプロセスを管理します。 `supervisor.conf` の `minfds` を設定して、必要なファイルディスクリプターを確保します。 ```ini theme={null} [supervisord] ... minfds=10000 ``` Supervisorの設定例: ```ini theme={null} [program:reverb] process_name=%(program_name)s command=php /path/to/artisan reverb:start autostart=true autorestart=true user=forge redirect_stderr=true stdout_logfile=/path/to/reverb.log ``` ### スケーリング (Redis) 単一サーバーでは処理しきれない接続数が必要な場合、Redisのpub/sub機能を使った水平スケーリングが可能です。 ```mermaid theme={null} flowchart TB LB["ロードバランサー"] --> R1["Reverbサーバー 1"] LB --> R2["Reverbサーバー 2"] LB --> R3["Reverbサーバー 3"] R1 <-->|"pub/sub"| Redis["Redis"] R2 <-->|"pub/sub"| Redis R3 <-->|"pub/sub"| Redis ``` `.env` でスケーリングを有効化します。 ```env theme={null} REVERB_SCALING_ENABLED=true ``` ReverbはアプリケーションのデフォルトRedis接続を使ってサーバー間でメッセージをやり取りします。 複数のReverbサーバーを起動し、ロードバランサーでリクエストを分散します。 [Laravel Cloud](https://cloud.laravel.com) では、インフラ管理なしにReverb対応アプリをデプロイできる フルマネージドのWebSocketインフラを提供しています。 ## イベント Reverbは接続・メッセージのライフサイクルで以下のイベントを発行します。 [イベントリスナー](/jp/events)でこれらを受け取り、独自の処理を追加できます。 | イベント | 説明 | | - | - | | `ChannelCreated` | チャンネルが作成されたとき(最初の接続がサブスクライブ) | | `ChannelRemoved` | チャンネルが削除されたとき(最後の接続がアンサブスクライブ) | | `ConnectionPruned` | 古い接続がサーバーによって切断されたとき | | `MessageReceived` | クライアントからメッセージを受信したとき | | `MessageSent` | クライアントへメッセージを送信したとき | これらのイベントはすべて `Laravel\Reverb\Events` 名前空間に属します。 ## 次のステップ ブロードキャストイベントの作成、チャンネル認可、Laravel Echoの設定方法を確認する Reverbが発行するイベントを受け取るためのLaravelイベントシステムを学ぶ # Laravel Sail Source: https://kawax.biz/jp/sail Laravel Sail を使って Docker ベースのローカル開発環境をゼロコンフィグで構築する方法を解説します。 ## Sail とは [Laravel Sail](https://github.com/laravel/sail) は、Laravel の Docker 開発環境を操作するための軽量なコマンドラインインターフェースです。 PHP、MySQL、Redis を使った Laravel アプリケーションを、Docker の事前知識がなくても構築できます。 Sail の中心にあるのは、プロジェクトルートに置かれる `compose.yaml` ファイルと `sail` スクリプトです。 `sail` スクリプトは `compose.yaml` で定義された Docker コンテナを操作するための便利な CLI メソッドを提供します。 Laravel Sail は macOS、Linux、および Windows([WSL2](https://docs.microsoft.com/en-us/windows/wsl/about) 経由)で動作します。 **Laravel 13 では Sail は標準の開発環境ではなくなりました。** スケルトンの `composer.json` から `laravel/sail` が削除され、代わりに `composer setup` コマンドが用意されています。 標準はローカルの PHP + SQLite で動かす構成です。 ```shell theme={null} composer setup composer dev ``` Docker コンテナが必要な場合は、引き続き Sail をインストールして使うことができます。 Sail はローカル開発専用のツールです。本番環境への利用は想定されていません。 本番環境では適切な Docker / クラウド構成を別途用意してください。 *** ## インストール ### 既存プロジェクトへのインストール Composer でパッケージをインストールします。 ```shell theme={null} composer require laravel/sail --dev ``` `sail:install` Artisan コマンドを実行します。 このコマンドは `compose.yaml` をプロジェクトルートに発行し、`.env` に必要な環境変数を追記します。 ```shell theme={null} php artisan sail:install ``` 対話形式でサービスを選択できます。MySQL、Redis、Mailpit などを選択してください。 ```shell theme={null} ./vendor/bin/sail up ``` 初回起動時は Docker イメージのダウンロードに時間がかかります。 起動後は `http://localhost` でアプリケーションにアクセスできます。 Docker Desktop for Linux を使用している場合は、`docker context use default` を実行して `default` コンテキストを使用してください。 コンテナ内でファイルパーミッションエラーが発生した場合は、`SUPERVISOR_PHP_USER` 環境変数を `root` に設定してください。 ### 追加サービスの追加 既存の Sail インストールにサービスを追加するには `sail:add` コマンドを使います。 ```shell theme={null} php artisan sail:add ``` ### Devcontainer の利用 [Devcontainer](https://code.visualstudio.com/docs/remote/containers) 内で開発したい場合は `--devcontainer` オプションを使います。 ```shell theme={null} php artisan sail:install --devcontainer ``` *** ## 設定 ### シェルエイリアスの設定 デフォルトでは `./vendor/bin/sail` と毎回入力する必要があります。 シェルエイリアスを設定すると `sail` とだけ入力すれば済むようになります。 ```shell theme={null} alias sail='sh $([ -f sail ] && echo sail || echo vendor/bin/sail)' ``` `~/.zshrc` または `~/.bashrc` に追記してシェルを再起動してください。 ```shell theme={null} sail up ``` エイリアスを設定した後は、このドキュメントのすべてのコマンド例で `./vendor/bin/sail` の代わりに `sail` と入力できます。 ### イメージの再ビルド パッケージを最新の状態に保ちたい場合はイメージを再ビルドします。 ```shell theme={null} docker compose down -v sail build --no-cache sail up ``` *** ## 起動と停止 `compose.yaml` に定義されたすべての Docker コンテナを起動するには `up` コマンドを使います。 ```shell theme={null} # フォアグラウンドで起動 sail up # バックグラウンドで起動 sail up -d ``` 停止するには `stop` コマンドを使うか、フォアグラウンドで起動中なら `Ctrl + C` を押します。 ```shell theme={null} sail stop ``` ### 起動フロー ```mermaid theme={null} flowchart TD A["sail up を実行"] --> B{"Docker イメージが
    存在する?"} B -- いいえ --> C["イメージをビルド/
    ダウンロード"] C --> D["コンテナを起動"] B -- はい --> D D --> E["laravel.test コンテナ
    (アプリ)起動"] D --> F["mysql コンテナ起動"] D --> G["redis コンテナ起動"] D --> H["mailpit コンテナ起動"] E --> I["http://localhost でアクセス可能"] ``` *** ## コマンドの実行 Sail を使う場合、アプリケーションは Docker コンテナ内で動作します。 PHP コマンド、Artisan コマンド、Composer コマンド、Node / NPM コマンドはすべて `sail` 経由で実行します。 Laravel 公式ドキュメントでよく見かける `php artisan`、`composer`、`npm` コマンドは、 Sail 環境では先頭に `sail` を付けて実行してください。 ### PHP コマンド ```shell theme={null} sail php --version sail php script.php ``` ### Composer コマンド ```shell theme={null} sail composer require laravel/sanctum ``` ### Artisan コマンド ```shell theme={null} sail artisan migrate sail artisan queue:work ``` ### Node / NPM コマンド ```shell theme={null} sail node --version sail npm run dev # Yarn を使う場合 sail yarn ``` ### コンテナ CLI (シェル) コンテナ内で直接 Bash セッションを開くこともできます。 ```shell theme={null} sail shell # root ユーザーとして接続 sail root-shell ``` Tinker セッションを開くには次のコマンドを使います。 ```shell theme={null} sail tinker ``` *** ## サービス Sail が提供するサービスの概要です。インストール時に `sail:install` で選択できます。 ### MySQL デフォルトで `compose.yaml` に含まれています。 データは Docker Volume で永続化されます。初回起動時にアプリ用と `testing` 用の 2 つのデータベースが自動作成されます。 `.env` の `DB_HOST` を `mysql` に設定するとアプリからアクセスできます。 ```ini theme={null} DB_HOST=mysql DB_PORT=3306 ``` ローカルマシンから接続するには [TablePlus](https://tableplus.com) などの GUI ツールを使ってください。デフォルトポートは `3306` です。 ### Redis `.env` の `REDIS_HOST` を `redis` に設定するとアプリから Redis にアクセスできます。 ```ini theme={null} REDIS_HOST=redis REDIS_PORT=6379 ``` ### Valkey Redis の代替として [Valkey](https://valkey.io/) を使う場合は `REDIS_HOST` を `valkey` に設定します。 ### Mailpit ローカル開発中のメール送信を捕捉して Web UI でプレビューできます。 ```ini theme={null} MAIL_HOST=mailpit MAIL_PORT=1025 MAIL_ENCRYPTION=null ``` Sail 起動中は `http://localhost:8025` で Mailpit の Web UI にアクセスできます。 Sail のインストール時に Mailtrap Local を選択した場合、または後から `sail:add` Artisan コマンドで追加した場合は、`compose.yaml` に Mailtrap Local のサービスが追加されます。デフォルトのホスト名は `mailtrap-local`、ポートは `3535` です。 ```ini theme={null} MAIL_HOST=mailtrap-local MAIL_PORT=3535 ``` Sail 起動中は `http://localhost:3550` で Mailtrap Local の Web UI にアクセスできます。 ### Meilisearch / Typesense [Laravel Scout](/jp/scout) と統合して全文検索を試せます。 * Meilisearch: `MEILISEARCH_HOST=http://meilisearch:7700` * Typesense: `TYPESENSE_HOST=typesense`、`TYPESENSE_PORT=8108` など ### RustFS (S3 互換ストレージ) 本番で Amazon S3 を使う予定がある場合、ローカルで S3 互換ストレージをエミュレートできます。 ```ini theme={null} FILESYSTEM_DISK=s3 AWS_ACCESS_KEY_ID=sail AWS_SECRET_ACCESS_KEY=password AWS_DEFAULT_REGION=us-east-1 AWS_BUCKET=local AWS_ENDPOINT=http://rustfs:9000 AWS_USE_PATH_STYLE_ENDPOINT=true ``` *** ## テストの実行 ```shell theme={null} sail test sail test --group orders ``` `sail test` は内部的に `sail artisan test` と同等です。デフォルトで専用の `testing` データベースが用意されるため、開発データに影響しません。 ### Laravel Dusk Sail を使えば Selenium をローカルにインストールせずに Dusk のブラウザテストを実行できます。 `compose.yaml` の Selenium サービスのコメントを外してください。 ```yaml theme={null} selenium: image: 'selenium/standalone-chrome' extra_hosts: - 'host.docker.internal:host-gateway' volumes: - '/dev/shm:/dev/shm' networks: - sail ``` Apple Silicon (M1/M2/M3) の場合は `selenium/standalone-chromium` イメージを使ってください。 その後 Dusk テストを実行します。 ```shell theme={null} sail dusk ``` *** ## PHP / Node バージョン ### PHP バージョンの変更 `compose.yaml` の `laravel.test` コンテナの `build.context` を変更します。 ```yaml theme={null} # PHP 8.5 (デフォルト) context: ./vendor/laravel/sail/runtimes/8.5 # PHP 8.4 context: ./vendor/laravel/sail/runtimes/8.4 # PHP 8.3 context: ./vendor/laravel/sail/runtimes/8.3 ``` 変更後はイメージを再ビルドしてください。 ```shell theme={null} sail build --no-cache sail up ``` ### 追加の PHP 拡張機能 Sail のランタイムイメージには一般的な PHP 拡張機能が含まれています。アプリケーションが追加の拡張機能を必要とする場合、`compose.yaml` の `laravel.test` サービスに `PHP_EXTENSIONS` ビルド引数をスペース区切りで追加してイメージビルド時にインストールできます。 ```yaml theme={null} build: args: WWWGROUP: '${WWWGROUP}' PHP_EXTENSIONS: 'gmp imagick' ``` `compose.yaml` を更新したあとはコンテナイメージを再ビルドしてください。 ```shell theme={null} sail build --no-cache sail up ``` ### Node バージョンの変更 ```yaml theme={null} build: args: WWWGROUP: '${WWWGROUP}' NODE_VERSION: '20' ``` *** ## サイトの公開共有 同僚のプレビューや Webhook テストのために、一時的にサイトを外部公開できます。 ```shell theme={null} sail share ``` ランダムな `laravel-sail.site` URL が発行されます。URL 生成ヘルパーを正しく動作させるために、`bootstrap/app.php` で信頼するプロキシを設定してください。 ```php theme={null} ->withMiddleware(function (Middleware $middleware): void { $middleware->trustProxies(at: '*'); }) ``` サブドメインを指定することもできます。 ```shell theme={null} sail share --subdomain=my-sail-site ``` *** ## Xdebug ### 有効化 まず `sail:publish` で設定ファイルを発行してから、`.env` に以下を追加します。 ```ini theme={null} SAIL_XDEBUG_MODE=develop,debug,coverage ``` 発行された `php.ini` ファイルに次の設定が含まれていることを確認します。 ```ini theme={null} [xdebug] xdebug.mode=${XDEBUG_MODE} ``` 変更後はイメージを再ビルドします。 ```shell theme={null} sail build --no-cache ``` ### CLI デバッグ ```shell theme={null} # Xdebug なしで実行 sail artisan migrate # Xdebug ありで実行 sail debug migrate ``` ### ブラウザデバッグ ブラウザからデバッグセッションを開始する手順は [Xdebug 公式ドキュメント](https://xdebug.org/docs/step_debug#web-application) を参照してください。 PhpStorm を使っている場合は [Zero-configuration debugging](https://www.jetbrains.com/help/phpstorm/zero-configuration-debugging.html) の設定が便利です。 Sail は `artisan serve` を使ってアプリを提供します。 `XDEBUG_CONFIG` と `XDEBUG_MODE` を受け付けるのは Laravel 8.53.0 以降です。 それより古いバージョンではデバッグ接続が機能しません。 *** ## カスタマイズ Sail の Dockerfile や設定ファイルをカスタマイズするには `sail:publish` コマンドで発行します。 ```shell theme={null} sail artisan sail:publish ``` 発行後は `docker/` ディレクトリに Dockerfile が配置されます。 変更後はコンテナを再ビルドしてください。 ```shell theme={null} sail build --no-cache ``` *** ## 本番環境との違い Sail はローカル開発専用の環境です。本番環境での使用は想定されていません。 本番環境への Docker デプロイには Laravel Cloud、Forge、Ploi などのサービスや、 独自の Docker Compose / Kubernetes 構成を検討してください。 Sail と本番環境の主な違いを以下にまとめます。 | 項目 | Sail (ローカル) | 本番環境 | | - | - | - | | 目的 | 開発・デバッグ | サービス提供 | | Xdebug | 有効にできる | 無効推奨 | | Mailpit | メールを捕捉してプレビュー | 実際のメールサーバー | | データ永続化 | Docker Volume | マネージド DB など | | パフォーマンス | 最適化なし | 最適化必須 | # タスクスケジューリング Source: https://kawax.biz/jp/scheduling Laravelのスケジューラを使って定期実行タスクをエレガントに管理する方法を学びます。 ## タスクスケジューリングとは 従来、サーバーで定期実行が必要なタスクごとに cron エントリを手書きする必要がありました。 しかしこの方法ではスケジュール定義がソースコード外に存在するため、バージョン管理できず、確認・変更のたびに SSH ログインが必要になります。 Laravelのスケジューラを使うと、**アプリケーション内でスケジュールを流暢に定義**できます。 サーバーに登録する cron エントリは1行だけで済み、スケジュール定義はコードとともにバージョン管理されます。 スケジュールは `routes/console.php` に定義するのが標準的なスタイルです。 ```php theme={null} delete(); })->daily(); ``` `schedule:list` Artisanコマンドで、定義済みのタスク一覧と次回実行予定時刻を確認できます。 ```shell theme={null} php artisan schedule:list ``` ### スケジューラーの実行フロー ```mermaid theme={null} flowchart TD A["* * * * * artisan schedule:run
    (毎分 cron が呼び出す)"] --> B["Console Kernel"] B --> C["schedule() メソッド呼び出し"] C --> D["登録済みタスクを順番に評価"] D --> E{"Cron 式が現在時刻に
    一致しているか?"} E -->|"一致しない"| F["スキップ"] E -->|"一致する"| G{"when / skip /
    environments 条件チェック"} G -->|"条件不一致"| F G -->|"条件一致"| H{"メンテナンス
    モード中?"} H -->|"通常モード"| I["タスク実行
    (before フック → 本体 → after フック)"] H -->|"メンテナンスモード"| J{"evenInMaintenanceMode()
    が設定されているか?"} J -->|"はい"| I J -->|"いいえ"| F ``` ## スケジュールの定義方法 スケジュールの定義は `routes/console.php` に記述します。 `bootstrap/app.php` の `withSchedule` メソッドを使って定義する方法もあります。 ```php theme={null} // bootstrap/app.php use Illuminate\Console\Scheduling\Schedule; ->withSchedule(function (Schedule $schedule) { $schedule->call(new DeleteRecentUsers)->daily(); }) ``` ## スケジュール可能なタスクの種類 ### Artisanコマンドのスケジュール `command` メソッドでArtisanコマンドをスケジュールします。 コマンド名またはクラス名で指定できます。 ```php theme={null} use App\Console\Commands\SendEmailsCommand; use Illuminate\Support\Facades\Schedule; // コマンド名で指定 Schedule::command('emails:send Taylor --force')->daily(); // クラス名で指定(引数を配列で渡せる) Schedule::command(SendEmailsCommand::class, ['Taylor', '--force'])->daily(); ``` クロージャで定義したArtisanコマンドにも、定義の直後にスケジュールメソッドをチェーンできます。 ```php theme={null} Artisan::command('delete:recent-users', function () { DB::table('recent_users')->delete(); })->purpose('Delete recent users')->daily(); ``` ### キュージョブのスケジュール `job` メソッドで[キュージョブ](/jp/queues)をスケジュールします。 クロージャを使わずにジョブをキューに積む便利な方法です。 ```php theme={null} use App\Jobs\Heartbeat; use Illuminate\Support\Facades\Schedule; Schedule::job(new Heartbeat)->everyFiveMinutes(); ``` キュー名や接続先を指定することもできます。 ```php theme={null} // "heartbeats" キュー、"sqs" 接続でディスパッチ Schedule::job(new Heartbeat, 'heartbeats', 'sqs')->everyFiveMinutes(); ``` ### シェルコマンドのスケジュール `exec` メソッドでOSコマンドを実行します。 ```php theme={null} use Illuminate\Support\Facades\Schedule; Schedule::exec('node /home/forge/script.js')->daily(); ``` ### クロージャのスケジュール `call` メソッドで任意のPHPクロージャをスケジュールします。 ```php theme={null} use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schedule; Schedule::call(function () { DB::table('recent_users')->delete(); })->daily(); ``` `__invoke` メソッドを持つ Invocable オブジェクトも渡せます。 ```php theme={null} Schedule::call(new DeleteRecentUsers)->daily(); ``` ## スケジュール頻度の設定 ### 主な頻度メソッド よく使う頻度メソッドを以下に示します。 | メソッド | 説明 | | - | - | | `->everySecond()` | 毎秒実行 | | `->everyMinute()` | 毎分実行 | | `->everyFiveMinutes()` | 5分ごとに実行 | | `->everyFifteenMinutes()` | 15分ごとに実行 | | `->everyThirtyMinutes()` | 30分ごとに実行 | | `->hourly()` | 毎時実行 | | `->hourlyAt(17)` | 毎時17分に実行 | | `->daily()` | 毎日0時に実行 | | `->dailyAt('13:00')` | 毎日13時に実行 | | `->twiceDaily(1, 13)` | 毎日1時と13時に実行 | | `->weekly()` | 毎週日曜0時に実行 | | `->weeklyOn(1, '8:00')` | 毎週月曜8時に実行 | | `->monthly()` | 毎月1日0時に実行 | | `->monthlyOn(4, '15:00')` | 毎月4日15時に実行 | | `->quarterly()` | 四半期ごとの初日0時に実行 | | `->yearly()` | 毎年1月1日0時に実行 | | `->timezone('Asia/Tokyo')` | タイムゾーンを指定 | ### cron式で直接指定 `cron` メソッドで cron 式を直接指定することもできます。 ```php theme={null} use Illuminate\Support\Facades\Schedule; Schedule::command('emails:send')->cron('0 9 * * *'); // 毎日9時に実行 ``` ### 頻度と曜日の組み合わせ 頻度メソッドと曜日の制約を組み合わせて、より細かいスケジュールを作れます。 ```php theme={null} use Illuminate\Support\Facades\Schedule; // 毎週月曜13時に実行 Schedule::call(function () { // ... })->weekly()->mondays()->at('13:00'); // 平日の8時から17時まで毎時実行 Schedule::command('foo') ->weekdays() ->hourly() ->timezone('Asia/Tokyo') ->between('8:00', '17:00'); ``` ### タイムゾーンの設定 `timezone` メソッドで個別タスクのタイムゾーンを指定できます。 ```php theme={null} use Illuminate\Support\Facades\Schedule; Schedule::command('report:generate') ->timezone('Asia/Tokyo') ->at('9:00'); ``` すべてのタスクに共通のタイムゾーンを設定したい場合は、`config/app.php` の `schedule_timezone` を使います。 ```php theme={null} // config/app.php 'schedule_timezone' => 'Asia/Tokyo', ``` 夏時間(サマータイム)が適用されるタイムゾーンを使う場合、切り替えのタイミングでタスクが2回実行されるか、まったく実行されないことがあります。 可能な限り UTC を使うことを推奨します。 ## 条件制約 ### when / skip `when` はクロージャが `true` を返した場合のみタスクを実行します。 `skip` はその逆で、`true` を返した場合にスキップします。 ```php theme={null} use Illuminate\Support\Facades\Schedule; // 条件が true のときだけ実行 Schedule::command('emails:send')->daily()->when(function () { return true; }); // 条件が true のときはスキップ Schedule::command('emails:send')->daily()->skip(function () { return true; }); ``` ### environments `environments` メソッドで特定の環境でのみ実行するよう制限できます。 ```php theme={null} Schedule::command('emails:send') ->daily() ->environments(['staging', 'production']); ``` ### 時刻の制約 `between` / `unlessBetween` で実行時間帯を制限できます。 ```php theme={null} // 7時から22時の間だけ実行 Schedule::command('emails:send') ->hourly() ->between('7:00', '22:00'); // 23時から4時の間は実行しない Schedule::command('emails:send') ->hourly() ->unlessBetween('23:00', '4:00'); ``` ### 曜日の制約 | メソッド | 説明 | | - | - | | `->weekdays()` | 平日のみ | | `->weekends()` | 週末のみ | | `->mondays()` 〜 `->sundays()` | 特定の曜日のみ | | `->days([0, 3])` | 日曜(0)と水曜(3)など複数指定 | ```php theme={null} use Illuminate\Support\Facades; use Illuminate\Console\Scheduling\Schedule; Facades\Schedule::command('emails:send') ->hourly() ->days([Schedule::SUNDAY, Schedule::WEDNESDAY]); ``` ## 重複防止 デフォルトでは、前回のタスクがまだ実行中でも次の実行が開始されます。 `withoutOverlapping` を使うと、前の実行が終わるまで次の実行を待機させられます。 ```php theme={null} use Illuminate\Support\Facades\Schedule; Schedule::command('emails:send')->withoutOverlapping(); ``` ロックの有効期限(分)を指定することもできます。デフォルトは24時間です。 ```php theme={null} // 10分後にロックが失効する Schedule::command('emails:send')->withoutOverlapping(10); ``` `withoutOverlapping` はアプリケーションのキャッシュを使ってロックを管理します。 タスクが予期せぬ問題でスタックした場合は `schedule:clear-cache` でロックを解除できます。 ```shell theme={null} php artisan schedule:clear-cache ``` ## 複数サーバーでの実行制御 複数サーバーでスケジューラが動いている場合、`onOneServer` を使うことで1台のサーバーだけでタスクを実行できます。 ```php theme={null} use Illuminate\Support\Facades\Schedule; Schedule::command('report:generate') ->fridays() ->at('17:00') ->onOneServer(); ``` この機能を使うには、アプリケーションのデフォルトキャッシュドライバを `database`、`memcached`、`dynamodb`、または `redis` に設定し、すべてのサーバーが同じキャッシュサーバーに接続している必要があります。 ### onOneServer() による分散実行制御フロー ```mermaid theme={null} flowchart TD A["複数サーバーで
    artisan schedule:run が同時実行"] --> B["サーバー 1"] A --> C["サーバー 2"] A --> D["サーバー 3"] B --> E["共有キャッシュサーバーで
    アトミックロックを取得試行"] C --> E D --> E E -->|"取得成功(最初の 1 台)"| F["タスクを実行"] E -->|"取得失敗(他のサーバー)"| G["タスクをスキップ"] F --> H["実行完了後にロックを解放"] ``` ## タスクのグループ化 複数のタスクに同じ設定を適用する場合は `group` メソッドを使ってまとめられます。 ```php theme={null} use Illuminate\Support\Facades\Schedule; Schedule::daily() ->onOneServer() ->timezone('Asia/Tokyo') ->group(function () { Schedule::command('emails:send --force'); Schedule::command('emails:prune'); }); ``` ## バックグラウンド実行 同じ時刻にスケジュールされたタスクは、デフォルトでは定義順に順番に実行されます。 長時間かかるタスクがあると後続のタスクの開始が遅れます。 `runInBackground` を使うと、タスクをバックグラウンドで並列実行できます。 ```php theme={null} use Illuminate\Support\Facades\Schedule; Schedule::command('analytics:report') ->daily() ->runInBackground(); ``` `runInBackground` は `command` と `exec` メソッドでのみ使用できます。 ## メンテナンスモード アプリケーションがメンテナンスモードのとき、スケジュールタスクは実行されません。 メンテナンスモード中でも強制的に実行させたい場合は `evenInMaintenanceMode` を使います。 ```php theme={null} Schedule::command('emails:send')->evenInMaintenanceMode(); ``` ### メンテナンスモードの対応フロー ```mermaid theme={null} flowchart TD A["artisan schedule:run"] --> B{"アプリがメンテナンス
    モードか?
    (artisan down)"} B -->|"通常モード"| C["タスクを通常実行"] B -->|"メンテナンスモード"| D{"タスクに
    evenInMaintenanceMode()
    が設定されているか?"} D -->|"はい"| C D -->|"いいえ"| E["タスクをスキップ"] ``` ## スケジューラの一時停止 コードを変更せずにスケジューラを一時停止できます。 ```shell theme={null} # スケジューラを停止 php artisan schedule:pause # スケジューラを再開 php artisan schedule:continue ``` 停止中でも特定のタスクだけ実行し続けたい場合は `evenWhenPaused` を使います。 ヘルスチェックやシステム監視など、メンテナンス中でも継続が必要なタスクに役立ちます。 ```php theme={null} Schedule::command('emails:send')->evenWhenPaused(); ``` ## 出力のハンドリング ### ファイルへの出力 `sendOutputTo` でタスクの出力をファイルに保存できます。 ```php theme={null} use Illuminate\Support\Facades\Schedule; Schedule::command('emails:send') ->daily() ->sendOutputTo(storage_path('logs/emails-send.log')); ``` `appendOutputTo` を使うと、既存のファイルに追記します。 ```php theme={null} Schedule::command('emails:send') ->daily() ->appendOutputTo(storage_path('logs/emails-send.log')); ``` ### メールへの出力 `emailOutputTo` でタスクの出力をメールで送信できます。 事前にLaravelの[メール設定](/jp/mail)が必要です。 ```php theme={null} Schedule::command('report:generate') ->daily() ->sendOutputTo($filePath) ->emailOutputTo('admin@example.com'); ``` 失敗時のみメールを送る場合は `emailOutputOnFailure` を使います。 ```php theme={null} Schedule::command('report:generate') ->daily() ->emailOutputOnFailure('admin@example.com'); ``` ## タスクフック `before` / `after` メソッドで、タスクの実行前後に処理を挿入できます。 ```php theme={null} use Illuminate\Support\Facades\Schedule; Schedule::command('emails:send') ->daily() ->before(function () { // タスク実行前の処理 }) ->after(function () { // タスク実行後の処理 }); ``` 成功・失敗時のフックは `onSuccess` / `onFailure` で定義します。 ```php theme={null} use Illuminate\Support\Stringable; Schedule::command('emails:send') ->daily() ->onSuccess(function (Stringable $output) { // 成功時の処理 }) ->onFailure(function (Stringable $output) { // 失敗時の処理 }); ``` ## サーバーへのデプロイ サーバーの crontab に以下の1行を追加するだけで、Laravelのスケジューラが毎分実行されます。 ```shell theme={null} * * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1 ``` `crontab -e` コマンドで編集できます。 定義済みタスクの一覧と次回実行時刻を確認します。 ```shell theme={null} php artisan schedule:list ``` [Laravel Cloud](https://cloud.laravel.com) を使うと、cron の設定なしにスケジュールタスクを管理できます。 ### ローカル開発での実行 ローカル環境では cron を使わず、`schedule:work` コマンドでスケジューラを常時起動できます。 ```shell theme={null} php artisan schedule:work ``` このコマンドはフォアグラウンドで動作し、毎分スケジューラを呼び出します。`Ctrl+C` で停止するまで動き続けます。 ### サブ分スケジュール(1分未満の頻度) 通常の cron は1分が最小単位ですが、Laravelでは1秒単位のスケジュールも設定できます。 ```php theme={null} use Illuminate\Support\Facades\Schedule; Schedule::call(function () { DB::table('recent_users')->delete(); })->everySecond(); ``` サブ分タスクが定義されている場合、`schedule:run` はその分が終わるまで動き続けて、すべてのサブ分タスクを処理します。 サブ分タスクはキュージョブやバックグラウンドコマンドに処理を委譲することを推奨します。 タスク自体が長時間かかると、後続のサブ分タスクの実行が遅れるためです。 デプロイ中に実行中の `schedule:run` を中断するには、デプロイスクリプトに以下を追加します。 ```shell theme={null} php artisan schedule:interrupt ``` ## よく使うコマンドまとめ ```shell theme={null} # タスク一覧を表示 php artisan schedule:list # スケジューラを手動実行(サーバーのcronが呼び出すコマンド) php artisan schedule:run # ローカル開発用 常時起動 php artisan schedule:work # スケジューラを一時停止 php artisan schedule:pause # スケジューラを再開 php artisan schedule:continue # 重複防止ロックをクリア php artisan schedule:clear-cache # サブ分タスクの実行中断(デプロイ時) php artisan schedule:interrupt ``` # 署名付きURLの入力を正しく扱う Source: https://kawax.biz/jp/signed-urls Laravel 13の署名付きURLで保護される範囲と、署名済みクエリ値をquery()で取得する理由を、期限付きリンクの実装例で解説します。 ## このページの目的 期限付きの共有リンクで、発行時に決めた条件をそのまま使いたい場合は、URLの署名検証に加えて**値をどこから取得するか**を揃えます。 このページでは、レポートの種類と出力形式をURLに含め、プレビュー用のJSONを返す例で説明します。 対象はLaravel 13です。URL生成全般は[URL生成](/jp/urls)、入力取得の基本は[HTTPリクエスト](/jp/requests)を参照してください。 署名はリクエスト全体を保護するものではありません。フォームやJSONの本文は署名対象外です。 署名済みクエリ値は、署名検証後に `$request->query('format')` のように取得してください。 `input()`、`all()`、動的プロパティでは、署名されていない本文の値を取得する可能性があります。GETリクエストでもこの区別は必要です。 ## 署名で保護される範囲 | 要素 | 扱い | | - | - | | URLのパス | 署名検証の対象 | | クエリ文字列 | 原則として署名検証の対象。検証から除外したパラメータを除く | | スキーム・ホスト | 通常は署名に含む。相対署名では含めない | | フォーム・JSONの本文 | 署名の対象外 | | 有効期限 | `temporarySignedRoute()` が追加し、検証時に期限切れを確認 | `input()` はクエリ文字列だけでなく本文も扱うAPIです。Laravelはこれらの入力を統合する際、本文の値をクエリ文字列より優先します。 一方、`query()` はクエリ文字列だけを取得します。 署名検証が成功したことと、その後に読み取った値が署名済みであることは別の確認事項です。 ```mermaid theme={null} flowchart TD A["期限付きURLでアクセス"] --> B{"signedミドルウェア
    署名・有効期限を検証"} B -->|無効| C["403レスポンス"] B -->|有効| D["query()で
    署名済みクエリ値を取得"] D --> E["値の形式・許可範囲を検証"] E --> F["プレビューを返す"] ``` ## 期限付きプレビューリンクを実装する ### 1. 検証するルートを定義する `routes/web.php` に、署名を必須とする名前付きルートを定義します。 ここではDBやファイルを操作せず、受け取った条件だけを返します。 ```php theme={null} use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Validator; Route::get('/reports/preview', function (Request $request) { $options = Validator::make([ 'report' => $request->query('report'), 'format' => $request->query('format'), ], [ 'report' => ['required', 'string', 'in:monthly,annual'], 'format' => ['required', 'string', 'in:csv,pdf'], ])->validate(); return response()->json($options); })->middleware('signed')->name('reports.preview'); ``` * `signed` ミドルウェアが、クロージャの実行前に署名と有効期限を検証します。無効なら `403` を返します。 * `query()` で、処理に使う値をクエリ文字列から明示的に取得します。 * バリデーションは取得した配列に対して行います。署名検証は値の型や業務上の許可範囲を保証するものではありません。 この例で `Validator::make()` に渡しているのは、`query()` で取り出した値だけです。 署名済みクエリの検証を目的とする箇所で `$request->all()` に置き換えると、本文を含む入力を扱うことになります。 ### 2. 信頼できる発行処理でURLを生成する 名前付きルートの登録後、リンクを発行する処理で次のコードを使います。 `report` と `format` はパスのプレースホルダーではないため、クエリ文字列に追加されます。 ```php theme={null} use Illuminate\Support\Facades\URL; $url = URL::temporarySignedRoute( 'reports.preview', now()->addMinutes(30), ['report' => 'monthly', 'format' => 'csv'], ); ``` このURLには、指定した条件に加えて `expires` と `signature` が含まれます。 `expires` と `signature` はLaravelが使用する予約済みパラメータなので、業務用のパラメータ名として渡さないでください。 実際のアプリケーションで発行処理を公開する場合は、任意の条件で誰でもリンクを発行できる状態にせず、必要な[認証](/jp/authentication)や[認可](/jp/authorization)を行ってください。 ### 3. 通常の利用を確認する 発行したURLをそのまま開くと、有効期限内であれば次のJSONが返ります。 ```json theme={null} { "report": "monthly", "format": "csv" } ``` リンクの有効期限が切れた場合は、ルートの `signed` ミドルウェアが `403` を返します。 署名付きURLは使い捨てトークンではありません。この例では、期限内の同じURLを再利用できます。 一度だけ利用させたい場合は、署名とは別に使用済み状態を管理する必要があります。 ## 検証から除外する値を分ける フロントエンドが追加するページ番号などを許可したい場合、`hasValidSignatureWhileIgnoring(['page'])` で署名検証から除外できます。 ただし、除外した値は誰でも変更できるため、署名済み条件として扱わないでください。 たとえば、表示用の `page` と、発行時に固定する `report`、`format` は役割が異なります。 対象データ、受信者、料金などの判断に使う値を、利便性だけを理由に除外しないようにします。 上の実装例は通常の `signed` ミドルウェアを使っているため、`page` も除外していません。 クエリパラメータを追加して使いたい場合は、生成側と検証側で何を署名対象にするかを先に決めてください。 ## 相対署名を使う場合 ドメインを署名に含めない設計では、生成時の `absolute: false` と検証時の `signed:relative` を組み合わせます。 ```php theme={null} $url = URL::temporarySignedRoute( 'reports.preview', now()->addMinutes(30), ['report' => 'monthly', 'format' => 'csv'], absolute: false, ); ``` この場合は、先ほどのルートの `middleware('signed')` を `middleware('signed:relative')` に変更します。 相対署名でも本文は署名されないので、`query()` で取得する方針は変わりません。 ## 実装時のチェックリスト * 受信ルートで署名検証を必須にしている。 * 署名済みクエリ値を `query()` から取得している。 * 値の形式や許可範囲を別途検証している。 * 署名検証から除外した値を、発行時に固定された条件として使っていない。 * 絶対署名・相対署名の設定を生成側と検証側で揃えている。 * URLを知っていることと、ログインユーザーの権限を同一視していない。 ## 関連ページと一次情報 * [URL生成](/jp/urls):署名付きURLのAPIと無効な署名へのレスポンス * [HTTPリクエスト](/jp/requests):入力値とクエリ文字列の取得 * [バリデーション](/jp/validation):入力形式と許可する値の検証 * [Laravel公式ドキュメント:Signed URLs](https://github.com/laravel/docs/blob/13.x/urls.md#signed-urls) * [Laravel公式ドキュメント:Retrieving input from the query string](https://github.com/laravel/docs/blob/13.x/requests.md#retrieving-input-from-the-query-string) * [Laravel Framework:UrlGenerator](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Routing/UrlGenerator.php) * [Laravel Framework:ValidateSignature](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Routing/Middleware/ValidateSignature.php) # Laravel Telescope Source: https://kawax.biz/jp/telescope Laravel Telescopeの導入を解説します。ローカル専用インストール、ウォッチャー構成までカバーします。 ## Laravel Telescopeとは [Laravel Telescope](https://github.com/laravel/telescope) は、Laravelアプリケーションの内部状態を可視化する公式デバッグツールです。 リクエスト、例外、クエリ、ジョブ、ログ、メール、通知、キャッシュ操作、スケジュール実行などを詳細に記録できます。 **ローカル開発環境専用**のデバッグツールです。本番環境の監視には [Laravel Pulse](/jp/pulse) や [Laravel Nightwatch](/jp/blog/nightwatch-introduction) を使ってください。 ### モニタリング対象レイヤー ```mermaid theme={null} flowchart LR A["HTTPレイヤー
    Request / Response"] --> T["Telescope エントリ保存"] B["アプリケーションレイヤー
    Events / Logs / Exceptions"] --> T C["データレイヤー
    DB Queries / Redis / Cache"] --> T D["非同期レイヤー
    Queue Jobs / Schedule / Notifications"] --> T T --> E["ダッシュボード
    /telescope"] ``` *** ## インストール ```shell theme={null} composer require laravel/telescope ``` ```shell theme={null} php artisan telescope:install ``` ```shell theme={null} php artisan migrate ``` インストール後、`/telescope` でダッシュボードにアクセスできます。 ## ローカル専用インストール(推奨) ローカル開発でのデバッグが主目的なら、`--dev` でインストールし、サービスプロバイダーを `local` 環境だけで手動登録する構成を推奨します。 ```shell theme={null} composer require laravel/telescope --dev php artisan telescope:install php artisan migrate ``` `telescope:install` 実行後、`bootstrap/providers.php` から `TelescopeServiceProvider` の登録を削除してください。 そのうえで、`App\Providers\AppServiceProvider` の `register` メソッドで手動登録します。 ```php theme={null} public function register(): void { if ($this->app->environment('local') && class_exists(\Laravel\Telescope\TelescopeServiceProvider::class)) { $this->app->register(\Laravel\Telescope\TelescopeServiceProvider::class); $this->app->register(TelescopeServiceProvider::class); } } ``` さらに `composer.json` で auto-discovery を無効化してください。 ```json theme={null} { "extra": { "laravel": { "dont-discover": [ "laravel/telescope" ] } } } ``` *** ## 利用可能なウォッチャー ウォッチャーはリクエストやコマンド実行時のテレメトリを収集します。 `config/telescope.php` で有効化・設定を管理します。 リクエスト、ヘッダー、セッション、レスポンスを記録します。\ `size_limit` でレスポンス記録サイズを制限できます。 SQL、バインディング、実行時間を記録します。\ デフォルトでは **100ms** 超のクエリが `slow` タグで記録されるため、性能ボトルネック検知に非常に有効です。 ```php theme={null} Watchers\QueryWatcher::class => [ 'enabled' => env('TELESCOPE_QUERY_WATCHER', true), 'slow' => 100, ], ``` キュージョブの投入と実行結果を記録します。 report対象の例外とスタックトレースを記録します。 アプリケーションログを記録します。\ デフォルトは `error` 以上で、設定で `debug` まで下げられます。 Artisanコマンドの引数、オプション、出力、終了コードを記録します。 dispatchされたイベントのペイロードとリスナー情報を記録します(Laravel内部イベントは除外)。 キャッシュのヒット、ミス、更新、削除を記録します。 アプリケーションから実行されたRedisコマンドを記録します。 Eloquentモデルイベントを記録し、必要に応じてhydration件数も収集できます。 送信された通知を記録します。 送信メールをブラウザでプレビューし、`.eml` としてダウンロードできます。 外向きHTTPクライアントリクエストを記録します。 Gate / Policy の認可チェック結果を記録します。 スケジュール実行されたコマンドと出力を記録します。 レンダリングされたビュー名、パス、データ、composerを記録します。 キューバッチ情報(ジョブやコネクション情報)を記録します。 TelescopeのDumpタブが開いている間の `dump` 出力を記録します。 *** ## まとめ | タスク | 推奨設定 | | - | - | | 導入 | `composer require laravel/telescope --dev` | | ローカル限定の確実な分離 | 手動プロバイダー登録 + `dont-discover` | | DB性能調査 | Query Watcher の slowクエリタグ(デフォルト100ms)を活用 | ## 次のステップ 本番環境のパフォーマンス集計ダッシュボードを導入します。 本番環境のリアルタイム監視にはNightwatchを使います。 # Viteによるアセットバンドル Source: https://kawax.biz/jp/vite LaravelプロジェクトでViteを使ってJavaScript・CSSをバンドルし、ホットリロードで快適な開発環境を構築する方法を解説します。 ## LaravelとViteとは [Vite](https://vitejs.dev) は高速なフロントエンドビルドツールです。開発中は即座にファイルの変更を反映するホットモジュールリプレースメント(HMR)を提供し、本番ビルドでは最適化されたアセットを生成します。 Laravel 9以降、Viteが標準のフロントエンドビルドツールとして採用されており、`laravel-vite-plugin` を通じてLaravelと連携します。このプラグインが担う主な役割は次のとおりです。 * エントリーポイントの管理 * 開発サーバーのHMRサポート * `@vite()` Bladeディレクティブとの連携 * 本番ビルド時のアセットのバージョニング(キャッシュバスティング) Laravelのスターターキット(Laravel Breezeなど)を使っている場合、ViteとTailwindの設定はすでに含まれています。このページではゼロから設定する方法を説明します。 ViteがLaravel全体のフロントエンド構成の中でどういう位置付けかは、[フロントエンド](/jp/frontend) で先に全体像を確認できます。 ## インストールと設定 ### Nodeのインストール確認 Viteを使うには Node.js(16以上)とnpmが必要です。インストール済みかどうか確認します。 ```shell theme={null} node -v npm -v ``` ### パッケージのインストール 新規インストールしたLaravelプロジェクトには、すでに `package.json` に `vite` と `laravel-vite-plugin` が含まれています。次のコマンドで依存パッケージをインストールします。 ```shell theme={null} npm install ``` ### vite.config.js の設定 プロジェクトルートに `vite.config.js` が生成されています。エントリーポイント(バンドルの起点となるファイル)を指定します。 ```js theme={null} import { defineConfig } from 'vite'; import laravel from 'laravel-vite-plugin'; export default defineConfig({ plugins: [ laravel([ 'resources/css/app.css', 'resources/js/app.js', ]), ], }); ``` SPAやInertiaを使う場合は、CSSをJavaScriptからインポートする形式が推奨されます。その場合はエントリーポイントから `resources/css/app.css` を除き、`resources/js/app.js` の先頭に `import '../css/app.css';` を追加します。 ## 開発サーバーの起動 開発中は `npm run dev` でViteの開発サーバーを起動します。ファイルを変更すると、ブラウザが自動的に更新されます(HMR)。 ```shell theme={null} npm run dev ``` Laravelの開発サーバーと同時に起動することもできます。 ```shell theme={null} composer run dev ``` `composer run dev` は `php artisan serve` と `npm run dev` を同時に起動します。 ### Bladeビューの自動リロード `refresh: true` を設定すると、Bladeビューやルートファイルを保存したときにブラウザが自動リロードされます。 ```js theme={null} import { defineConfig } from 'vite'; import laravel from 'laravel-vite-plugin'; export default defineConfig({ plugins: [ laravel({ input: [ 'resources/css/app.css', 'resources/js/app.js', ], refresh: true, }), ], }); ``` `refresh: true` のとき、次のディレクトリへの変更が監視対象になります。 * `resources/views/**` * `app/Livewire/**` * `routes/**` * `lang/**` ## 本番ビルド 本番環境へデプロイする前に `npm run build` を実行します。アセットがバンドル・バージョニングされ、`public/build/` に出力されます。 ```shell theme={null} npm run build ``` ビルド後のディレクトリ構造の例: ```text theme={null} public/ build/ assets/ app-Cv82Mlke.css app-DnAZBF4U.js manifest.json ``` `manifest.json` にはファイル名とハッシュのマッピングが記録されており、`@vite()` ディレクティブがこれを参照して正しいファイルを読み込みます。 `public/build/` ディレクトリはビルド成果物なので `.gitignore` に追加することを推奨します。デプロイ先でビルドを実行するか、CIでビルドしてデプロイします。 ## Bladeからのアセット読み込み `@vite()` Bladeディレクティブをレイアウトの `` に追加します。 ```blade theme={null} {{ config('app.name') }} @vite(['resources/css/app.css', 'resources/js/app.js']) @yield('content') ``` CSSをJavaScriptからインポートしている場合は、JavaScriptのエントリーポイントだけ指定します。 ```blade theme={null} @vite('resources/js/app.js') ``` `@vite()` ディレクティブは開発中と本番で自動的に動作を切り替えます。 | 状況 | 動作 | | - | - | | 開発サーバー起動中 | Vite開発サーバーからアセットを読み込み、HMRを有効化 | | 本番(ビルド済み) | `public/build/manifest.json` を参照してバージョン付きファイルを読み込む | ## JavaScript・CSSのエントリーポイント設定 ### JavaScriptの設定 `resources/js/app.js` がメインのエントリーポイントです。ここから他のモジュールをインポートします。 ```js theme={null} import './bootstrap'; import '../css/app.css'; // 自作モジュールのインポート import './components/modal'; import './utils/format'; ``` ### CSSの設定 `resources/css/app.css` にグローバルなスタイルを記述します。 ```css theme={null} /* Tailwind CSSを使う場合 */ @import 'tailwindcss'; /* カスタムスタイル */ body { font-family: 'Noto Sans JP', sans-serif; } ``` ### 複数のエントリーポイント 管理画面など、ページごとに異なるアセットをバンドルしたい場合は、複数のエントリーポイントを指定します。 ```js theme={null} import { defineConfig } from 'vite'; import laravel from 'laravel-vite-plugin'; export default defineConfig({ plugins: [ laravel([ 'resources/css/app.css', 'resources/js/app.js', 'resources/css/admin.css', 'resources/js/admin.js', ]), ], }); ``` Bladeでは対応するエントリーポイントのみ読み込みます。 ```blade theme={null} {{-- 管理画面レイアウト --}} @vite(['resources/css/admin.css', 'resources/js/admin.js']) ``` ## Tailwind CSSとの連携 Tailwind CSS v4以降はViteプラグインとして統合されます。 ```shell theme={null} npm install tailwindcss @tailwindcss/vite ``` ```js theme={null} import { defineConfig } from 'vite'; import laravel from 'laravel-vite-plugin'; import tailwindcss from '@tailwindcss/vite'; export default defineConfig({ plugins: [ tailwindcss(), laravel([ 'resources/css/app.css', 'resources/js/app.js', ]), ], }); ``` ```css theme={null} /* resources/css/app.css */ @import 'tailwindcss'; ``` Tailwind CSS v3を使う場合は `tailwind.config.js` と `postcss.config.js` が必要です。最新のTailwind CSS v4ではこれらのファイルは不要です。 ## エイリアス設定 `laravel-vite-plugin` は `@` エイリアスを自動で設定します。これは `resources/js` ディレクトリを指します。 ```js theme={null} // エイリアスを使ったインポート import UserCard from '@/components/UserCard.vue'; // 上記は次と同じ import UserCard from '/resources/js/components/UserCard.vue'; ``` カスタムエイリアスを追加したい場合は `resolve.alias` で設定します。 ```js theme={null} import { defineConfig } from 'vite'; import laravel from 'laravel-vite-plugin'; import path from 'path'; export default defineConfig({ plugins: [ laravel(['resources/js/app.js']), ], resolve: { alias: { '@': '/resources/js', '@css': '/resources/css', '@images': '/resources/images', }, }, }); ``` エイリアスを使ったインポート例: ```js theme={null} import logo from '@images/logo.png'; import '@css/custom.css'; ``` ## フレームワーク別の設定 ### Vue ```shell theme={null} npm install --save-dev @vitejs/plugin-vue ``` ```js theme={null} import { defineConfig } from 'vite'; import laravel from 'laravel-vite-plugin'; import vue from '@vitejs/plugin-vue'; export default defineConfig({ plugins: [ laravel(['resources/js/app.js']), vue({ template: { transformAssetUrls: { base: null, includeAbsolute: false, }, }, }), ], }); ``` ### React ```shell theme={null} npm install --save-dev @vitejs/plugin-react ``` ```js theme={null} import { defineConfig } from 'vite'; import laravel from 'laravel-vite-plugin'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [ laravel(['resources/js/app.jsx']), react(), ], }); ``` Reactを使う場合、Bladeテンプレートに `@viteReactRefresh` ディレクティブを `@vite` より前に追加します。 ```blade theme={null} @viteReactRefresh @vite('resources/js/app.jsx') ``` ## 静的アセットの処理 Bladeテンプレートから参照する画像やフォントもViteでバージョニングできます。`assets` オプションで対象ディレクトリを指定します。 ```js theme={null} laravel({ input: 'resources/js/app.js', assets: ['resources/images/**', 'resources/fonts/**'], }) ``` Bladeテンプレートでは `Vite::asset()` メソッドでバージョン付きURLを取得します。 ```blade theme={null} ロゴ ``` ## まとめ | やりたいこと | 方法 | | - | - | | 開発サーバーを起動する | `npm run dev` | | 本番用アセットをビルドする | `npm run build` | | Bladeでアセットを読み込む | `@vite(['resources/css/app.css', 'resources/js/app.js'])` | | Bladeビュー変更で自動リロード | `refresh: true` を `vite.config.js` に設定 | | Tailwind CSSを使う | `@tailwindcss/vite` プラグインを追加 | | `@` エイリアスを使う | デフォルトで `resources/js` を指す | | 静的アセットをバージョニングする | `assets` オプションと `Vite::asset()` を使う | ## 次のステップ Laravelのキャッシュ機能でアプリケーションのパフォーマンスをさらに向上させる方法を学びます。 # Laravel Agent Detector — AIエージェント検出パッケージ Source: https://kawax.biz/jp/blog/agent-detector-introduction laravel/agent-detectorの使い方。Claude、Copilot、Cursorなど主要AIエージェントの実行環境を検出するLaravel公式パッケージの解説。 ## はじめに [laravel/agent-detector](https://github.com/laravel/agent-detector) は、PHPコードがAIエージェントや自動化開発環境の中で実行されているかを検出するための軽量ユーティリティです。2026年4月にLaravel公式パッケージとして公開されました。 AIエージェントが開発の現場に深く入り込んだ今、コード自体がどの環境で動作しているかを把握することが重要になっています。AIエージェントからのリクエストか人間からのリクエストかによってログの詳細度やレート制限を変えたり、エージェント専用のレスポンスを返したりといったケースが現実の開発で生まれています。 ```mermaid theme={null} graph TD A["AgentDetector::detect()"] --> B{"AI_AGENT
    環境変数あり?"} B -->|Yes| C["カスタムまたは既知エージェント
    として返す"] B -->|No| D{"既知の環境変数
    あり?"} D -->|Yes| E["対応する KnownAgent
    として返す"] D -->|No| F{"/opt/.devin
    ファイルあり?"} F -->|Yes| G["KnownAgent::Devin
    として返す"] F -->|No| H["isAgent: false
    (エージェントなし)"] ``` ## インストール ```bash theme={null} composer require laravel/agent-detector ``` PHP 8.2 以上が必要です。 ## 基本的な使い方 `AgentDetector::detect()` を呼び出すと `AgentResult` オブジェクトが返ります。 ```php theme={null} use Laravel\AgentDetector\AgentDetector; use Laravel\AgentDetector\KnownAgent; $result = AgentDetector::detect(); if ($result->isAgent) { echo "Running inside: {$result->name}"; } // 特定のエージェントを確認する if ($result->knownAgent() === KnownAgent::Claude) { echo "Hello from Claude!"; } ``` スタンドアロン関数を使う書き方もあります。 ```php theme={null} use function Laravel\AgentDetector\detectAgent; $result = detectAgent(); ``` ### AgentResult のプロパティ | プロパティ / メソッド | 型 | 説明 | | - | - | - | | `$result->isAgent` | `bool` | AIエージェントの中で実行されている場合 `true` | | `$result->name` | `?string` | エージェント名(例: `"claude"`)。エージェントでない場合 `null` | | `$result->knownAgent()` | `?KnownAgent` | `KnownAgent` enumを返す。未知のエージェントは `null` | ## 対応エージェント一覧 | エージェント | 検出方法(環境変数 / ファイル) | | - | - | | Custom | `AI_AGENT` 環境変数(任意の値) | | GitHub Copilot | `AI_AGENT=github-copilot`、`AI_AGENT=github-copilot-cli`、`COPILOT_MODEL`、`COPILOT_ALLOW_ALL`、`COPILOT_GITHUB_TOKEN`、`COPILOT_CLI` | | Cursor | `CURSOR_AGENT` | | Claude | `CLAUDECODE` または `CLAUDE_CODE` | | Cowork | `CLAUDE_CODE_IS_COWORK`(`CLAUDECODE` または `CLAUDE_CODE` と同時に設定) | | Gemini | `GEMINI_CLI` | | Codex | `CODEX_SANDBOX`、`CODEX_CI`、`CODEX_THREAD_ID` | | Augment CLI | `AUGMENT_AGENT` | | AMP | `AMP_CURRENT_THREAD_ID` | | Opencode | `OPENCODE_CLIENT` または `OPENCODE` | | Replit | `REPL_ID` | | Devin | `/opt/.devin` ファイルが存在する | | Antigravity | `ANTIGRAVITY_AGENT` | | Pi | `PI_CODING_AGENT` | | Kiro CLI | `KIRO_AGENT_PATH` | | v0 | `AI_AGENT=v0` | 検出の優先順位は `AI_AGENT` 環境変数 → 既知の環境変数 → ファイルシステムの順です。 ## カスタムエージェントの設定 `AI_AGENT` 環境変数に任意の値を設定すると、カスタムエージェントとして検出できます。 ```bash theme={null} AI_AGENT=my-custom-agent php your-script.php ``` ```php theme={null} $result = AgentDetector::detect(); // $result->isAgent === true // $result->name === 'my-custom-agent' // $result->knownAgent() === null(未知のエージェント) ``` ## 実用的なユースケース ### ミドルウェアでAIエージェントを検出する Laravelのミドルウェアで実行環境を判定し、エージェント向けの処理を分岐させます。 ```php theme={null} isAgent) { // エージェント名をリクエスト属性にセット $request->attributes->set('ai_agent', $agent->name); } return $next($request); } } ``` ### AIエージェント環境では詳細なログを出力する ```php theme={null} use Laravel\AgentDetector\AgentDetector; use Illuminate\Support\Facades\Log; $agent = AgentDetector::detect(); if ($agent->isAgent) { Log::withContext(['ai_agent' => $agent->name]) ->debug('Agent request received', $request->all()); } else { Log::info('User request received'); } ``` ### AIエージェントからのリクエストにレート制限を適用する `ThrottleRequests` ミドルウェアのキーをエージェント検出に基づいて変えることで、エージェントと人間で異なるレート制限を設けられます。 ```php theme={null} isAgent ? 10 : 60; return app(ThrottleRequests::class)->handle( $request, $next, $maxAttempts, ); } } ``` ### コンソールコマンドで動作を切り替える Artisanコマンドをエージェントが実行する際に詳細な進捗を出力する例です。 ```php theme={null} use Laravel\AgentDetector\AgentDetector; public function handle(): void { $agent = AgentDetector::detect(); if ($agent->isAgent) { $this->info("Running in {$agent->name} environment — verbose mode enabled."); } // 処理... } ``` ## まとめ `laravel/agent-detector` は、シンプルな環境変数チェックとファイルシステム検査をまとめたパッケージです。複雑な設定なしで `AgentDetector::detect()` を呼び出すだけで使えます。 AIエージェントが開発フローの一部として定着しつつある今、「コードが誰(何)によって動かされているか」を把握することはますます重要になっていきます。ログ、レート制限、レスポンスのカスタマイズなど、様々な場面でこのパッケージが活用できるでしょう。 ソースコードと最新の対応エージェント一覧はこちら。 # laravel/agent-skills — Laravel公式AIエージェントスキル集 Source: https://kawax.biz/jp/blog/agent-skills-introduction Laravel公式が管理するAIエージェントスキルリポジトリ「laravel/agent-skills」を紹介します。Claude Code・Cursor向けのプラグインとして提供されるスキルの概要、インストール方法、各スキルの詳細を解説します。 `laravel/agent-skills` はアクティブに開発中のリポジトリです(2026年4月時点)。現在のプラグインバージョンは `1.0.0` ですが、今後の内容変更・追加が予想されます。 ## laravel/agent-skillsとは [laravel/agent-skills](https://github.com/laravel/agent-skills) は、Laravel公式が管理するAIエージェント向けスキル・プラグインのコレクションリポジトリです。Taylor Otwell 本人が作者として登録されており、Laravel公式のmarketplaceとして整備されていく予定です。 もともとはClaude Code向けのスキルを置くリポジトリとして始まりましたが、リポジトリ名を `agent-skills` にリニューアルし、**Claude Code** と **Cursor** の両方に対応したプラグインを提供するようになりました。 AIを活用してLaravelアプリを開発する場面で、エージェントがLaravelのベストプラクティスを理解し、Laravel CloudやNightwatchなどの公式サービスも自律的に操作できるようにするためのスキルセットです。 ```mermaid theme={null} graph TD A["laravel/agent-skills
    (公式スキルリポジトリ)"] --> B["laravel-simplifier
    Laravelコード改善エージェント"] A --> C["laravel-cloud
    Laravel Cloudデプロイスキル"] A --> D["laravel-nightwatch
    Nightwatch設定スキル"] B --> E["Claude Code
    .claude-plugin"] B --> F["Cursor
    .cursor-plugin"] C --> E C --> F D --> E D --> F D --> G["Nightwatch
    MCPサーバー"] ``` ## リポジトリの構造 リポジトリはスキル(プラグイン)ごとにディレクトリが分かれており、各ディレクトリに Claude Code 用と Cursor 用の設定ファイルが含まれています。 ``` laravel/agent-skills/ ├── laravel/ # Laravel コード改善エージェント │ ├── .claude-plugin/ │ │ └── plugin.json # Claude Codeプラグイン定義 │ ├── .cursor-plugin/ │ │ └── plugin.json # Cursorプラグイン定義 │ └── agents/ │ └── laravel-simplifier.md # エージェントのシステムプロンプト ├── laravel-cloud/ # Laravel Cloud デプロイスキル │ ├── .claude-plugin/ │ │ └── plugin.json │ ├── .cursor-plugin/ │ │ └── plugin.json │ └── skills/ │ └── deploying-laravel-cloud/ └── laravel-nightwatch/ # Laravel Nightwatch 設定スキル ├── .claude-plugin/ │ └── plugin.json ├── .cursor-plugin/ │ └── plugin.json ├── .mcp.json # Nightwatch MCPサーバー設定 └── skills/ └── configure-nightwatch/ ``` ## 提供されるプラグイン ### laravel — コード品質改善エージェント **laravel-simplifier** は、最近変更されたPHP/Laravelコードをレビューし、機能を変えずにコードの明瞭さ・一貫性・保守性を高めるエージェントです。リポジトリ内では `laravel/` ディレクトリに格納されており、インストール時は `laravel-simplifier@laravel` と指定します。 主な機能: * Laravelの規約とPSR-12標準の適用 * 不要な複雑さとネストの削減 * 変数名・関数名の可読性向上 * 関連ロジックの整理・統合 * `match` 式の活用による条件分岐の明確化 エージェントはデフォルトで「最近変更されたコード」だけを対象にします。コーディングセッションが終わった後に呼び出すと、自動的に変更箇所を特定してリファクタリングを提案してくれます。 ``` > Review recent changes using the laravel-simplifier agent ``` ### laravel-cloud — デプロイ・インフラ管理スキル **laravel-cloud** は、[Laravel Cloud](https://cloud.laravel.com) へのデプロイとリソース管理を支援するスキルです。`cloud` CLIのコマンドパターンを熟知しており、デプロイ手順をステップバイステップでガイドします。 主な機能: * 初回デプロイ・既存アプリのデプロイワークフローのガイド * データベース・キャッシュ・ドメイン・バケットなどのインフラリソース管理 * マルチステップ作業のオペレーションチェックリスト * Cloud CLIのCRUDコマンドパターンに準拠した操作 ``` > Deploy my app to Laravel Cloud > Set up a database and cache for the staging environment ``` ### laravel-nightwatch — 監視設定スキル **laravel-nightwatch** は、[Laravel Nightwatch](https://nightwatch.laravel.com) のデータ収集・サンプリングレート・フィルタリングルール・リダクションポリシーの設定を支援するスキルです。 さらに、**Nightwatch MCP サーバー**をバンドルしており、AIエージェントがNightwatchの問題を直接参照・操作できます。 | 機能 | 説明 | | - | - | | セットアップ・設定 | アプリへのNightwatchセットアップをガイド | | サンプリング・フィルタリング | ルール設定によるデータ量のコントロール | | PII リダクション | 機密データを保護するリダクションポリシー | | イベント最適化 | 本番環境向けのイベント収集最適化 | | MCP連携 | 問題の閲覧・スタックトレース確認・ステータス更新・コメント追加 | ``` > Configure Nightwatch sampling rates for production > Set up PII redaction for Nightwatch ``` ## インストール方法 ### Claude Code Claude Codeのマーケットプレイス機能を使ってインストールします。 ```bash theme={null} # マーケットプレイスへの追加 /plugin marketplace add laravel/agent-skills # 各プラグインのインストール /plugin install laravel-simplifier@laravel /plugin install laravel-cloud@laravel /plugin install laravel-nightwatch@laravel ``` 必要なプラグインだけをインストールすることもできます。すべてインストールする必要はありません。 ### Cursor Cursorの場合は、プラグインマーケットプレイスのパネルで **Laravel** を検索してインストールします。 Cursorのサイドバーまたは設定から「Plugins」パネルを開きます。 検索バーに「**Laravel**」と入力します。 `laravel`・`laravel-cloud`・`laravel-nightwatch` の中から必要なものをインストールします。 ## 使い方の例 ### laravel-simplifier でコードレビューを自動化する コーディングセッションの最後に呼び出すことで、書いたコードを自動的にLaravelのベストプラクティスに沿って洗練させます。 ``` > Review recent changes using the laravel-simplifier agent ``` エージェントは自律的に動作し、変更されたファイルを特定して改善案を提示します。明示的に指示しない限り、最近変更したコードのみが対象になります。 ### Laravel Cloudへのデプロイ 自然言語でデプロイ作業を指示できます。エージェントが `cloud` CLIのコマンドを適切に組み合わせて実行します。 ``` > Deploy my app to Laravel Cloud > Set up a database and cache for the staging environment > Add a custom domain to my production environment ``` ### Nightwatchで問題を調査する NightwatchのMCPサーバーが統合されているため、AIに直接問題の調査を依頼できます。 ``` > Show me the latest errors in Nightwatch > What's the stack trace for issue #42? > Mark issue #42 as resolved ``` ## プラグインの仕組み 各プラグインは `.claude-plugin/plugin.json` または `.cursor-plugin/plugin.json` で定義されています。エージェント定義(`.md` ファイル)にはシステムプロンプトが記述されており、AIがどのように動作すべきかが詳細に規定されています。 laravel-simplifier のプロンプトの一部: ``` You are an expert PHP/Laravel code simplification specialist focused on enhancing code clarity, consistency, and maintainability while preserving exact functionality... ``` スキルはコンテキストファイル(Markdownドキュメント)の形でAIに追加知識を与えます。例えば laravel-cloud のスキルには、Cloud CLIの操作手順や注意点が詳細に記述されており、AIがデプロイを誤って実行しないようにガイドします。 ## 今後の展望 `laravel/agent-skills` リポジトリは、**Laravelエコシステム全体をカバーするAIスキルの公式マーケットプレイス**として整備されていく予定です。 現在は Laravel・Laravel Cloud・Laravel Nightwatch の3つのプラグインが提供されていますが、今後は以下のような拡張が期待されます: * 他のLaravel公式パッケージ(Forge・Vapour・Cashier・Sanctumなど)への対応 * GitHub CopilotなどClaude Code・Cursor以外のAIツールへの対応 * コミュニティによるサードパーティスキルの追加(マーケットプレイス化) AI開発が標準的なワークフローになりつつある今、Laravelチームが公式にAIスキルを提供・管理することは、エコシステム全体の品質向上に大きく貢献するはずです。 スキルの詳細やインストール手順は公式リポジトリのREADMEを参照してください。 laravel-cloud スキルと連携するLaravel公式のホスティングサービスです。 laravel-nightwatch スキルと連携するLaravel公式のエラー監視サービスです。 # Laravel AIエージェントがMCPサーバーに対応 Source: https://kawax.biz/jp/blog/ai-sdk-mcp-client Laravel AI SDKとLaravel MCPに追加されたMCPクライアント機能を解説します。エージェントのtools()にMCPサーバーのツールをそのまま組み込めるようになりました。 ## 概要 2026年6月、[Laravel公式ブログ](https://laravel.com/blog/laravel-ai-agents-now-support-mcp-servers)で、Laravel AI SDKで構築したAIエージェントから **MCP(Model Context Protocol)サーバー**に接続できるようになったことが発表されました。 これまでLaravel MCPは「Laravelアプリを **MCPサーバーとして** 公開する」機能を提供していましたが、今回追加されたのは逆方向、つまり「Laravelアプリが **MCPクライアントとして** 他のMCPサーバーに接続する」機能です。 MCP自体の詳しい仕組みは[Laravel MCPのドキュメント](https://laravel.com/docs/mcp)を参照してください。このページではクライアント機能に絞って解説します。 ## なぜAI SDK本体ではなくLaravel MCPに実装されたか MCPはトランスポートのネゴシエーション、ハンドシェイク、認証フローなど扱う範囲が広いプロトコルです。これをAI SDK内に直接実装すると、エージェントなしでMCPサーバーと通信したい場面(キュージョブやコンソールコマンドなど)で再利用できません。 そこでLaravel チームは機能を2つに分けました。 ```mermaid theme={null} graph LR A["laravel/mcp
    MCPクライアント"] --> B["接続・認証・ハンドシェイク"] C["laravel/ai
    薄い統合層"] --> D["エージェントのtools()に
    MCPツールをそのまま追加"] A --> C ``` * **`laravel/mcp`**:接続・ネゴシエーション・認証・ツール呼び出しを担当するMCPクライアント本体 * **`laravel/ai`**:エージェントがそのクライアントを`tools()`から違和感なく使えるようにする薄い統合層 それぞれが単独でも使え、組み合わせるとエージェントからMCPサーバーのツールを手書きのツールと同じように扱えます。 ## MCPサーバーへの接続 ローカルプロセスとして起動するSTDIOサーバーと、HTTP経由のリモートサーバーの両方に対応しています。 ```php theme={null} use Laravel\Mcp\Client; // ローカルサーバー(STDIO) $client = Client::local('npx', ['-y', '@modelcontextprotocol/puppeteer']); $tools = $client->tools(); // リモートサーバー(Streamable HTTP) $client = Client::web('https://nightwatch.laravel.com/mcp'); $tools = $client->tools(); ``` 接続、ハンドシェイク、プロトコルバージョンのネゴシエーションはすべてクライアントが処理するため、アプリ側は`tools()`を呼ぶだけで済みます。 ## 認証 ### Bearerトークン ```php theme={null} $tools = Client::web('https://mcp.example.com') ->withToken($token) ->tools(); ``` ### OAuth Nightwatchなど多くのホスト型MCPサーバーはOAuthを要求します。サービスプロバイダーで名前付きクライアントを登録します。 ```php theme={null} use Laravel\Mcp\Client; use Laravel\Mcp\Facades\Mcp; Mcp::registerClient('nightwatch', fn () => Client::web('https://nightwatch.laravel.com/mcp')->withOAuth() ); ``` OAuthのルートとコールバック処理を配線します。 ```php theme={null} use Laravel\Mcp\Facades\Mcp; use Laravel\Mcp\Client\OAuth\TokenSet; Mcp::oAuthRoutesFor('nightwatch', function (string $provider, TokenSet $token) { auth()->user()->update([ 'mcp_nightwatch_token' => encrypt($token->accessToken), 'mcp_nightwatch_refresh' => encrypt($token->refreshToken), ]); return redirect('/dashboard'); }, middleware: 'auth'); ``` これで`mcp.oauth.nightwatch.connect`と対応するコールバックルートが生成されます。Blade側では接続ボタンを置くだけです。 ```blade theme={null} Connect Nightwatch ``` ユーザーがログインして承認すると、コールバックのクロージャがトークンを受け取ります。リダイレクトURLやPKCEの詳細を自分で扱う必要はありません。 ユーザーの介在がないバックグラウンド処理向けには、クライアントクレデンシャルグラントも用意されています。 ```php theme={null} $token = Mcp::client('billing')->oAuthClient()->clientCredentials(); ``` ## エージェントへの組み込み 一番のポイントは、エージェントの`tools()`メソッドを変更せずにMCPツールを混ぜられることです。 ```php theme={null} use Laravel\Mcp\Facades\Mcp; use App\Tools\SendSlackMessage; class SupportAgent extends Agent { public function instructions(): string { return 'You help triage production issues.'; } public function tools(): array { return [ ...Mcp::client('nightwatch') ->withToken(auth()->user()->mcp_nightwatch_token) ->tools(), new SendSlackMessage, ]; } } ``` Laravel AIは`tools()`配列の中身がMCPツールであることを検知し、エージェントのツール契約に合うようラップします。MCPの入力スキーマをLaravelのJSONスキーマに変換し、モデルがツールを呼び出したときにリモート呼び出しを行い、結果を正規化して返します。エラー、構造化データ、プレーンテキスト、ストリーミング更新のいずれも自動的に処理されるため、エージェント側のコードにMCPの詳細が漏れ出すことはありません。 複数のトランスポートを1つのエージェントに混在させることもできます。 ```php theme={null} public function tools(): array { return [ ...Mcp::client('nightwatch')->tools(), ...Client::local('npx', ['-y', '@modelcontextprotocol/server-puppeteer'])->tools(), new SendSlackMessage, ]; } ``` さらに、自分のLaravel MCPサーバー用に書いたツールクラスは、クライアント接続なしにそのままエージェントへ渡せます。同じツールを外部公開用とエージェント用の両方で使い回せるということです。 ```php theme={null} use App\Mcp\Tools\CurrentWeatherTool; public function tools(): array { return [ new CurrentWeatherTool, new SendSlackMessage, ]; } ``` ## ツール一覧のキャッシュ ツール一覧の取得にはサーバーへの往復が発生します。特にOAuth経由のリモートサーバーでは、プロンプトのたびに呼び出すのは無駄です。ツール一覧は頻繁に変わらないため、キャッシュ対象として適しています。 ```php theme={null} public function tools(): array { $tools = Cache::remember('mcp.nightwatch.tools', now()->addHour(), fn () => Mcp::client('nightwatch')->tools() ); return [...$tools, new SendSlackMessage]; } ``` MCPツールはプレーンなデータとして返るため、キャッシュから復元してもそのまま動作します。 ## テスト 実際に稼働しているMCPサーバーがなくても、Laravel AIのフェイク機能でエージェントをテストできます。MCPツール名は`mcp_tools_`という命名規則に従うため、`search`というツールは`mcp_tools_search`として現れます。 ```php theme={null} use Laravel\Ai\Responses\Data\ToolCall; SupportAgent::fake([ new ToolCall('call_1', 'mcp_tools_search', ['query' => 'laravel']), 'Found the issue.', ]); $response = (new SupportAgent)->prompt('Find the latest error'); expect($response->toolCalls)->toHaveCount(1); expect($response->toolResults->first()->result)->toContain('Found'); ``` エージェントの通常のループはそのまま動き、モデルの発言だけをフェイクが決定します。ツール呼び出し自体はMCPレイヤーを実際に通過するため、ネットワークなしで本番と同じ経路をテストできます。 ## 現時点でサポートされる範囲 この最初のリリースでは、STDIOとStreamable HTTPの両トランスポートで、ツールとプロンプトをサポートしています。認証はBearerトークンとOAuthに対応しています。MCP自体の進化に合わせて、今後も対応範囲が広がっていく見込みです。 ## 関連ページ 標準対応していないAIサービスに対応するカスタムプロバイダーの実装方法 この記事で例に使われているホスト型監視サービスNightwatchの解説 # Blaze パッケージの紹介 Source: https://kawax.biz/jp/blog/blaze-introduction Livewire公式の Blaze を使って Blade コンポーネント描画を高速化する方法を解説します。compile / memo / fold の選択基準、導入方法、制限事項、内部の仕組みまでまとめます。 この記事は [livewire/blaze](https://github.com/livewire/blaze) の README を一次情報としてまとめています。現時点では laravel.com 側に Blaze の専用ドキュメントはありません。 ## Blazeとは何か [Blaze](https://github.com/livewire/blaze) は、Livewire 公式が公開している Blade コンポーネント高速化パッケージです。対象は Livewire コンポーネントだけではなく、通常の匿名 Blade コンポーネントも含まれます。 README では、匿名コンポーネント 25,000 回描画のベンチマークとして **500ms → 13ms**(約 **97.4% 削減**)が示されています。シナリオ別でも 91〜97% の削減が報告されています。 ## 3つの最適化戦略 Blaze は `compile`(デフォルト)、`memo`、`fold` の3戦略を提供します。 ```mermaid theme={null} flowchart LR A["コンポーネント呼び出し"] --> B["compile
    関数コンパイル"] A --> C["memo
    同一propsをメモ化"] A --> D["fold
    コンパイル時にHTML化"] B --> E["広い互換性と大きい改善"] C --> F["繰り返し描画で強い"] D --> G["最大性能だが要注意"] ``` ### どれを選ぶべきか | 戦略 | いつ選ぶか | 利点 | 注意点 | | - | - | - | - | | `compile` | まず最初に選ぶ標準戦略 | 互換性が高く、設定なしで高速化しやすい | 制限事項は共通で確認が必要 | | `memo` | 同じ props で何度も描画するアイコン・バッジなど | 初回以降の再描画コストを下げられる | slot を持つコンポーネントでは使えない | | `fold` | 静的に確定できる UI で最大性能を狙うとき | 実行時オーバーヘッドをほぼ除去できる | グローバル状態や動的値の扱いを誤ると不整合になる | 判断に迷う場合は **`compile` から開始**し、ボトルネックが特定できた箇所にだけ `memo` / `fold` を適用する進め方が安全です。 ## インストールと有効化 ```bash theme={null} composer require livewire/blaze:^1.0 ``` 有効化は2通りあります。 ### 1) `@blaze` を個別コンポーネントに付ける ```blade theme={null} @blaze ``` 必要に応じて戦略を切り替えます。 ```blade theme={null} @blaze(memo: true) @blaze(fold: true) ``` ### 2) `Blaze::optimize()` でディレクトリ単位で有効化する ```php theme={null} use Livewire\Blaze\Blaze; public function boot(): void { Blaze::optimize() ->in(resource_path('views/components')); } ``` 有効化後はコンパイル済みビューをクリアします。 ```bash theme={null} php artisan view:clear ``` `@blaze` は「まず試す」用途、`Blaze::optimize()` は「運用で広く適用する」用途に向いています。README でもまず限定ディレクトリから段階的に適用することが推奨されています。 ## 制限事項 README で明示されている制限事項です。 * クラスベースコンポーネントは非対応 * `$component` 変数は利用不可 * View composers / creators / lifecycle events は発火しない * `View::share()` 変数の自動インジェクトは非対応(必要なら `$__env->shared('key')` を明示的に使う) * Blade と Blaze をまたぐ `@aware` は制限あり(親子とも Blaze 側で使う必要がある) * `view()` で Blaze コンポーネントを直接レンダリングできない(コンポーネントタグ経由のみ) ## Flux UI との相性 Blaze README では、[Flux UI](https://fluxui.dev/docs/installation) を使っている場合は **Blaze をインストールするだけで追加設定なしで利用開始できる** と案内されています。 ## 仕組みの概要 通常の Blade は、コンポーネント解決や属性処理を含むレンダリングパイプラインを毎回通ります。Blaze の `compile` は、これを最適化済み PHP 関数へコンパイルして直接呼び出すことで、パイプラインのオーバーヘッドを大きく削減します。 README の説明にある通り、概念的には次の差があります。 * 通常: Blade の標準レンダリング処理を都度実行 * Blaze `compile`: コンパイル済み関数を直接呼び出す * Blaze `fold`: コンパイル時に HTML を埋め込み、実行時計算をさらに減らす `fold` は最速ですが、静的化の前提を崩す要素(認証状態、リクエスト依存値、セッション依存値、時間依存値など)があると不具合の原因になります。高負荷箇所だけを対象に、挙動確認をしながら段階適用してください。 # AIエージェントは「正しい」コードから「Laravelらしい」コードへ - Boost Benchmarksの次の一手 Source: https://kawax.biz/jp/blog/boost-benchmarks-idiomatic-laravel Laravel公式ブログ「AI agents pass tests. Can they write idiomatic Laravel?」を基に、Boost Benchmarksがテスト合格から「トークンあたりの正しさ」と「Laravelらしさ」の計測へ舵を切った経緯を解説します。 ## 概要 2026年7月24日、Laravel公式ブログに [AI agents pass tests. Can they write idiomatic Laravel?](https://laravel.com/blog/idiomatic-laravel-ai-coding-agents) が公開されました。[Laravel Boost](https://laravel.com/docs/13.x/boost) の評価基盤である **Boost Benchmarks** が、AIコーディングエージェントの評価方針を大きく転換するという内容です。 Boost Benchmarksは、AIコーディングエージェントがLaravelアプリケーションを正しく実装できるかを、[Pest](https://pestphp.com/) テストとアーキテクチャテストで検証する評価スイートです。 ## これまでの経緯:17個のevalは「ほぼ全問正解」に到達 以前の記事 [which AI model is best for Laravel](https://laravel.com/blog/which-ai-model-is-best-for-laravel) の時点では、主要モデルはBoostを使って17個のeval(評価タスク)中16個をクリアし、正答率99.4%でした。それでも最も難しいタスクだけは突破できませんでした。 今回の記事によると、GPT-5.6、Claude Fable 5 / Mythos 5、Gemini 3.x系などの最新フロンティアモデルは、同じ17タスクをほぼ100%クリアするようになりました。「AIエージェントは正しいLaravelコードを書けるか?」という問いには、はっきり「Yes」という答えが出た形です。 これはBoostに限った話ではなく、コーディングベンチマーク全体で起きている現象です。 * **SWE-bench Verified**:主要モデルが人間エキスパートに近い水準で横並びになり、事実上飽和状態 * **HumanEval**:以前からモデル間の差を検出できなくなっている ## 「テストに合格する」だけでは不十分な理由 記事では、テスト合格が「良いコード」の完全な証明にはならない点も指摘しています。 * OpenAIは、多くの問題設定に欠陥があったとする監査結果を受けて[SWE-bench Verifiedの報告を取りやめました](https://openai.com/index/why-we-no-longer-evaluate-swe-bench-verified/) * [Cursorの調査](https://cursor.com/blog/reward-hacking-coding-benchmarks)では、モデルが `.git` 履歴などから正解を見つけて「解いた」ように見えるケースがあり、履歴を隠すとスコアが急落したことが報告されています Boost Benchmarksは、毎回新規のLaravelアプリからスタートし `.git` に答えを仕込まない設計のため、これらの問題には比較的強いとされています。ただし「Pestスイートに合格する」ことは「そのモデルがLaravelらしい書き方をする」ことを直接は証明しません。この「動く」と「そのコードベースに馴染む」の間のギャップこそが、次の焦点です。 ## 新しい評価軸:トークンあたりの正しさ、そして「Laravelらしさ」 記事が提示する今後の方針は次の2点です。 全モデルが正解にたどり着けるなら、次に重要なのは「どれだけのコストで」到達したかです。同じ結果でも、モデルによって消費トークン数が一桁、二桁変わることがあります。Boostはすでに各実行のトークン数とコストを記録していますが、これを脚注ではなく主要指標として扱うようになります。 テストに通っても、生のSQLクエリを書いたり `Route::resource()` を使わなかったり、[フォームリクエスト](https://laravel.com/docs/13.x/validation)の代わりにバリデーションを手書きしたり、`$fillable` を省略したりするコードは「良いLaravel」とは言えません。LLM-as-judge方式のスコアリングにより、模範解答なしで「Laravelらしいかどうか」を判定できる段階に来ているとしています。 判定基準として参照されるのが、Boostの [best-practices skill](https://github.com/laravel/boost/blob/main/.ai/laravel/skill/laravel-best-practices/SKILL.md) にまとめられた19項目の具体的な規約です。例として次のようなものが挙げられています。 * 独自実装のキャッシュ処理ではなく `Cache::remember()` を使う * インラインバリデーションではなくフォームリクエストを使う * Eagerロードには `with()` を使う * ルート定義には `Route::resource()` を使う 最初のルールは「Consistency First(一貫性を最優先)」で、より良いとされるパターンに飛びつく前に、既存の兄弟ファイルのスタイルに合わせることを求めています。 ## 今後のBoost Benchmarksで計測される予定の項目 記事末尾では、今後追加を検討している計測軸が挙げられています。 * **トークン・コストを主要指標として** eval・モデルごとに正答率と並べて報告 * **Laravelらしさのスコアリング**をPestテスト・アーキテクチャテストに加えて導入 * **コンテキストの軽量化**をBoost自身のエンジニアリング上の主目標にする(冗長なガイドラインの削除、ツール出力の最適化、必要な情報だけをインデックスする) 既存の17個のevalは回帰テストとして維持されつつ、この上に新しいレイヤーを構築していく方針です。 ## まとめ 「AIエージェントは正しいLaravelコードを書けるか」という問いには決着がつき、次の問いは「どれだけ効率的に」「どれだけLaravelらしく」書けるかに移っています。まだBoostを試していない場合は、[公式ドキュメント](https://laravel.com/docs/13.x/boost) からインストールできます。Boostの導入方法自体は [Laravel Boost](/jp/boost) ページも参照してください。 AI agents pass tests. Can they write idiomatic Laravel?(laravel.com) # 2026年3月 Laravel アップデート Source: https://kawax.biz/jp/blog/changelog/202603 2026年3月のLaravelエコシステムアップデート。Laravel 13リリース、AI SDK、JSON:API、Inertia v3など大型アップデートのまとめ。 2026年3月はLaravelエコシステムにとって歴史的な月でした。**Laravel 13**が正式リリースされ、ファーストパーティAI SDK、JSON:APIリソース、Inertia v3など、多数の大型機能が一斉に公開されました。 参照元:[Laravel March Product Updates](https://laravel.com/blog/laravel-march-product-updates) *** ## Laravel Framework ### Laravel AI SDK Laravel 13はファーストパーティのAI SDKを導入し、テキスト生成・ツール呼び出しエージェント・エンベディング・音声・画像・ベクターストア統合のための統一APIを提供します。プロバイダー非依存のAI機能を一貫したLaravelネイティブな開発体験で構築できます。 ```php theme={null} use Laravel\Ai\Facades\Ai; $response = Ai::text('gpt-4o')->generate('What is Laravel?'); ``` ### JSON:API リソース LaravelにJSON:APIリソースがファーストパーティで導入されました。JSON:API仕様に準拠したレスポンスを簡単に返せるようになり、リソースオブジェクトのシリアライズ、リレーションのインクルード、スパースフィールドセット、リンク、JSON:API準拠のレスポンスヘッダーを自動的に処理します。 ```php theme={null} use Laravel\JsonApi\Resources\JsonApiResource; class UserResource extends JsonApiResource { public function toAttributes(Request $request): array { return [ 'name' => $this->name, 'email' => $this->email, ]; } } ``` ### リクエスト偽造防止の強化 セキュリティ強化として、CSRFプロテクションミドルウェアが`PreventRequestForgery`として正式化されました。トークンベースのCSRF保護との互換性を維持しながら、オリジン対応のリクエスト検証が追加されています。 ### キュールーティング `Queue::route()`でクラス単位のキュールーティングが可能になりました。特定のジョブに対するデフォルトのキュー・コネクションのルーティングルールを一元管理できます。 ```php theme={null} Queue::route(ProcessPodcast::class, connection: 'redis', queue: 'podcasts'); ``` ### PHPアトリビュートの拡充 Laravel 13はフレームワーク全体でファーストパーティPHPアトリビュートのサポートを拡充しました。コントローラーや認証に関するアトリビュート(`#[Middleware]`、`#[Authorize]`など)が追加され、設定や振る舞いをより宣言的に記述できるようになりました。 ```php theme={null} use Illuminate\Routing\Attributes\Middleware; class UserController extends Controller { #[Middleware('auth')] public function index(): Response { // ... } } ``` ### Cache::touch() 既存のキャッシュアイテムのTTLを、値を取得・再保存せずに延長できる`Cache::touch()`が追加されました。 ```php theme={null} Cache::touch('user-session', now()->addHour()); ``` ### セマンティック検索・ベクター検索 ネイティブなベクタークエリサポート、エンベディングワークフロー、関連APIが追加されました。PostgreSQL + pgvectorを使ったAI搭載検索を手軽に構築できます。 *** ## Inertia v3 ### Vite プラグイン 新しい`@inertiajs/vite`プラグインにより、エントリポイントに書いていたボイラープレートが不要になりました。`./Pages`ディレクトリからページが自動解決され、コードスプリッティングとレイジーローディングも自動で処理されます。SSRの設定もゼロコンフィグです。 ```bash theme={null} npm install @inertiajs/vite --save-dev ``` ### 開発中のSSR `npm run dev`で自動的にSSRが動作するようになりました。開発中に別のNode.jsプロセスが不要で、ページがデフォルトでサーバーサイドレンダリングされます。 ### Axiosの削除 AxiosとqsがInertiaの依存関係から完全に削除されました。軽量な組み込みXHRクライアントがすべてのHTTP通信を処理し、バンドルサイズと依存関係のフットプリントが削減されます。 Axiosインターセプターなどに依存している場合は、Axiosアダプターが移行中も動作を維持します。 ### オプティミスティック更新 サーバーの応答を待たずに即時UIを更新する`.optimistic()`メソッドが追加されました。 ```javascript theme={null} router.post('/posts', data, { optimistic: (currentProps) => ({ posts: [...currentProps.posts, { ...data, id: 'temp' }], }), }); ``` ### インスタントビジット バックグラウンドでサーバーリクエストが進む間、即座にターゲットページコンポーネントに遷移できるようになりました。 *** ## その他パッケージ ### Prompts 新機能 * **`datatable()`** : 表形式データのブラウズ・検索・選択に対応した新プロンプト * **`title()`** : ターミナルウィンドウタイトルの設定 * **`stream()`** : AIレスポンスのリアルタイムストリーミング表示 * **`task()`** : スピナー付きタスク実行表示 * **`autocomplete()`** : 入力補完プロンプト * **`notify()`** : macOS/Linuxネイティブデスクトップ通知 ### Passport * **`#[TokenCan]` アトリビュート** : コントローラーメソッドに必要なトークンスコープを直接宣言 ```php theme={null} class PhotoController { #[TokenCan('read')] public function index() { /* ... */ } #[TokenCan('write')] public function store() { /* ... */ } } ``` ### Reverb * **レート制限** : Laravelの組み込みレートリミッターを使ったアプリ単位のレート制限をサポート ### Scout * **`where()`のオペレーター対応** : クエリビルダーで`where('created_at', '>', $date)`の記法が使えるように *** ## Laravel Cloud・Laravel Forge | 製品 | 主なアップデート | | - | - | | **Laravel Cloud** | クレカ不要 \$5 無料トライアル、GitHub SSO、スケジュールオートスケーリング、使用量ページリデザイン | | **Laravel Forge** | VPS上でManaged PostgreSQLデータベースをサポート(可観測性、リードレプリカ、自動バックアップ付き) | # 2026年4月 Laravel アップデート Source: https://kawax.biz/jp/blog/changelog/202604 2026年4月のLaravelエコシステムアップデート。Passkeys(パスワードレス認証)、Debounceable Jobs、MCP UI App、Redis Cluster対応など。 2026年4月はフルスタックのパスワードレス認証がLaravelの標準機能となり、バースト的なジョブディスパッチを制御するDebounceableジョブ、MCP UIアプリのサポートなど実用的な機能が多数追加されました。 参照元:[Laravel April Product Updates](https://laravel.com/blog/laravel-april-product-updates) *** ## Laravel Framework ### Debounceable Queued Jobs バースト的なワークロードに対するクリーンな解決策。`#[DebounceFor]`アトリビュートを使うと、指定時間内の複数ディスパッチを1つにまとめ、最後のディスパッチのみが実行されます。`ShouldBeUnique`の逆の発想で、デバウンスウィンドウ内の連続更新を効率化します。 ```php theme={null} use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Queue\Attributes\DebounceFor; #[DebounceFor(30)] class RebuildSearchIndex implements ShouldQueue { // ユーザーが30秒以内に10回ドキュメントを更新しても // インデックス再構築は1回だけ実行される } // 呼び出し側でもデバウンス可能(クラス変更不要) RebuildSearchIndex::dispatch()->debounceFor(seconds: 30); ``` デバウンスされたジョブは`JobDebounced`イベントを発火するので、スキップされたジョブを可視化できます。 ### ヘルスルートのJSONレスポンス対応 APIのみのアプリで`/up`からHTMLをスクレイピングする必要がなくなりました。組み込みヘルスルートがJSONリクエストに対してJSONを返すようになります。 ```json theme={null} { "status": "Application is up" } ``` ロードバランサー、アップタイムモニター、オーケストレーターの連携が簡単になります。設定変更は不要です。 ### JsonFormatter `JsonFormatter`により、カスタム例外の`context()`データが構造化ログに確実に記録されるようになりました。例外チェーン内の前の例外のコンテキストも含まれます。 ```php theme={null} // config/logging.php 'formatter' => Illuminate\Log\Formatters\JsonFormatter::class, ``` ### prefersJsonResponses() APIアプリで`bootstrap/app.php`に1行追加するだけで、広い`Accept`ヘッダーをJSONとして扱い、認証リダイレクト・バリデーションエラー・例外ページがクライアント向けに適切にシリアライズされます。 ```php theme={null} return Application::configure(basePath: dirname(__DIR__)) ->withRouting(...) ->prefersJsonResponses() ->create(); ``` ### Cloudflare Email Serviceサポート CloudflareのEmail ServiceがLaravelのメーラーとして正式サポートされました。 ```php theme={null} // config/services.php 'cloudflare' => [ 'account_id' => env('CLOUDFLARE_ACCOUNT_ID'), 'token' => env('CLOUDFLARE_TOKEN'), ], ``` ### Redis Clusterの完全サポート AWS ElastiCache ServerlessなどのRedis Cluster環境でのCROSSSLOTエラーが解消されました。Laravelのキュー、ConcurrencyLimiterがCluster接続を検出すると、キュー名を自動的にハッシュタグでラップします。 ``` queues:{default} # "default"スロット queues:{default}:delayed # 同じスロット queues:{default}:reserved # 同じスロット ``` ### Queue ジョブ検査メソッド ジョブの内容をDBを直接覗かずに確認できる3つのメソッドが追加されました。 ```php theme={null} Queue::pendingJobs(); // 待機中のジョブ Queue::delayedJobs(); // 遅延ジョブ Queue::reservedJobs(); // 処理中のジョブ // InspectedJobインスタンスにアクセス Queue::reservedJobs('high-priority')->first()->name; // => 'App\Jobs\SendEmail' ``` ### Form Request Strict Mode 未宣言のフィールドがバリデーションを通過しないよう制御できます。`rules()`で宣言されていない受信フィールドはバリデーション失敗になります。 ```php theme={null} // AppServiceProvider public function boot(): void { FormRequest::failOnUnknownFields(! app()->isProduction()); } ``` *** ## Passkeys(パスワードレス認証) パスワードレス認証がファーストパーティのスタック全体で実現しました。 * **`laravel/passkeys`(サーバーサイド)**: マイグレーション、ログイン/確認/クレデンシャル管理のルート、WebAuthnアクション、イベントを提供 * **`@laravel/passkeys`(クライアントサイド)**: React・Vue・Svelteの各ヘルパーで、ブラウザのWebAuthnセレモニーを処理 * **Fortify統合**: `Features::passkeys()`で有効化するだけで、Fortifyアプリが同じエンドポイントと契約を利用可能 ```php theme={null} // config/fortify.php 'features' => [ Features::passkeys(), ], ``` *** ## MCP UI App サポート MCPツールがテキストのみでなく、完全なサンドボックス化されたHTMLアプリをiframe内でレンダリングできるようになりました。 ```bash theme={null} php artisan make:mcp-app-resource DashboardApp ``` ```php theme={null} class DashboardApp extends AppResource { #[RendersApp] public function handle(Request $request): Response { return Response::view('mcp.dashboard-app', [ 'title' => 'Dashboard', ]); } } ``` *** ## Inertia・エコシステム * **`useHttp`フック**: `onHttpException`、`onNetworkError`コールバックが追加 * **Laravel Echo Svelte 5アダプター**: `useEcho` runeでSvelteからリアルタイム機能を利用可能 * **Dusk**: `clickOnceEnabled()`、`clickOnceVisible()`でフレイキーなテストを防止 * **Horizon**: Redis Clusterの完全サポート(AWS ElastiCache Serverless対応) * **VS Code拡張**: Laravel 13の新アトリビュートのオートコンプリート・ホバー・ナビゲーション対応 *** ## Laravel Cloud・Laravel Forge | 製品 | 主なアップデート | | - | - | | **Laravel Cloud** | モバイル対応のフルレスポンシブUI、エッジネットワーク強化(トップIP/パス表示、ファイアウォール・キャッシュルール変換) | | **Laravel Forge** | PHP 8.5サポート、複数SSLサーバーのサポート、証明書更新アラート | # 2026年5月 Laravel アップデート Source: https://kawax.biz/jp/blog/changelog/202605 2026年5月のLaravelエコシステムアップデート。@fonts Bladeディレクティブ、Interruptibleジョブ、サブエージェントサポート、CloudのScale-to-Zeroなど。 2026年5月はLaravel CloudのフルスタックScale-to-ZeroとManaged Queues、フォント最適化、マルチエージェントパイプラインなど実用的な大型アップデートが届きました。 参照元:[Laravel May Product Updates](https://laravel.com/blog/laravel-may-product-updates) *** ## Laravel Framework ### @fonts Bladeディレクティブ Webフォントの読み込みに必要だった``タグ、`@font-face`スタイル、HTTP/2プッシュヘッダーをすべて自動処理する`@fonts`ディレクティブが追加されました。Viteフォントプラグインが生成するフォントマニフェストを読み込み、必要な要素を一括で注入します。 ```blade theme={null} {{-- 全フォントを読み込む --}} @fonts {{-- 特定のフォントファミリーのみ --}} @fonts(["sans", "mono"]) ``` `@fonts`はページごとに必要なフォントのみを制御することで、不要なプリロードを防ぎパフォーマンスを最適化できます。 ### Interruptible Jobs(シグナル対応ジョブ) 新しい`Interruptible`インターフェースにより、デプロイ中にキューワーカーがSIGTERMを受信したときにジョブが反応できるようになりました。ループの停止、ロックの解放、状態の保存などをワーカーのシャットダウン前に安全に行えます。 ```php theme={null} use Illuminate\Contracts\Queue\Interruptible; use Illuminate\Contracts\Queue\ShouldQueue; class ProcessLargeReport implements ShouldQueue, Interruptible { public function handle(): void { foreach ($this->getChunks() as $chunk) { if ($this->shouldInterrupt()) { $this->saveProgress(); return; } $this->processChunk($chunk); } } } ``` ### JSON例外のAPIルートデフォルト対応 APIルートが自動的にJSON形式でエラーレスポンスを返すようになりました。これまでAPIルートでも条件によってはHTMLが返ることがありましたが、`api.php`で定義されたルートは常にJSONレスポンスが保証されます。 ### ストレージキャッシュストア 新しい`storage`キャッシュストアが追加されました。ファイルシステムをキャッシュバックエンドとして使用しますが、Laravelのストレージ設定を活用できます。 ```php theme={null} // config/cache.php 'stores' => [ 'storage' => [ 'driver' => 'storage', 'disk' => 'local', 'path' => 'framework/cache', ], ], ``` ### 大容量SQSペイロードのディスク保存 SQSのペイロードが大きすぎる場合(256KB超)にディスクに保存するオプションが追加されました。大容量データをキューで扱う際に役立ちます。 ### スケジュールコマンドの環境フィルター `schedule:list`コマンドで環境別にフィルタリングできるようになりました。 ```bash theme={null} php artisan schedule:list --environment=production ``` ### ワーカーのStop When Emptyオプション キューが空になったら自動的にワーカーを停止する`--stop-when-empty`オプションが追加されました。バッチ処理やCI環境でキューが空になったら終了するワーカーに便利です。 ```bash theme={null} php artisan queue:work --stop-when-empty ``` ### foreignUuidForスキーマヘルパー `foreignIdFor()`のUUID版として`foreignUuidFor()`が追加されました。 ```php theme={null} Schema::table('posts', function (Blueprint $table) { $table->foreignUuidFor(User::class)->constrained(); }); ``` *** ## AIサブエージェントサポート Laravel AIエージェントが他のエージェントにデリゲートできるようになりました。`tools()`メソッドから任意のエージェントを返すと、親エージェントのLLMが通常のツールと同様にそれを呼び出します。コンテキストは分離されるので会話履歴が混在することはありません。 ```php theme={null} class OrchestratorAgent extends Agent { public function tools(): array { return [ new AnalysisAgent(), new WritingAgent(), new SearchTool(), ]; } } ``` *** ## Inertia 3.x * **`router.poll` の `mode` オプション**: ポーリングの動作モードを設定可能 * **`usePoll` の動的データ対応**: ポーリング中にデータを動的に更新 * **`Inertia.once`**: 一度だけ発火するイベントのサポート * **`` の `rescue` スロット**: defer済みプロップが失敗した時のフォールバック *** ## PAO(PHP Agentic Output) スターターキットにデフォルトでPAOが組み込まれました。以下の機能も追加されています: * **Rector対応**: PAOがRectorのJSON出力も処理 * **PHPStanのエージェントガイダンス**: エラー箇所の修正方法をエージェントに伝えるガイダンスを追加 * **`PAO_FORCE`環境変数**: 強制的にPAOモードを有効化 * **Artisan RECTORサポート**: PAOがArtisanコマンドに対応 *** ## Laravel Cloud・Laravel Forge・Nightwatch | 製品 | 主なアップデート | | - | - | | **Laravel Cloud** | フルスタックScale-to-Zero(500ms以下で起動、20倍高速化)、Managed Queues(自動スケーリングワーカー・失敗ジョブダッシュボード)、支出制限、プラン更新(月額\$5 Starterプラン) | | **Laravel Forge** | Managed MySQL 8.4(自動バックアップ・リードレプリカ・アラート)、PHP 8.5がデフォルトに変更、envが変わると自動的にHorizonを再起動 | | **Nightwatch** | MCPサーバーがルート実行時間・クエリ時間・ジョブ実行メトリクスなどパフォーマンスデータを提供 | # 2026年6月 Laravel アップデート Source: https://kawax.biz/jp/blog/changelog/202606 2026年6月のLaravelエコシステムアップデート。Bus::bulk()、Postgresトランザクションプーラー対応、MCPクライアント/サーバー、artisan devコマンド、ルートメタデータなど。 2026年6月はLaravel CloudがSymfonyアプリをサポートし、Forgeに管理型Valkeyキャッシュとオブジェクトストレージが追加。フレームワークはバルクジョブディスパッチ、Postgresプーラー対応、MCPクライアント/サーバーなど充実したアップデートが届きました。 参照元:[Laravel June Product Updates](https://laravel.com/blog/laravel-june-product-updates) *** ## Laravel Framework ### Bus::bulk() — バルクジョブディスパッチ 大量のジョブを1回のデータベースINSERTで登録できるようになりました。キュー・コネクションごとにバルクINSERTが行われるため、1件ずつINSERTするコストが不要になります。 ```php theme={null} use Illuminate\Support\Facades\Bus; Bus::bulk([ new ProcessOrder($order1), new ProcessOrder($order2), new ProcessOrder($order3), // 数千件でも効率的 ]); ``` ### Postgresトランザクションプーラーサポート PgBouncer、AWS RDS Proxy、NeonなどのトランザクションモードのPostgres接続プーラーをフレームワークレベルでサポートしました。 ```php theme={null} // config/database.php 'pgsql' => [ 'driver' => 'pgsql', 'pooled' => true, // プーラー経由の接続を宣言 // ... ], ``` スキーマ操作やDDL文など直接接続が必要な場合は、コネクション名に`::direct`を追加するとプーラーをバイパスします。 `DB::connection('pgsql::direct')->statement('CREATE INDEX ...')` フレームワークが自動的にemulated prepares、boolean binding、その他プーラーが必要とするすべての処理を行います。 ### MCPクライアント・サーバーツールサポート Laravel AIエージェントがリモートMCPサーバー(HTTP経由)とローカルMCPサーバークラスのツールを利用できるようになりました。スキーマ変換、ラッピング、呼び出しがすべて自動で処理されます。 ```php theme={null} class MyAgent extends Agent { public function tools(): array { return [ // リモートMCPサーバーのツールを利用 McpClient::tools('https://mcp.example.com'), // ローカルMCPサーバーのツールを利用 McpServer::tools(MyMcpServer::class), ]; } } ``` ### artisan dev コマンド すべての開発プロセスを1つのコマンドで起動できる`artisan dev`が追加されました。サーバー、キューワーカー、ログテーリング、Viteがそれぞれカラーコード付きの出力で同時に動作します。 ```bash theme={null} php artisan dev ``` ```php theme={null} // サービスプロバイダーで追加のコマンドを登録 use Illuminate\Foundation\Console\DevCommands; DevCommands::artisan('reverb:start', 'reverb')->orange(); DevCommands::register('stripe listen --forward-to ' . config('app.url'))->green(); ``` Node.jsパッケージマネージャー(npm、yarn、pnpm、bun)を自動検出します。従来の`composer dev`スクリプトを置き換えるArtisanの正式なコンベンションです。 ### ルートメタデータサポート ルートに任意のメタデータを付与できる`->metadata()`メソッドが追加されました。ルートキャッシュとも完全互換で、グループに設定したメタデータは子ルートにマージされます。 ```php theme={null} Route::metadata(['head' => ['robots' => ['noindex'], 'author' => 'Taylor']]) ->group(function () { Route::get('/users', [UserController::class, 'index']) ->metadata(['head' => ['title' => 'Users']]); }); // コントローラーやミドルウェアからアクセス $request->route()->getMetadata('head.title'); // 'Users' $request->route()->getMetadata('head.author', 'Taylor'); // 'Taylor' ``` ### 再試行しない例外ハンドラー リトライしても意味のない例外(不正な入力、永続的な外部障害など)に対して、再試行を抑制できるようになりました。 ```php theme={null} // 例外クラスに直接定義 class InvalidInputException extends RuntimeException { public function retry(): bool { return false; } } // bootstrap/app.php でも設定可能 ->withExceptions(function (Exceptions $exceptions) { $exceptions->dontRetry(InvalidInputException::class); }) ``` ### schema:dump の --without-migration-data フラグ `schema:dump`でマイグレーションテーブルの行を省いた純粋な構造ダンプが生成できるようになりました。 ```bash theme={null} php artisan schema:dump --without-migration-data ``` ### between()/unlessBetween()のタイムゾーン修正 `between()`や`unlessBetween()`の前に`timezone()`を呼び出す順序に関係なく、正しいタイムゾーンでスケジュールが動作するようになりました。 ```php theme={null} // どちらも同じ動作をする(以前はbetweenを先に書くとUTCが使われてしまう問題があった) $schedule->command('foo')->timezone('Europe/Rome')->between('10:00', '12:00'); $schedule->command('foo')->between('10:00', '12:00')->timezone('Europe/Rome'); ``` ### その他のフレームワーク変更 * **`Bus::bulk()`のトレイト対応**: トレイトにキューアトリビュートを定義可能 * **MariaDBベクターインデックス**: MariaDBでベクターインデックスのサポート * **`attachFromStorage()`ヘルパー**: 通知の`MailMessage`にストレージから添付ファイルを追加 * **`Cache::rememberWithState()`**: ステートを持つキャッシュサポート * **平行テスト用メンテナンスモードドライバー**: `array`ドライバーで各プロセスが独立したメンテナンス状態を持てる * **`whenFilledEnum()`**: Enumを使ったフィールドの条件付き処理 * **JSON SchemaのanyOfサポート**: バリデーションでJSON Schemaの`anyOf`が使用可能 *** ## Inertia * **`Client\Request::uri()`**: クライアントリクエストのURI取得 * **アンカーの`target`属性対応**: ``コンポーネントで`target="_blank"`などが使用可能 * **フォームの`async`オプション**: フォームコンポーネントで非同期送信をサポート * **`titleCallback`にページ情報**: タイトルコールバックにページデータが渡される *** ## Laravel Cloud・Laravel Forge | 製品 | 主なアップデート | | - | - | | **Laravel Cloud** | Symfonyアプリの完全サポート(Symfony 7.4 LTS・8.x、PHP 8.2〜8.5対応)、Stripe Projectsからのデプロイ・支払い対応、チームメンバーごとのSlack/メール通知設定 | | **Laravel Forge** | 管理型Valkeyキャッシュ(ダッシュボードから作成・管理)、管理型オブジェクトストレージ、通知メールのリデザイン | # 2026年7月 Laravel アップデート Source: https://kawax.biz/jp/blog/changelog/202607 2026年7月のLaravel 13とLaravel Cloudの公式アップデート。画像処理、条件付きコンテナバインド、キュー管理、月次ログ、Laracon US 2026の発表をまとめます。 2026年7月は、Laravel 13のマイナーリリースで画像処理やキュー、コンテナ、ログに関する機能が追加されました。Laravel Cloudでは、Managed QueuesやNext.js・Nuxtアプリのデプロイが発表され、Laracon US 2026ではLaravel LSPやInertia DevToolsなどの新機能が紹介されました。 7月には、6月のような月次プロダクトアップデート記事は公開されていません。本ページでは、7月に公開された公式リリースと公式ブログの記事を対象にしています。 参照元: * [Laravel Framework v13.20.0](https://github.com/laravel/framework/releases/tag/v13.20.0) * [Laravel Framework v13.22.0](https://github.com/laravel/framework/releases/tag/v13.22.0) * [Laravel Framework v13.23.0](https://github.com/laravel/framework/releases/tag/v13.23.0) * [Everything we announced at Laracon US 2026](https://laravel.com/blog/everything-we-announced-at-laracon-us-2026) * [Deploy Next.js and Nuxt apps on Laravel Cloud](https://laravel.com/blog/deploy-nextjs-and-nuxt-apps-on-laravel-cloud) * [AI coding agents pass tests. Can they write idiomatic Laravel?](https://laravel.com/blog/idiomatic-laravel-ai-coding-agents) * [Laravel Cloud security defaults behind every deploy](https://laravel.com/blog/laravel-cloud-security-defaults-behind-every-deploy) *** ## Laravel Framework ### 画像処理のファーストパーティサポート Laravel 13.20.0で、画像処理のファーストパーティサポートが追加されました。`Image`クラスを使った画像の変換や、ストレージから読み込んだ画像の処理をLaravelのAPIから扱えます。 ### 条件付きのコンテナバインド 13.22.0では、`#[BindWhen()]`属性が追加されました。実行時の条件に応じたコンテナバインドを、サービスプロバイダーの登録処理からクラスの属性へ移せます。 ### キューとジョブの改善 * `queue:clear`が複数のキューを扱えるようになりました。 * `QueueFake`で保留中の最古のジョブ作成時刻を取得できるようになりました。 * `Bus::batch`と`bulk`で`Delay`属性がサポートされました。 * キューのテストで、遅延時間が考慮されるようになりました。 ### ログとセキュリティ 13.23.0では、月次ログチャンネルと月次ログドライバーが追加されました。毎月ローテーションするログを、標準のロギング設定で構成できます。 また、メンテナンスモードのバイパスシークレットにタイミングセーフな比較が使われるようになり、HTTPテストの認証情報が機密値として扱われるようになりました。 ### その他の変更 * `RateLimiter`をマクロ可能に設定できるようになりました。 * `Cache::touch()`の有効期限処理が修正されました。 * `Storage`ファサードに`assertEmpty`が追加されました。 * `#[SensitiveParameter]`が機密情報を受け取るパラメータに追加されました。 * `ImageManager`の`fromStorage`でEnumをディスク名に使えるようになりました。 *** ## Laravel Cloud ### Managed Queues Laravel CloudにManaged Queuesが追加されました。キューの負荷に応じてワーカーを自動でスケールし、失敗したジョブをダッシュボードで確認できます。アイドル時はゼロまでスケールできます。 ### Next.jsとNuxtのデプロイ Laravel CloudでLaravelアプリと同じプロジェクト内にNext.jsやNuxtのフロントエンドをデプロイできるようになりました。アプリケーションとフロントエンドを同じデプロイフローで管理できます。 ### セキュリティのデフォルト Laravel Cloudのデプロイには、WAF、ランタイムのパッチ適用、監査ログ、ネットワーク分離、パッケージの脆弱性スキャンが標準で組み込まれています。 *** ## Laracon US 2026 7月のLaracon US 2026では、次のLaravelエコシステムのアップデートが発表されました。 * **Laravel LSP**:Laravelプロジェクト向けのLanguage Server Protocolサポート * **Inertia DevTools**:Inertiaアプリケーションの開発者向けツール * **Managed Queues**:Laravel Cloudのキュー自動スケーリング * **Scale to Zero**:Laravel Cloudのアイドル時スケールダウン *** ## AI開発 Laravel Boostの評価では、AIコーディングエージェントがLaravelのテストを通過するだけでなく、Laravelらしいコードを書けるかどうかを測定する取り組みが紹介されました。Laravelの規約やフレームワークのAPIを正しく使うことを、AI開発の評価軸として扱っています。 # 2026年8月 Laravel アップデート Source: https://kawax.biz/jp/blog/changelog/202608 2026年8月のLaravel 13、Laravel Cloud、Laravel Forgeの公式アップデート。read-throughファイルシステム、ScoutのTurbopuffer・セマンティック検索、CloudファサードやMariaDBベクター検索対応をまとめます。 2026年8月は、Laravel Framework v13.24.0からv13.29.0にかけて、ファイルシステムの移行機能やScoutのセマンティック検索、Eloquent、キュー管理まわりの機能追加が続きました。Laravel CloudとLaravel Forgeでも大きめのアップデートがあり、公式ブログの月次プロダクトアップデート記事「Laravel August product updates」で公開されています。 参照元: * [Laravel August product updates](https://laravel.com/blog/laravel-august-product-updates) * [Laravel Framework v13.24.0](https://github.com/laravel/framework/releases/tag/v13.24.0) * [Laravel Framework v13.25.0](https://github.com/laravel/framework/releases/tag/v13.25.0) * [Laravel Framework v13.26.0](https://github.com/laravel/framework/releases/tag/v13.26.0) * [Laravel Framework v13.27.0](https://github.com/laravel/framework/releases/tag/v13.27.0) * [Laravel Framework v13.29.0](https://github.com/laravel/framework/releases/tag/v13.29.0) * [Laravel Framework changelog](https://laravel.com/docs/changelog) * [Laravel Cloud changelog](https://cloud.laravel.com/docs/changelog) * [Laravel Forge changelog](https://laravel.com/forge/docs/changelog) *** ## Laravel Framework ### read-throughファイルシステムドライバー ダウンタイムなしでストレージディスクを移行できる`read-through`ファイルシステムドライバーが追加されました。プライマリディスクを先にチェックし、ファイルがフォールバックディスクにしか存在しない場合はそこから配信しつつ次回以降のためにコピーします。新規の書き込みは移行先ディスクに保存され、古いファイルはアクセスされるたびに昇格していく仕組みです。詳細は[ファイルシステムページ](/jp/filesystem#read-throughディスク)を参照してください。 ### `artisan dev`が`@laravel/multiplex`経由で動作するように v13.26.0で、`php artisan dev`が内部で[`@laravel/multiplex`](https://github.com/laravel/multiplex)を通して実行されるようになりました。開発サーバーやVite、キューワーカーなどを1つのタブUIで同時実行する仕組みが強化され、Viteを使わないプロジェクトで不要にVite起動やmultiplexインストールを促されていた問題も修正されました。 ### ScoutのTurbopufferとセマンティック検索 Scoutにバッチインデックスとベクター距離メタデータを備えたTurbopufferエンジンが追加されました。あわせて、Scoutのデータベースエンジン・Meilisearchエンジンでセマンティック検索とハイブリッド検索が使えるようになり、既存のScoutビルダーに`->semantic()`や`->hybrid()`をチェーンするだけで、キーワードではなく意味による検索ができます。詳細は[検索の統一ガイド](/jp/search#セマンティック--ベクター検索)と[Scoutページ](/jp/scout)を参照してください。 ### Cloudファサード v13.27.0で`Illuminate\Support\Facades\Cloud`ファサードが追加されました。アプリが実行時にLaravel Cloud上でホストされているかを検出したり、マネージドキュー接続の有無を確認して直接アクセスしたりできます。 ### `Queue::forward()` v13.27.0で`Queue::forward()`が追加され、あるキュー接続からジョブを別の接続へ転送できるようになりました。 ### Eloquentの`refreshForUpdate()` v13.29.0で、トランザクション内でモデルを再読み込みしつつ`FOR UPDATE`ロックを取得する`refreshForUpdate()`メソッドが追加されました。詳細は[Eloquentページ](/jp/eloquent#悲観的ロック付きでモデルを再読み込みする)を参照してください。 ### ベクター検索のMariaDB対応 Eloquentの`AsVector`キャストや、`whereVectorSimilarTo`・`orderByVectorDistance`などのベクター類似度検索メソッドが、PostgreSQL(`pgvector`)に加えてMariaDB 11.7以降でも利用できるようになりました。 ### その他の変更 * 画像処理でHEIC・AVIF形式のサポートが拡充されました。 * クエリビルダーに`whereBinary()`、Eloquentビルダーに`orWhereKey()` / `orWhereKeyNot()`が追加されました。 * `#[BindWhen]`属性で、条件を満たさなかったバインディングが再評価されるようになりました。 * キューのテストフェイクに、ジョブの合計サイズを取得する`totalXSize`系メソッドが追加されました。 *** ## Laravel Cloud ### 事前デプロイのパッケージチェック Cloudがプッシュのたびに`composer.lock`を読み取り、Octane、Inertia SSR、Nightwatchなど不足・古いパッケージをデプロイ前に警告するようになりました。修正をプッシュすると通知は自動でクリアされます。 ### Managed Queuesの刷新 Managed Queuesに2つのコンピュートクラスが追加されました。ゼロまでスケールし1秒未満で起動する「Flex」と、レイテンシに敏感な安定したスループット向けに常時起動する「Pro」です。また、FIFOキューにより、決済や台帳更新のように順序が重要なワークロード向けに、ジョブが正確に1回・送信した順序どおりに配信されることが保証されます。 ### スコープ付きAPIトークン 組織のAPIトークンに、特定の権限へのスコープ設定と単一のアプリケーション・環境への制限が可能になりました。自動化やエージェントに必要最小限のアクセス権のみを与えられます。 ### Private CloudのHIPAA対応 医療系ソフトウェアを開発するチームがBusiness Associate Agreement(BAA)を要求し、専用の監査済みインフラにデプロイできるようになりました。 *** ## Laravel Forge ### 複数のソースコントロール接続 プロバイダーごとに複数のソースコントロールアカウントを接続できるようになりました。個人アカウントと組織アカウントを切り替えるたびに接続・切断していたチームに便利です。 ### Ubuntu 26.04 Ubuntu 26.04 LTSが全サーバープロバイダーで利用可能になりました。新規サーバーではUbuntu 22.04は提供されなくなりました。 ### その他の変更 * アプリ内通知のプリファレンスが追加され、デプロイ成功やサーバーの停滞通知をメールとは別に個別にオン・オフできるようになりました。 * 新規サーバーでBrotli圧縮モジュールが標準でコンパイル・ロードされ、Nginx設定で有効化できます。 *** ## まとめ 8月はストレージ移行を安全に行える`read-through`ドライバーや、Scoutのセマンティック・ハイブリッド検索など、既存システムの信頼性と検索体験を底上げする変更が目立った月でした。Laravel Cloud側でもManaged Queuesの刷新やスコープ付きAPIトークンなど、本番運用を意識した機能強化が続いています。ベクター検索のMariaDB対応は、PostgreSQL以外のデータベースを使うプロジェクトにとって朗報です。 # 2026年9月 Laravel アップデート Source: https://kawax.biz/jp/blog/changelog/202609 2026年9月のLaravel 13と公式プロダクトの更新。実行時のAIプロバイダー構築、Scoutの検索拡張、Mercure、ディスク間転送、キュー管理、Cloudの隔離プレビュー環境をまとめます。 2026年9月は、テナントごとのAI設定や検索エンジンの選択肢が広がり、ストレージ・キュー・データベースの運用を支える機能も追加されました。Laravel Cloudでは、Pull Requestごとにリソースを分離するプレビュー環境や、追加パッケージなしで使えるリクエストメトリクスが紹介されています。 本ページは、10月2日公開の公式ブログ「Laravel September product updates」と、9月公開のLaravel 13の公式リリースノートをもとに、主な変更をまとめた記録です。Frameworkの項目にはAI SDK・Scout・Echoなど公式パッケージの変更も含みます。これらのパッケージの更新を、Framework本体のバージョン番号だけで判断しないようにしてください。 参照元: * [Laravel September product updates](https://laravel.com/blog/laravel-september-product-updates) * [Laravel Framework changelog](https://laravel.com/framework/docs/changelog) * [Laravel Framework v13.30.0](https://github.com/laravel/framework/releases/tag/v13.30.0) * [Laravel Framework v13.30.1](https://github.com/laravel/framework/releases/tag/v13.30.1) * [Laravel Framework v13.31.0](https://github.com/laravel/framework/releases/tag/v13.31.0) * [Laravel Framework v13.32.0](https://github.com/laravel/framework/releases/tag/v13.32.0) * [Laravel Framework v13.33.0](https://github.com/laravel/framework/releases/tag/v13.33.0) * [Laravel Framework v13.34.0](https://github.com/laravel/framework/releases/tag/v13.34.0) ## Laravel Framework・公式パッケージ ### テナントごとのAIプロバイダーを実行時に構築 AI SDKの`Ai::build()`で、設定配列からプロバイダーを構築できるようになりました。アプリケーションに登録済みのプロバイダー設定を変更せず、実行時に取得した認証情報を使えます。 たとえば、テナントがそれぞれ契約したAnthropicのAPIキーを使う場合に、テナントごとの設定からプロバイダーを作り、エージェントの`prompt()`へ渡せます。構築したプロバイダーはフェイルオーバーの候補にも指定できます。 複数テナントの認証情報を扱う際に、グローバルな設定の書き換えではなく、呼び出しに使うプロバイダーを明示できる点が重要です。AI SDKの基本は[AI SDKガイド](/jp/ai-sdk)を参照してください。 一次情報:[Build AI providers from runtime configuration](https://laravel.com/framework/docs/changelog#september-2026-build-ai-providers-from-runtime-configuration) ### ScoutのTypesense・Meilisearch検索拡張 ScoutのTypesenseエンジンで、`semantic()`によるセマンティック検索と`hybrid()`によるハイブリッド検索が使えるようになりました。ベクターはLaravel AIで生成するだけでなく、Typesenseの自動Embedding機能や、独自に用意したベクターも利用できます。 Meilisearchエンジンでは、Meilisearch自身のEmbedderを使えるようになりました。アプリ側でEmbeddingを生成する方式と、検索エンジン側に任せる方式を、構成に応じて選べます。 8月の検索拡張に続き、9月はTypesenseとMeilisearch側でのEmbedding生成の選択肢が増えた月です。[Scout](/jp/scout)と[検索ガイド](/jp/search)で、エンジンごとの構成を確認してください。 一次情報:[Typesenseのセマンティック・ハイブリッド検索](https://laravel.com/framework/docs/changelog#september-2026-semantic-and-hybrid-search-for-typesense)、[MeilisearchのネイティブEmbedder](https://laravel.com/framework/docs/changelog#september-2026-use-meilisearchs-native-embedders-with-scout) ### Mercureによるリアルタイム通信 Laravel EchoにMercureコネクターが追加されました。ブラウザー標準の`EventSource` APIを使うため、追加のクライアントライブラリは不要です。Private・Presence・暗号化チャンネルに加え、Whisperにも対応します。 Framework v13.32.0にはMercureブロードキャストドライバーと、`install:broadcasting`によるMercureの導入サポートが含まれます。v13.33.0ではMercure Hubの相対URLも扱えるようになりました。 Echo側のコネクターとLaravel側のドライバーを組み合わせて使う機能です。構成や認証は[Mercureブロードキャスティング](/jp/advanced/mercure-broadcasting)を参照してください。 一次情報:[Mercure broadcasting in Laravel Echo](https://laravel.com/framework/docs/changelog#september-2026-mercure-broadcasting-in-laravel-echo)、[v13.32.0](https://github.com/laravel/framework/releases/tag/v13.32.0)、[v13.33.0](https://github.com/laravel/framework/releases/tag/v13.33.0) ### ディスク間でファイルをコピー・移動 v13.32.0で、ファイルシステムに`copyToDisk()`と`moveToDisk()`が追加されました。ローカルディスクからS3へ移すような、異なるディスク間の転送をストリームで行えます。転送先にはディスク名だけでなく、ファイルシステムのインスタンスも渡せます。 `moveToDisk()`が転送元を削除するのは、コピーに成功した後です。ディスク間移行のために、アプリケーション側で読み込み・書き込み・削除を別々に組み立てる必要が減ります。 一次情報:[Copy or move files between disks](https://laravel.com/framework/docs/changelog#september-2026-copy-or-move-files-between-disks)、[v13.32.0](https://github.com/laravel/framework/releases/tag/v13.32.0)。関連ページ:[ファイルシステム](/jp/filesystem) ### キューの計測・停止・再試行を改善 9月のリリースでは、キューの状態把握やワーカーの制御が段階的に強化されました。 | リリース | 主な変更 | | - | - | | v13.30.0 | `queue:work`の停止理由の表示、Cloud Queueの`totalXSize`系メソッド、Queue Managerでの`createPayloadUsing`対応。 | | v13.30.1 | `WorkerStopping`イベントへの接続・キュー情報の追加。 | | v13.31.0 | Queueの`totalSize()`、`JobInterrupted`イベント、ワーカーを終了させた際の`WorkerStopping`への接続・キュー情報の追加。Redis Clusterでのキュー操作も修正。 | | v13.32.0 | キューの一時停止・再開メソッドでEnumを利用可能に。Cloud Managerには`isManagedQueue`を追加。 | | v13.33.0 | ジョブのタイムアウト時にワーカーを終了させない選択肢、Cloudジョブの再試行改善、再試行できないバッチを`queue:retry-batch`でスキップ。 | | v13.34.0 | `JobProcessed`イベントへの処理時間の追加、タイムアウト前のジョブ通知、`queue:flush`の`--queue`オプション、ワーカーのクラッシュを`maxExceptions`へ数える選択肢。 | v13.34.0では、再起動できないワーカーに対する`queue:restart`がエラーを表示するようになりました。運用コマンドが成功したように見えても意図した再起動が行われない、といった状況を把握しやすくする変更です。 各項目の出典は上記の[公式リリースノート](https://github.com/laravel/framework/releases)です。基本操作は[キューガイド](/jp/queues)、ジョブ側の制御は[ジョブ制御の応用ガイド](/jp/advanced/queue-job-control)を参照してください。 ### データベース・Eloquent・キャッシュの追加機能 * **ベクターインデックスの削除**:v13.30.1で、Schema Blueprintに`dropVectorIndex()`が追加されました。 * **MySQLのINPLACE操作**:v13.33.0で、インデックス・外部キー操作に`inplace()`のDDLアルゴリズム修飾子が追加されました。関連ページ:[マイグレーション](/jp/migrations)。 * **書き込み後のモデル属性の再取得**:v13.33.0で、設定したモデル属性を書き込み後に再取得する機能が追加されました。 * **NULLを維持するキャスト**:v13.33.0で、`AsCollection`・`AsArrayObject`に実際の`NULL`を保存するオプトイン機能が追加されました。 * **タグ付きメモ化キャッシュ**:v13.33.0で、タグを付けたキャッシュのメモ化に対応しました。関連ページ:[キャッシュ](/jp/cache)。 * **スキーマ情報取得**:v13.34.0で、Schema Builderに`getColumn()`が追加されました。 * **データベーステスト**:v13.34.0で、`assertDatabaseCount`が配列を受け取れるようになりました。 * **コレクション**:v13.30.0で`chunkBy()`、v13.33.0でHigher Order Proxyの`sole`対応が追加されました。 ### 既存動作の修正 機能追加だけでなく、既存アプリケーションに影響する修正も含まれます。 * v13.31.0では、`lazy()`・`lazyById()`が`limit()`・`offset()`を無視する問題と、並行プロセスへContextが伝播しない問題が修正されました。 * v13.32.0では、Redisのタグ付きキャッシュで`touch()`時にタグの有効期限が同期されない問題が修正されました。 * v13.34.0では、HTTPクライアントのMultipartリクエストを再試行すると、添付ストリームの内容が失われる問題が修正されました。 * v13.34.0では、Pivotの`wherePivotBetween()`が`sync()`・`detach()`・`updateExistingPivot()`で無視される問題が修正されました。 * v13.34.0では、トランザクション内の`DatabaseLock`がデッドロックを握りつぶす問題が修正されました。 この一覧は9月の全変更ではありません。細かな修正や依存関係の更新は、各リリースノートを確認してください。 ## Laravel Cloud 公式月次ブログでは、次の変更が紹介されています。 | 機能 | 更新内容 | | - | - | | 隔離されたプレビュー環境 | Pull Requestごとに新しいリソースを用意し、本番リソースを参照しない構成へ改善。Flexサイズではレビューの合間に休止し、PRのマージ・クローズで削除されます。Deployments画面にはブランチ・最新コミット・プレビューへのリンクも表示されます。 | | リクエストメトリクス | MonitoringのRequestsタブでリクエスト数・レスポンスコード・ページごとの処理時間を確認可能に。ドメインと期間で絞り込め、パッケージ追加やコード変更は不要です。 | | Managed database users | データベースクラスターに追加ユーザーを作成し、必要なデータベースだけに読み取り専用・読み書き権限を付与できます。Laravel MySQL・RDS MySQL・RDS Postgresに対応します。 | | MySQLストレージの自動拡張 | Laravel MySQLのストレージ使用率が90%に達すると、容量を50%増やします。オンラインのまま拡張し、最大サイズの指定やデータベース単位での無効化もできます。 | | Log drains | Enterpriseプランで、アプリケーションログを自分たちのAmazon S3バケットへリアルタイム転送できます。長期保存や既存の監視基盤への連携に利用できます。 | 一次情報:[Laravel Cloud changelog](https://laravel.com/cloud/docs/changelog)。関連ページ:[Laravel Cloudの紹介](/jp/blog/laravel-cloud) ## Laravel Forge * **リダイレクトルール管理**:CSVから一括インポートし、既存ルールへの追加または置き換えを選べるようになりました。CSVエクスポートと、ドラッグによるルール実行順の変更にも対応します。 * **Server muting**:通知設定でサーバーを選んでミュートできるようになりました。ステージング・テスト環境の通知を受信箱から除外する用途に使えます。 一次情報:[Laravel Forge changelog](https://laravel.com/forge/docs/changelog) ## 関連記事 read-throughディスク、Scoutの検索拡張、CloudのManaged Queues刷新を振り返ります。 MercureをLaravelとEchoに組み込む構成を確認します。 # Laravel Chisel — スターターキットのポストインストールスクリプトライブラリ Source: https://kawax.biz/jp/blog/chisel-introduction laravel/chiselの紹介。スターターキットから不要な機能を削除するためのポストインストールスクリプト用プリミティブを提供するパッケージ。v0.1として2026年5月に公開されました。 この記事はソースコード調査に基づく情報です。v0.1タグが付いたばかりの開発初期段階のパッケージです(2026年5月時点)。 ## Laravel Chiselとは [Laravel Chisel](https://github.com/laravel/chisel) は、Laravelスターターキットから不要な機能を削除するための**ポストインストールスクリプト**を構築するためのプリミティブを提供するパッケージです。 スターターキットには多くの機能があらかじめ含まれていますが、プロジェクトによっては一部の機能が不要な場合があります。Chiselはこの問題を、インストール後に対話的に「必要な機能だけを残す」仕組みを提供することで解決します。 ```mermaid theme={null} graph TD A["laravel new でプロジェクト作成"] --> B["スターターキットのインストール"] B --> C["chisel.php スクリプトの実行"] C --> D{"ユーザーへの質問
    (multiselect)"} D --> E["選択に基づいて
    不要ファイルを削除・編集"] E --> F["最終的なプロジェクト構成"] ``` ## なぜ必要か Laravelスターターキットは多くの機能をバンドルしていますが、すべてのプロジェクトですべての機能が必要なわけではありません。例えば: * メール認証が不要なプロジェクト * パスキー認証を使わないプロジェクト * 特定のLivewireコンポーネントが不要な構成 これまでは不要な機能を手動で削除する必要がありましたが、Chiselを使えば**インストール時の対話的なプロンプトでカスタマイズが完結**します。 ## chisel.php スクリプトの定義 Chiselを使うスターターキットは、プロジェクトルートに `chisel.php` ファイルを配置します。このファイルが「どの機能をオプションにするか」を定義します。 ```php theme={null} questions([ Question::multiselect( name: 'auth_features', label: 'Which authentication features would you like to enable?', options: [ 'email-verification' => 'Email verification', ], hint: 'Use space to select, enter to confirm.', ), ]) ->selected('auth_features', 'email-verification', then: function (Chisel $c) { // 選択された場合: セクションマーカーを除去してコードを残す $c->files( 'resources/js/pages/settings/profile.tsx', 'app/Providers/FortifyServiceProvider.php', )->removeSectionMarkers('email-verification'); }, else: function (Chisel $c) { // 選択されなかった場合: 関連ファイルと機能を削除 $c->php('app/Models/User.php') ->removeImport('Illuminate\Contracts\Auth\MustVerifyEmail') ->removeInterface('MustVerifyEmail'); $c->file('config/fortify.php')->removeLinesContaining('Features::emailVerification()'); $c->files( 'app/Providers/FortifyServiceProvider.php', 'resources/js/pages/settings/profile.tsx', )->removeSection('email-verification'); $c->files( 'resources/js/components/email-verification-notice.tsx', 'resources/js/pages/auth/verify-email.tsx', 'tests/Feature/Auth/EmailVerificationTest.php', 'tests/Feature/Auth/VerificationNotificationTest.php', )->delete(); }, ); ``` ## スクリプト定義メソッド | メソッド | 説明 | | - | - | | `Chisel::script($directory)` | スクリプト定義を作成 | | `Question::multiselect(...)` | 複数選択の質問を定義 | | `questions([...])` | スクリプトの質問を設定 | | `questions()` | 登録された質問を取得 | | `collectAnswers()` | 質問への回答収集を開始 | | `apply($callback)` | 無条件のミューテーションステップを登録 | | `selected($key, $value, then:, else:)` | 単一の選択値で分岐 | | `selectedAny($key, $values, then:, else:)` | いずれかの値が選択された場合に分岐 | | `selectedAll($key, $values, then:, else:)` | すべての値が選択された場合に分岐 | | `chisel($answers)` | 登録されたミューテーションを実行 | ## 対話的な回答収集 `collectAnswers()` は回答コレクターを返します。すべてのメソッドはフルエントで任意の順序で呼び出せます。非インタラクティブ環境ではデフォルト値が自動的に使用されます。 外部のArtisanコマンドが[Laravel Prompts](https://laravel.com/docs/prompts)を使ってプロンプトを表示し、回答をChiselに渡します。 ```php theme={null} use Illuminate\Console\Command; use Laravel\Chisel\Chisel; use Laravel\Chisel\Question; use RuntimeException; use function Laravel\Prompts\multiselect; class InstallFeatures extends Command { protected $signature = 'install:features {--answers= : JSON string of answers to skip interactive prompts}'; public function handle(): void { $script = require base_path('chisel.php'); $providedAnswers = $this->option('answers') === null ? [] : json_decode((string) $this->option('answers'), true, 512, JSON_THROW_ON_ERROR); $answers = $script ->collectAnswers() ->onQuestion(fn (Question $question) => match ($question->type) { 'multiselect' => multiselect( label: $question->label, options: $question->options, default: $question->default ?? [], required: $question->required, hint: $question->hint, ), default => throw new RuntimeException("Unsupported question type [{$question->type}]."), }) ->interactive($this->input->isInteractive()) ->withAnswers($providedAnswers); $script->chisel($answers); $chisel = Chisel::in(base_path()); $chisel->npm()->install(); $chisel->npm()->run('build'); } } ``` | メソッド | 説明 | | - | - | | `onQuestion($callback)` | 質問プロンプトのハンドラを設定 | | `interactive($interactive)` | インタラクティブモードを設定 | | `withAnswers($answers)` | 事前収集した回答を提供してプロンプトをスキップ | ## ファイルミューテーション `file($path)` で単一ファイルを、`files(...$paths)` で複数ファイルを対象にします。 | メソッド | 説明 | | - | - | | `replace($search, $replace)` | 文字列を置換 | | `removeLinesContaining($content)` | 指定文字列を含む行を削除 | | `removeSectionMarkers($tag)` | セクションマーカーを除去し、コンテンツを残す | | `removeSection($tag)` | セクションマーカーとその中のコンテンツを削除 | | `delete()` | 対象ファイルを削除 | ## PHP ASTベース編集 `php($path)` はPHP ASTを使った編集を提供します。オブジェクトが破棄されるときに変更が自動保存されます。 PHP AST(抽象構文木)の基礎と `nikic/php-parser` の使い方については[こちら](/jp/advanced/php-ast)で解説しています。 | メソッド | 説明 | | - | - | | `removeImport($class)` | `use` 文を削除 | | `removeTrait($trait)` | クラスからtraitの使用を削除 | | `removeInterface($interface)` | 実装されたinterfaceを削除 | ## Section Markers オプションのコードをコメントペアで囲みます。 ```php theme={null} /* @chisel-passkeys */ Fortify::authenticateUsingPasskeys(); /* @end-chisel-passkeys */ ``` JSXファイルでは波括弧(`{}`)を使ったブロックコメントを使用します。 ```tsx theme={null} { /* @chisel-passkeys */ } ; { /* @end-chisel-passkeys */ } ``` * `removeSectionMarkers('passkeys')` — マーカーを削除し、コードは残す * `removeSection('passkeys')` — マーカーもコードも両方削除 `chisel-` プレフィックスは自動的に付加されます。 ```mermaid theme={null} graph LR A["/* @chisel-feature */
    コード
    /* @end-chisel-feature */"] --> B{"ユーザーが
    featureを選択?"} B -->|removeSectionMarkers| C["コードのみ残す
    (マーカーを削除)"] B -->|removeSection| D["マーカーもコードも
    すべて削除"] ``` ## npm サポート | メソッド | 説明 | | - | - | | `npm()->install()` | 検出されたパッケージマネージャーで依存関係をインストール | | `npm()->run($script, ...$arguments)` | パッケージマネージャーのスクリプトを実行 | | `npm()->remove(...$packages)` | 検出されたパッケージマネージャーでパッケージを削除 | `npm()` メソッドはnpm・yarn・pnpm・bunを自動検出します。 ## 現在の開発状況 * **GitHubリポジトリ**: [laravel/chisel](https://github.com/laravel/chisel) * **リリース**: v0.1 (2026年5月公開) * **開発期間**: 3ヶ月間の非公開開発を経て公開 ChiselはエンドユーザーがLaravelアプリに直接インストールするパッケージではなく、**スターターキットが内部的に使用するライブラリ**です。将来的にはLaravelの公式スターターキットがChiselを活用したポストインストールスクリプトを提供することが期待されています。 ソースコードと最新のAPIリファレンスはこちら。 スターターキット自体の使い方については公式ドキュメントを参照してください。 # cpx 2.0 — Composer Package Executor がフルリライト Source: https://kawax.biz/jp/blog/cpx-introduction laravel/cpxの紹介。npxのようにComposerパッケージをインストールせずに実行できるツール。v2.0で自己完結型PHARとしてフルリライトされ、ローカルバイナリ優先実行やGist実行、エイリアス機能などが追加されました。 この記事は[laravel/cpx](https://github.com/laravel/cpx)のREADME.mdとUPGRADE.mdに基づく情報です。v2.0.0(2026年7月24日リリース)時点の内容です。 ## cpxとは [cpx](https://github.com/laravel/cpx) は、Composerパッケージに含まれるコマンドをインストールせずに実行できるツールです。npmにおける`npx`のComposer版と考えると分かりやすいです。 ```bash theme={null} composer global require cpx/cpx cpx friendsofphp/php-cs-fixer fix ./src ``` 内部的にはパッケージを分離ディレクトリにインストールして実行するため、プロジェクトやグローバルのComposer依存関係と衝突しません。2回目以降の実行は同じインストール済みパッケージを再利用するため高速です。 ## なぜ必要か `composer global require` で個別ツールをインストールする方法には以下の課題があります。 * グローバル依存関係同士が競合する(`nikic/php-parser`や`symfony/console`など共通依存を使うツールで発生しやすい) * 同じツールの異なるバージョンをプロジェクトごとに切り替えたい * 長期運用時にグローバルパッケージの更新を忘れる * 一度しか使わないツールをグローバルインストールしたくない cpx自体はPHARとして自己完結しており、ランタイムのComposer依存を持たないため、グローバルインストールしても競合の原因になりません。 ## v2.0での全面リライト cpxは v2.0 で全面的に書き直され、**自己完結型のPHAR**として配布されるようになりました。基本的な使い方(`cpx [arguments]`)や `~/.cpx` キャッシュディレクトリはそのまま維持されています。1.xは開発が凍結されており、バグ修正・セキュリティ修正を含め今後のリリースはありません。 アップグレードはグローバルインストールし直すだけです。 ```bash theme={null} composer global require cpx/cpx:^2.0 ``` v2.0はPHP 8.3以上が必要です。 ## v2.0の新機能 ### ローカルバイナリの優先実行 npxと同様に、プロジェクトに既にインストールされているバイナリがあれば、分離コピーをインストールする前にそちらを優先して実行します。カレントディレクトリから上に遡って最も近いComposerプロジェクトを探し、`vendor/bin`(デフォルトの`bin-dir`)内の一致するバイナリを実行します。 ```bash theme={null} cpx pint # vendor/bin/pint が存在すればそれを実行 cpx phpunit --filter=Foo # vendor/bin/phpunit が存在すればそれを実行 cpx laravel/pint:^2.0 # インストール済みバージョンが制約を満たす場合のみローカルを使用 ``` 分離コピーを強制したい場合は `--skip-local` を使います。 ```bash theme={null} cpx --skip-local laravel/pint --version ``` ### ローカルパッケージディレクトリの直接実行 開発中のComposerパッケージのディレクトリを直接指定して、そのバイナリを実行できます。 ```bash theme={null} cpx /absolute/path/to/package --version cpx ../path/to/package --version ``` 対象ディレクトリには有効な `composer.json` が必要で、依存関係は事前に `vendor/autoload.php` にインストールされている必要があります。cpxはこの場合、Composerの実行やパッケージのコピー・キャッシュを行いません。 ### cpx exec と cpx tinker PHPコードを素早く実行する複数の方法が用意されています。 | コマンド | 説明 | | - | - | | `cpx exec ` | PHPファイルを実行(ファイル実行はこの方法のみ) | | `cpx exec -r ` | PHPコードを直接実行 | | `cpx exec ` | GitHub Gistをダウンロードして実行 | | `cpx tinker` | インタラクティブREPLを開く | Laravelプロジェクト内では `cpx exec` がconfig・facade・`.env`を含めてフルにアプリケーションをブート(`$app`が使用可能)し、`cpx tinker` はプロジェクト自身の `php artisan tinker` を実行します(`laravel/tinker`がインストールされている場合)。それ以外の場所ではPsySHシェルが起動します。 スクリプト内では `cpx_require('vendor/package')`(1.xの`composer_require()`から改名)でパッケージをその場でオートロードできます。 ### エイリアス機能 長いパッケージ名を覚えなくて済むよう、独自のショートカットを定義できます。 ```bash theme={null} cpx alias laravel/pint pint cpx aliases # 定義済みエイリアスの一覧 cpx unalias pint # エイリアスの削除 ``` 1.xに組み込まれていたポピュラーパッケージ向けのビルトインエイリアス一覧は廃止されたため、必要なら自分で定義する必要があります。 ### 非対話モードとJSON出力 cpxは[laravel/agent-detector](https://github.com/laravel/agent-detector)によるAIエージェント検出、標準入力のリダイレクト、`--no-interaction`/`-n` フラグから非対話環境を自動検出します。非対話モードでは、`installed`・`aliases`・`alias`・`unalias`・`clean`・`update` などcpx自身の管理コマンドが整形テキストの代わりに1行のJSONを返します。対話端末からでも `--json` を渡せば同じ出力を得られます。 ## v1.xからの主な変更点(コマンド) | 1.x | 2.x | | - | - | | `cpx list`(インストール済み一覧) | `cpx installed` に変更(`list`はコマンド一覧を表示) | | ビルトインのエイリアス一覧 | 廃止。`cpx alias`で自分で定義 | | `cpx check` / `cpx format` / `cpx test` | 廃止。`cpx pint`・`cpx phpstan`・`cpx pest`など直接実行 | | `cpx version` / `-v` | 廃止。`cpx --version`を使用 | | `cpx script.php`(ファイル直接実行) | 廃止。`cpx exec script.php`が必要 | | `composer_require()` | `cpx_require()`に改名 | ## 関連ページ `~/.cpx`ディレクトリのキャッシュや旧バージョンパッケージは自動的に新形式へ移行されますが、バージョン制約付きで実行していたパッケージは新しいディレクトリ名で管理されるため初回のみ再インストールされます。不要になった1.x時代のコピーは `cpx clean` で削除できます。 ソースコードと最新のREADMEはこちら。 1.xから2.xへの詳細な移行手順。 # Inertia.jsでSPAを構築する Source: https://kawax.biz/jp/blog/inertia-introduction LaravelバックエンドとVue/React/SvelteフロントエンドをつなぐInertia.js v3を紹介します。APIなしでSPAを実現する仕組みから、ページコンポーネント・共有データ・フォーム処理まで実践的に解説します。 ## Inertia.jsとは フロントエンドをReactやVueで作りたい。でもAPIを別途設計・実装・管理するのは避けたい——そんなジレンマを解決するのが **Inertia.js** です。 Inertia はサーバーサイドのルーティングとコントローラーをそのままに、フロントエンドだけをReact・Vue・Svelteで書けるようにする「グルー(接着剤)」です。フレームワークではなく、既存のLaravelとJavaScriptフレームワークをつなぐアダプター層として機能します。 現在の最新バージョンはInertia v3(2026年3月26日リリース)です。Laravel 13のスターターキット(React・Vue・Svelte)はInertia対応済みです。 ### 従来のSPA・MPAとの違い | アーキテクチャ | 特徴 | 課題 | | - | - | - | | MPA(従来のBladeアプリ) | シンプル、Laravelとの統合が容易 | ページ遷移のたびにフルリロード | | SPA(API + フロントエンド分離) | 高いインタラクション性 | API設計・認証・型定義の二重管理 | | **Inertia(モダンモノリス)** | SPAのUX + サーバー側のシンプルさ | 独自の学習コストがある | Inertia を使うと、Laravelのコントローラーから直接VueコンポーネントやReactコンポーネントにデータを渡せます。REST APIを定義する必要はなく、ページ遷移もブラウザのフルリロードではなくXHRで行われるため、SPA的なスムーズな操作感を実現できます。 *** ## Laravelスターターキットとの関係 `laravel new` でプロジェクトを作成する際にReact・Vue・Svelteを選ぶと、Inertiaが自動的にセットアップされます。 ```shell theme={null} laravel new my-app # 対話式プロンプトでReact / Vue / Svelteを選ぶとInertia構成になる ``` スターターキットでは以下の構成が自動で用意されます。 * `inertiajs/inertia-laravel`(サーバーサイドアダプター) * `@inertiajs/react` / `@inertiajs/vue3` / `@inertiajs/svelte`(クライアントアダプター) * `HandleInertiaRequests` ミドルウェア * ログイン・登録・パスワードリセットなどの認証画面(すでにInertiaコンポーネントで実装済み) 既存プロジェクトに手動でInertiaを導入する場合はサーバーサイドとクライアントサイドを別々にインストールします。詳細は公式ドキュメントのインストール手順を参照してください。 *** ## `Inertia::render()` の基本 Laravelのコントローラーから Inertia レスポンスを返すには `Inertia::render()` を使います。第一引数にJavaScriptコンポーネント名、第二引数にプロップとして渡すデータを指定します。 ```php theme={null} use Inertia\Inertia; class PostController extends Controller { public function index() { return Inertia::render('Posts/Index', [ 'posts' => Post::latest()->paginate(10), ]); } public function show(Post $post) { return Inertia::render('Posts/Show', [ 'post' => $post->only('id', 'title', 'content', 'created_at'), 'author' => $post->user->only('id', 'name'), ]); } } ``` `Inertia::render()` の代わりに `inertia()` ヘルパー関数も使えます。どちらを使うかはチームのスタイルで統一してください。 コンポーネント名 `'Posts/Index'` はファイルパスに対応します。Reactなら `resources/js/Pages/Posts/Index.jsx`、Vueなら `resources/js/Pages/Posts/Index.vue` が対応するコンポーネントです。 *** ## ページコンポーネントの構造 Inertiaのページコンポーネントは通常のVue/Reactコンポーネントです。コントローラーから渡したデータがプロップとして受け取れます。 ```jsx React theme={null} // resources/js/Pages/Posts/Index.jsx import { Link } from '@inertiajs/react' export default function PostsIndex({ posts }) { return (

    投稿一覧

    {posts.data.map(post => (

    {post.title}

    ))}
    ) } ``` ```vue Vue theme={null} ``` `` コンポーネントを使うと、ページ遷移がXHRで行われ、フルページリロードを回避できます。通常の `` タグとまったく同じように書けますが、裏側でInertiaがページコンポーネントだけを差し替えます。 *** ## 共有データ — `HandleInertiaRequests` ミドルウェア すべてのページで共通して必要なデータ(ログイン中のユーザー情報・フラッシュメッセージなど)は、`HandleInertiaRequests` ミドルウェアの `share()` メソッドで定義します。 ```php theme={null} // app/Http/Middleware/HandleInertiaRequests.php use Illuminate\Http\Request; use Inertia\Middleware; class HandleInertiaRequests extends Middleware { public function share(Request $request): array { return array_merge(parent::share($request), [ 'appName' => config('app.name'), 'auth' => [ 'user' => $request->user() ? $request->user()->only('id', 'name', 'email') : null, ], 'flash' => [ 'success' => $request->session()->get('success'), 'error' => $request->session()->get('error'), ], ]); } } ``` 共有データは自動的にすべてのページのプロップにマージされます。 ```jsx React theme={null} // レイアウトコンポーネントからアクセスする例 import { usePage } from '@inertiajs/react' export default function Layout({ children }) { const { auth, flash } = usePage().props return (
    {auth.user ? `ログイン中: ${auth.user.name}` : 'ゲスト'}
    {flash.success &&
    {flash.success}
    }
    {children}
    ) } ``` 共有データはすべてのリクエストに含まれるため、必要最低限のデータに絞ることを推奨します。`fn()` を使ったレイジー評価にすると、実際に必要なリクエストでのみ評価されます。 *** ## フォーム送信とバリデーションエラーの処理 Inertiaでのフォーム処理はLaravelのバリデーションと自然に統合されます。`useForm()` ヘルパーを使うと、フォームの状態管理・送信・エラー表示がシンプルに実装できます。 ### コントローラー側 ```php theme={null} class PostController extends Controller { public function store(Request $request) { $validated = $request->validate([ 'title' => ['required', 'string', 'max:255'], 'content' => ['required', 'string'], ]); Post::create($validated + ['user_id' => auth()->id()]); return redirect()->route('posts.index') ->with('success', '投稿を作成しました。'); } } ``` バリデーションエラーが発生した場合、Laravelは自動的にフォームページにリダイレクトし、エラー情報をセッションに保存します。Inertiaはこれを自動的に検知して `errors` プロップとしてページに渡します。 ### フロントエンド側 ```jsx React theme={null} // resources/js/Pages/Posts/Create.jsx import { useForm } from '@inertiajs/react' export default function PostsCreate() { const { data, setData, post, processing, errors } = useForm({ title: '', content: '', }) function handleSubmit(e) { e.preventDefault() post('/posts') } return (
    setData('title', e.target.value)} /> {errors.title &&

    {errors.title}

    }

    {{ form.errors.content }}

    ``` バリデーションエラーが返ったとき、`useForm()` は入力内容を保持したままエラーを表示します。フォームを再入力させる手間がなく、ユーザー体験が向上します。 *** ## 向いているユースケース・向いていないケース ### Inertiaが向いているケース * Laravelをよく知っているチームがSPA的なUIを作りたいとき * 認証・認可・バリデーションをLaravel側で一元管理したいとき * 管理画面・社内ツールなど、SEOの優先度が低いアプリ * APIを別途設計・管理するコストを避けたいプロジェクト ### 向いていないケース * 外部の複数クライアント(モバイルアプリなど)が同じAPIを使う場合 * SEOが非常に重要なコンテンツサイト(SSRで対応可能だが複雑になる) * マイクロフロントエンド構成など、フロントエンドが完全に独立したチームで開発される場合 Inertia はサーバーサイドレンダリング(SSR)もサポートしています。SEOが必要なページがある場合は SSR オプションを検討してください。Laravel のスターターキットでも SSR 設定が組み込まれています。 *** ## Inertia v3 の主な変更点 Inertia v3 は2026年3月26日にリリースされました。v2からの主な変更点を紹介します。 ### Axios を廃止 — 軽量な組み込みXHRクライアントへ v3 では Axios が廃止され、より軽量な組み込みXHRクライアントに置き換えられました。ほとんどのアプリケーションでコード変更は不要です。Axios のインターセプターは組み込みインターセプターに直接移行できます。引き続き Axios を使いたい場合は Axios アダプター経由で利用できます。 ### `@inertiajs/vite` プラグインによるSSRのシンプル化 新しい Vite プラグインを導入すると、ページコンポーネントの自動解決とSSR設定が大幅に簡略化されます。開発中のSSRは `npm run dev` を実行するだけで動作するようになり、別途Nodeサーバーを起動する必要がなくなりました。 ```bash theme={null} npm install @inertiajs/vite@^3.0 ``` ### `useHttp` フック — ページ遷移を伴わないHTTPリクエスト `useHttp` フックを使うと、ページ遷移(Inertia visit)を発生させずにサーバーへHTTPリクエストを送れます。モーダルの保存やバックグラウンド処理など、現在のページを維持したまま通信したい場面で役立ちます。 ### 楽観的UI更新(Optimistic Updates) `useForm` およびルーターレベルで楽観的な更新がサポートされました。サーバーの応答を待たずにUIを即座に更新し、失敗した場合は自動的にロールバックします。 ### レイアウトプロップ(Layout Props) `useLayoutProps` フックを使うと、ページコンポーネントから永続レイアウトへデータを渡せるようになりました。従来は共有データかページプロップに頼るしかなかった「特定ページのレイアウト状態制御」がシンプルに書けます。 ### 要件の変更 v3 では以下の最低バージョンが引き上げられました。 | 項目 | v2 | v3 | | - | - | - | | PHP | 8.1+ | 8.2+ | | Laravel | 10+ | 11+ | | React | 18+ | 19+ | | Svelte | 4+ | 5+ | ### `Inertia::lazy()` の廃止 v2 で非推奨になっていた `Inertia::lazy()` が v3 で完全に削除されました。代わりに `Inertia::optional()` を使います。 ```php theme={null} // v2(非推奨) 'users' => Inertia::lazy(fn () => User::all()), // v3 'users' => Inertia::optional(fn () => User::all()), ``` ### イベント名の変更 | v2 | v3 | | - | - | | `invalid` | `httpException` | | `exception` | `networkError` | ### `future` オプションの廃止 v2 で実験的オプションとして提供されていた `future` 設定ブロックが廃止されました。これらのオプションはすべてデフォルトで有効になっています。`createInertiaApp` の設定から `future` ブロックを削除してください。 v2 からのアップグレード手順の詳細は
    公式アップグレードガイド を参照してください。v2 は2026年9月26日までバグ修正が、2027年3月26日までセキュリティ修正が提供されます。 *** ## まとめ Inertia.js は「LaravelはそのままでフロントエンドをReact/Vueで書きたい」という現実的なニーズに応えるツールです。APIを設計せずに、コントローラーから直接コンポーネントにデータを渡せるシンプルさが最大の強みです。 Laravel 13 のスターターキットが Inertia に対応していることからも、Laravel エコシステムにおける Inertia の位置づけは確立されています。Livewire がPHPだけで動的UIを作るアプローチなら、Inertia はJavaScriptフレームワークのパワーをサーバー側のシンプルさを損なわずに使えるアプローチです。 どちらを選ぶかはチームのスキルセットとプロジェクトの要件次第ですが、「LaravelのコントローラーをそのままにReactで画面を作りたい」という場面では Inertia が最も自然な選択です。 Inertia v3の全機能については公式ドキュメントを参照してください。 # Laravel 13 新機能まとめ Source: https://kawax.biz/jp/blog/laravel-13-new-features 2026年3月にリリースされたLaravel 13の主要な新機能と改善点を解説します。 ## はじめに Laravel 13 は 2026年3月17日にリリースされました。毎年恒例のメジャーリリースですが、Laravel 13 は特に **AI ネイティブなワークフロー**、**より安全なデフォルト**、**より表現力豊かな開発者向け API** に焦点を当てています。 破壊的変更は最小限に抑えられており、ほとんどのアプリケーションは少ない修正でアップグレードできます。一方で、Laravel AI SDK やセマンティック検索など、モダンなアプリケーション開発を大きく変える新機能が多数追加されています。 Laravel 13 のサポートポリシー: バグフィックスは 2027年 Q3 まで、セキュリティフィックスは **2028年3月17日** まで提供されます。 *** ## PHP 要件の更新 Laravel 13 では **PHP 8.3 以上** が必要です。PHP 8.2 のサポートは終了しています。 | バージョン | PHP | リリース日 | バグフィックス期限 | セキュリティフィックス期限 | | - | - | - | - | - | | 11 | 8.2 - 8.4 | 2024年3月12日 | 2025年9月3日 | 2026年3月12日 | | 12 | 8.2 - 8.5 | 2025年2月24日 | 2026年8月13日 | 2027年2月24日 | | **13** | **8.3 - 8.5** | **2026年3月17日** | **2027年 Q3** | **2028年3月17日** | PHP 8.3 の主な恩恵として、型付き定数、`json_validate()` 関数、`#[\Override]` アトリビュートなどが利用できるようになります。 *** ## 主要な新機能 ### Laravel AI SDK Laravel 13 の目玉機能は **ファーストパーティの AI SDK** です。テキスト生成、ツール呼び出しエージェント、埋め込み、音声、画像生成、ベクターストア統合を統一した API で提供します。 プロバイダーに依存しない形で AI 機能を構築しながら、Laravel らしい開発者体験を保てます。 **テキスト生成 (エージェント)** ```php theme={null} use App\Ai\Agents\SalesCoach; $response = SalesCoach::make()->prompt('このセールストランスクリプトを分析してください...'); return (string) $response; ``` **画像生成** ```php theme={null} use Laravel\Ai\Image; $image = Image::of('キッチンカウンターの上のドーナツ')->generate(); $rawContent = (string) $image; ``` **音声合成** ```php theme={null} use Laravel\Ai\Audio; $audio = Audio::of('Laravelでのコーディングが大好きです。')->generate(); $rawContent = (string) $audio; ``` **埋め込み生成** ```php theme={null} use Illuminate\Support\Str; $embeddings = Str::of('ナパバレーのワインは最高です。')->toEmbeddings(); ``` 詳細は [Laravel AI SDK ドキュメント](https://laravel.com/ai) を参照してください。 *** ### セマンティック / ベクター検索 AI SDK と連携したネイティブなベクタークエリサポートが追加されました。PostgreSQL + `pgvector` を使ったセマンティック検索を、クエリビルダーから直接実行できます。 ```php theme={null} $documents = DB::table('documents') ->whereVectorSimilarTo('embedding', 'ナパバレーの最高のワイナリー') ->limit(10) ->get(); ``` 埋め込み列の追加からクエリまで、Laravel のエコシステム内で完結できます。 *** ### JSON:API リソース Laravel にファーストパーティの [JSON:API](https://jsonapi.org/) リソースサポートが追加されました。JSON:API 仕様に準拠したレスポンスを簡単に返せるようになります。 * リソースオブジェクトのシリアライゼーション * リレーションシップのインクルード * スパースフィールドセット * リンク * JSON:API 準拠のレスポンスヘッダー *** ### PHP アトリビュートの拡張 Laravel 13 は PHP アトリビュートによる宣言的な設定をフレームワーク全体に拡大しました。 **コントローラーへの適用** ```php theme={null} withoutMiddleware([PreventRequestForgery::class]); ``` `VerifyCsrfToken` は非推奨のエイリアスとして残りますが、新しいクラス名への移行が推奨されます。 ### キャッシュのデシリアライゼーション制限 `config/cache.php` に `serializable_classes` オプションが追加され、デフォルトは `false` です。これにより `APP_KEY` が漏洩した場合の PHP デシリアライゼーション攻撃を防ぎます。 ```php theme={null} // config/cache.php 'serializable_classes' => [ App\Data\CachedDashboardStats::class, App\Support\CachedPricingSnapshot::class, ], ``` キャッシュに PHP オブジェクトを保存している場合は、明示的にクラスを許可リストに追加する必要があります。 *** ## スターターキットの変更 Laravel 12 で刷新されたスターターキット(React/Vue/Livewire ベース)は Laravel 13 でも引き続き利用可能です。 Laravel 13 のリリース時期に合わせて **Inertia v3** もリリースされました。主な変更点: * よりシンプルなレイアウト props * Vite 8 サポート * `withApp` コールバックの追加 * 新しい Blade コンポーネント Inertia v3 を使用している場合は、合わせてアップグレードを検討してください。 *** ## その他の改善点 ### AI アシストアップグレード (Laravel Boost) [Laravel Boost](https://github.com/laravel/boost) はファーストパーティの MCP サーバーです。Claude Code、Cursor、OpenCode、Gemini、VS Code などの AI エディターと連携して、`/upgrade-laravel-v13` スラッシュコマンドでアップグレードを半自動化できます。 ```shell theme={null} composer require laravel/boost:^2.0 --dev ``` ### コントラクトの追加 以下のコントラクトに新しいメソッドが追加されました: | コントラクト | 追加メソッド | | - | - | | `Bus\Dispatcher` | `dispatchAfterResponse` | | `Routing\ResponseFactory` | `eventStream` (SSE サポート) | | `Auth\MustVerifyEmail` | `markEmailAsUnverified` | | `Queue\Queue` | `pendingSize`, `delayedSize`, `reservedSize`, `creationTimeOfOldestPendingJob` | | `Cache\Store` / `Cache\Repository` | `touch` | *** ## まとめ Laravel 13 は「少ない破壊的変更、多くの新機能」というリリースです。アップグレード作業は最小限で済む一方、特に **AI SDK** と **セマンティック検索** は現代のアプリケーション開発のあり方を変える可能性を持っています。 既存のアプリケーションを Laravel 12 から 13 へアップグレードする準備ができたら、詳細な手順は以下のガイドを参照してください。 破壊的変更の一覧と、ステップバイステップのアップグレード手順を解説しています。 # Laravel Cloud — Laravel専用PaaSの全貌 Source: https://kawax.biz/jp/blog/laravel-cloud 2025年に登場したLaravel Cloud。Forgeとの違い、得意なこと・苦手なこと、実際に使うメリットを解説する。 ## Laravel Cloudとは [Laravel Cloud](https://cloud.laravel.com/docs/intro) は、Laravel公式の**フルマネージドPaaS**です。\ アプリのデプロイ、スケーリング、データベース、キャッシュ、WebSocket基盤まで、Laravel向けに統合されています。 Cloudの[Changelog](https://cloud.laravel.com/docs/changelog)では、`February 24, 2025` に **Launched Laravel Cloud!** と記録されています。2025年に一般提供が始まった比較的新しいサービスです。 *** ## Forgeとの違い Laravel CloudとLaravel Forgeは、どちらもLaravel開発者向けですが、レイヤーが違います。 | 観点 | Laravel Cloud | Laravel Forge | | - | - | - | | 立ち位置 | フルマネージドPaaS | サーバー管理ツール | | 主な操作 | 環境・リソースを選んでデプロイ | VPS / EC2 などをプロビジョニングして運用 | | インフラ管理 | 最小限(Cloud側が管理) | OS・ミドルウェア運用の責任はユーザー側 | | root権限 | なし(OSレベルの直接操作は想定しない) | あり(サーバー運用を柔軟に制御可能) | 「インフラを意識せずLaravelアプリを早く出す」ならCloud、\ 「サーバーを細かく制御したい」ならForgeが向いています。 *** ## Laravel Cloudが得意なこと Laravel Cloudは、Laravelの実運用で使う機能をダッシュボード中心で有効化しやすい点が強みです。 ### Task Scheduler [Scheduled Tasks](https://cloud.laravel.com/docs/scheduled-tasks) で、AppクラスタまたはWorkerクラスタに **Scheduler** を有効化するだけで `schedule:run` が毎分実行されます。 ### Managed Queues(マネージドキュー) [Queues](https://cloud.laravel.com/docs/queues) では、**Managed Queues** が推奨されています。Laravel Cloud がキューのプロビジョニング、ワーカーの起動、スケーリングを自動で処理し、ジョブがない時はワーカー数がゼロまで下がるため、処理中だけ課金されます。 | 選択肢 | 向いている用途 | スケーリング | 料金の考え方 | | - | - | - | - | | **Managed Queues** | 本番・プレビュー・開発環境全般。まず最初に選ぶ構成 | キューの滞留状況に応じて自動。アイドル時はゼロまで縮退 | ワーカー稼働秒数 + キュー操作 | | **Worker cluster** | `queue:work` を自前管理したい、大きめのバックグラウンド処理 | CPU / メモリベース | Worker cluster の通常料金 | | **App cluster のバックグラウンドプロセス** | 小規模開発や低頻度ジョブ | CPU / メモリベース | App cluster に含まれる | Managed Queues は、Webトラフィックとジョブ処理を分離しつつ、失敗ジョブのダッシュボードも使えます。\ 一方で `database` や `redis` など別ドライバを自前管理したいなら Worker cluster、アプリと同じコンピュートで十分なら App cluster のバックグラウンドプロセスを使います。 環境のインフラキャンバスダッシュボードで **New Managed Queue** をクリックします。 キュー名、インスタンスサイズ、ポーリング間隔、最大ワーカー数などを設定します。 設定を保存して再デプロイすると、Laravel Cloud がキューとワーカープールを自動で用意します。 ジョブがない時は Managed Queues のワーカーがゼロまで下がるため、後述の Scale to Zero と組み合わせた構成と相性が良いです。App cluster 側を眠らせても、キュー処理だけ必要な時に自動で起動できます。 ### Scale to Zero(Flex コンピュート) [Compute](https://cloud.laravel.com/docs/compute#scale-to-zero) にある **Flex** は、低コストで柔軟に使える compute class です。新しい **Scale to Zero** は、この Flex コンピュートを旧 Hibernation から発展させた仕組みです。新しい **Flex コンピュート** では、HTTP リクエスト到着時の起動が **500ms 未満** まで短縮され、従来の 5〜20 秒待つ前提ではなくなりました。 有効化は App cluster の設定画面で **Scale to Zero** トグルをオンにし、保存後に再デプロイするだけです。 さらに、Scale to Zero が有効な環境では、スリープ中でも Scheduled Tasks とキュー処理のために環境が自動起動します。\ App cluster 上で `schedule:run` を回しつつ、ジョブ処理は Managed Queues に任せる構成が、現在の公式ドキュメントでも最も扱いやすい組み合わせです。 App cluster 上の自前 `queue:work` は sleep timeout をまたぐと中断される可能性があります。キュー処理を確実に続けたいなら、公式ドキュメント通り Managed Queues を優先してください。 ### MySQL Scale to Zero [Laravel MySQL](https://cloud.laravel.com/docs/resources/databases/laravel-mysql#scale-to-zero) の **Flex** サイズも Scale to Zero に対応しました。データベースが一定時間(1分〜1時間で設定可能なアイドルタイムアウト)接続を受け取らないとスリープし、コンピュート課金が止まります。ストレージは常時オンラインのままなので、スリープ中もデータは保持され、ストレージ分の課金のみ継続します。 次の接続が来ると自動的に起動します。すべての Laravel MySQL クラスタはプロキシ層でフロントされているため、起動中の接続はプロキシで保持されてからデータベースへ転送されます。接続エラーにはならず、最初のクエリが数百ミリ秒ほど遅くなる程度です(メモリサイズが大きいほど起動に時間がかかる傾向)。自動バックアップや手動スナップショットの際も一時的に起動し、完了後は再びスリープします。 現時点では US East (N. Virginia) と US East (Ohio) の Flex サイズで利用可能で、他リージョンは順次対応予定です。既存のデータベースは現在のサイズ・料金のまま変わらず稼働し続けます。**Pro** サイズは常時稼働で Scale to Zero の対象外です。 Scale to Zero 対応のFlexデータベースは、複数アプリで共有している、または1日中アクセスがあるデータベースではスリープしないため恩恵が薄くなります。既存のMySQLが安定して稼働しているなら、Scale to Zero対応版に変更すると最低料金がわずかに上がる($5.5 → $6.6)ケースもあるため、ワークロードに応じて判断してください。新規作成するMySQLはすでにScale to Zero対応版のみが選択肢になります。 [Compute の Scale to Zero](https://cloud.laravel.com/docs/compute#scale-to-zero)、[Laravel Valkey の Scale to Zero](https://cloud.laravel.com/docs/resources/caches/valkey#scale-to-zero) と組み合わせることで、開発環境やサイドプロジェクト、さらにはアクセスに波がある本番環境でもコストを最小化できます。 ### Laravel Octane [Compute](https://cloud.laravel.com/docs/compute#laravel-octane) の **Use Octane as runtime** を有効化すると、CloudがOctane + FrankenPHP構成で実行します。サーバー設定を手で組み立てる必要がありません。 ### Inertia SSR [Compute](https://cloud.laravel.com/docs/compute#inertia-ssr) の **Use Inertia SSR** を有効化して使えます。スターターキットを使う場合は `npm run build:ssr` が推奨されています。 ### WebSocketサーバー(Reverb) [Laravel Cloud WebSockets](https://cloud.laravel.com/docs/resources/websockets) で、ReverbベースのWebSocketクラスタを作成できます。環境にアタッチすると `REVERB_*` と `VITE_REVERB_*` が自動注入されます。 ### Laravel Nightwatch連携 [Environments](https://cloud.laravel.com/docs/environments#nightwatch-integration) にはワンクリック連携があり、Cloudの[Changelog](https://cloud.laravel.com/docs/changelog)でも `July 1, 2025` に **One-Click Nightwatch Integration** が記録されています。\ 手動構成が必要な場合も [nightwatch-on-cloud](https://cloud.laravel.com/docs/knowledge-base/nightwatch-on-cloud) に手順があります。 *** ## 料金と無料プラン Laravel Cloudの料金は、基本的に**プラン料金 + 使用量課金**です。\ 詳細は常に [Pricing](https://cloud.laravel.com/docs/pricing) を確認してください。 * **Starter**: 月額 `$5`。毎月 `$5` 分の使用クレジット付き。新規サインアップは初月無料 * **Growth**: `$20/mo + usage`、毎月 `$5` 分の使用クレジット付き * **Business**: `$200/mo + usage`、毎月 `$5` 分の使用クレジット付き 以前よりも **Starter** で小規模プロジェクトを始めやすくなりました。\ Scale to Zero と Managed Queues を組み合わせると、待機時間が長い個人開発や社内ツールでもコストを読みやすくなります。 *** ## 実際に使う際の注意点 ### root権限前提の運用はできない CloudはPaaSなので、OSレベルの自由度より運用の簡便さを優先します。\ カーネル設定や独自デーモンの常駐など、root権限前提の作業が必要ならForgeなどの選択肢を検討します。 ### Nightwatchのイベント量は最初に制御する 無料枠や小規模構成でNightwatchを使う場合、イベント量が増えやすいため最初にサンプリング調整を入れるのが安全です。 ```ini theme={null} NIGHTWATCH_REQUEST_SAMPLE_RATE=0.1 NIGHTWATCH_IGNORE_QUERIES=true ``` *** ## 向いているプロジェクト・向いていないプロジェクト ### 向いている * Laravelアプリを最短で本番投入したい * Scheduler / Queue / Octane / Inertia SSR / Reverb をまとめて運用したい * アプリ開発に集中し、サーバー運用コストを下げたい ### 向いていない * root権限でOSレベルまで強くチューニングしたい * 既存の特殊インフラ要件(独自ミドルウェアや特殊ネットワーク制約)が強い *** ## まとめ Laravel Cloudは「Laravelの高度な機能を、インフラ構築の負担を下げて運用したい」チームに強く向いています。\ 特に Scheduler / Queue / Octane / Inertia SSR / Reverb / Nightwatch を組み合わせるプロジェクトでは、立ち上げと運用の初速が大きく上がります。 一方で、root権限が必要な運用を前提にする場合はForgeのほうが適しています。\ まずは無料トライアルで実際のワークロードを流し、運用要件に合うかを確認するのが確実です。 # Laravel Cloud CLI — ターミナルからLaravel Cloudを操作する Source: https://kawax.biz/jp/blog/laravel-cloud-cli laravel/cloud-cliを使ってターミナルからLaravel Cloudのアプリ・環境・データベース・ドメインなどを管理する方法。 ## Laravel Cloud CLIとは `laravel/cloud-cli` は、[Laravel Cloud](https://cloud.laravel.com) をターミナルから操作するためのCLIです。\ このツールは Laravel Zero 製で、ダッシュボードで行う操作を `cloud` コマンドで実行できます。 あなたはこのCLIで、アプリケーション、環境、データベース、キャッシュ、バケット、ドメイン、WebSocketなどを一元管理できます。 ## 要件 導入前に次の要件を満たしてください。 * `PHP 8.2+` * `Composer` * `GitHub CLI (gh)`(インストール済み、認証済み) * `Git` ## インストール まず、CLIをローカルにセットアップしてください。 ```bash theme={null} gh repo clone laravel/cloud-cli cd cloud-cli composer install ``` zsh: ```bash theme={null} echo 'alias cloud="php '$(pwd)'/cloud"' >> ~/.zshrc source ~/.zshrc ``` bash: ```bash theme={null} echo 'alias cloud="php '$(pwd)'/cloud"' >> ~/.bashrc source ~/.bashrc ``` ```bash theme={null} alias cloud="php /path/to/cloud-cli/cloud" ``` ## 認証 多くのコマンドを使う前に認証してください。 ```bash theme={null} cloud auth ``` `cloud auth` はブラウザを開き、OAuthで認証します。\ CI用途などでトークンを管理したい場合は `cloud auth:token` を使います。 ```bash theme={null} cloud auth:token ``` ## リポジトリ設定 あなたのプロジェクトで毎回 `application` や `environment` を選びたくない場合は、`cloud repo:config` を実行してください。 ```bash theme={null} cloud repo:config ``` この設定でGitリポジトリにデフォルト値が保存され、`cloud deploy` などを短いコマンドで実行できます。 ## デプロイ 初回デプロイと継続デプロイを分けて使うと効率的です。 ```bash theme={null} # 初回: ガイド付きでアプリ作成とデプロイ cloud ship # 2回目以降: 既存アプリにデプロイ cloud deploy # ダッシュボードを開く cloud dashboard ``` ## 主なコマンドカテゴリ READMEのコマンドリファレンスから、日常的に使うカテゴリをまとめます。 | カテゴリ | 主なコマンド例 | 用途 | | - | - | - | | Auth / Config | `cloud auth`, `cloud auth:token`, `cloud repo:config` | 認証とデフォルト設定 | | Application | `cloud application:list`, `cloud application:create` | アプリ管理 | | Environment | `cloud environment:list`, `cloud environment:get`, `cloud environment:logs` | 環境情報・ログ確認 | | Deploy | `cloud ship`, `cloud deploy`, `cloud deploy:monitor` | デプロイ管理 | | Database | `cloud database-cluster:*`, `cloud database:*`, `cloud database-snapshot:*` | DBクラスタ・DB・スナップショット管理 | | Cache | `cloud cache:list`, `cloud cache:create`, `cloud cache:types` | キャッシュ管理 | | Bucket | `cloud bucket:*`, `cloud bucket-key:*` | オブジェクトストレージ管理 | | Domain | `cloud domain:list`, `cloud domain:create`, `cloud domain:verify` | ドメイン設定・検証 | | WebSocket | `cloud websocket-cluster:*`, `cloud websocket-application:*` | Reverb関連リソース管理 | ## 環境変数管理 環境変数は `cloud environment:variables` で管理します。\ このコマンドは append / set / replace などの操作をまとめて扱えます。 ```bash theme={null} cloud environment:variables ``` CIやチーム運用では、このコマンドを使って設定変更の手順を統一してください。 ## 使用量確認 課金やリソース利用状況は `cloud usage` で確認できます。 ```bash theme={null} # 当月の使用量 cloud usage # 詳細表示 cloud usage --detailed # 前期間の使用量 cloud usage --period=previous ``` 環境単位で絞り込む場合は `cloud usage --environment=` を使ってください。 # Laravel Cloud Hibernation(自動休止) Source: https://kawax.biz/jp/blog/laravel-cloud-hibernation Laravel Cloudの自動休止機能の仕組み、有効化方法、制限事項、Path Blockingによる不要な復帰防止を解説します。 ## Hibernation とは Laravel Cloud の **Hibernation(自動休止)** は、一定時間 HTTP リクエストがない場合に環境を休止状態に移行する旧来の機能です。\ 現在の新しい Flex コンピュートでは、より高速な **Scale to Zero** が後継として使われます。 Legacy Flex の Hibernation は、休止中のコンピュート課金を止められる一方で、HTTP リクエストを受けてからの復帰に通常 5〜20 秒かかります。新しい Scale to Zero では起動が **500ms 未満** まで短縮され、さらにスリープ中でもスケジュールタスクやキュー処理のために環境が自動で起動します。 新規に Laravel Cloud を使うなら、Hibernation ではなく **Scale to Zero** を前提に考えるのが安全です。Managed Queues や新しい料金プランも含めた全体像は [Laravel Cloud — Laravel専用PaaSの全貌](/jp/blog/laravel-cloud) を確認してください。 ```mermaid theme={null} sequenceDiagram participant User as ユーザー participant Cloud as Laravel Cloud participant App as アプリ User->>Cloud: リクエストなし(タイムアウト) Cloud->>App: 休止状態へ移行
    (課金停止) User->>Cloud: HTTP リクエスト Cloud->>App: 5〜20秒で復帰 App-->>User: レスポンス ``` ## Scale to Zero で変わったこと 新しい Flex コンピュートの Scale to Zero では、以前の Hibernation で課題だった点がかなり解消されています。 | 項目 | 旧 Hibernation | 新しい Scale to Zero | | - | - | - | | HTTP リクエストでの復帰 | 5〜20 秒 | 500ms 未満 | | Scheduled Tasks | 休止中は動かない | スリープ中でも自動起動して実行 | | キュー処理 | 休止中は動かない | スリープ中でも自動起動。Managed Queues 併用が推奨 | | 推奨用途 | 旧 Flex 環境の節約運用 | 現在の標準構成 | 以前の Hibernation では、休止中にタスクスケジュールやキューが動かない問題がありました。Scale to Zero ではこの問題が解消され、特にキュー処理は Managed Queues を使うと App cluster が眠っていても安全に継続できます。 ## 旧 Hibernation の有効化手順 環境のインフラキャンバスダッシュボードで、App compute cluster をクリックします。 **Hibernation** トグルをオンにします。 **Save and Redeploy** をクリックして変更を反映します。 設定を変更しただけでは有効になりません。必ず Save and Redeploy が必要です。 現在の環境で新しく有効化するなら、App compute cluster の **Scale to Zero** トグルを使います。以下の制限事項は、主に旧 Hibernation / Legacy Flex を前提にした説明です。 ## 旧 Hibernation 休止中の制限事項 Hibernation が有効で環境が休止している間は、以下の機能は動作しません。 | 機能 | 休止中の挙動 | | - | - | | HTTP リクエスト処理 | 停止(復帰後に処理) | | Task Scheduler | 実行されない | | Queue Worker | 実行されない | | カスタムバックグラウンドプロセス | 実行されない | 休止から復帰すると、これらの処理は自動的に再開されます。ただし休止中に「来るべきだった」スケジュールのジョブが遡って実行されることはありません。 また、Hibernation には以下の制限があります。 * **Flex コンピュートのみ** 休止が可能です。Pro コンピュートでは Hibernation を有効にできません。 * 休止は**環境単位**で行われます。App cluster が休止すると、同環境内のすべての Worker cluster も休止します。 ## 不要な復帰の原因 HTTP リクエストがあれば環境は自動で復帰しますが、意図しないリクエストによっても復帰が発生します。 * **ボット・クローラー** — 検索エンジンやセキュリティスキャナーなどが自動的にページをクロールする * **Slack や Teams のリンクプレビュー** — メッセージングアプリがリンクのサムネイル取得のためにアクセスする * **WordPress スキャン** — WordPress 環境を探す自動スキャンが `/wp-admin` などにアクセスする * **`.php` ファイルを探す攻撃** — PHP ファイルを直接狙う自動スキャン Laravel Cloud の `*.laravel.cloud` ドメインは検索エンジンにインデックスされないよう `X-Robots-Tag: noindex, nofollow` ヘッダーが付与されますが、ドメインが発見されてしまうと防ぎきれません。カスタムドメインでは `noindex` ヘッダーは付与されません。 なるべく複雑なドメイン名を使うことで、ボットにバニティドメインを発見されるリスクを減らせます。 ## Path Blocking で不要な復帰を防ぐ **Path Blocking** は、特定の拡張子やパスへのリクエストを休止状態のまま処理せずにブロックする機能です。WordPress スキャンや PHP ファイルを狙った攻撃リクエストによる不要な復帰を防ぎます。 デフォルトでブロックされる拡張子とパスは以下の通りです。 **ブロックされる拡張子:** ``` .php, .php3, .php4, .php5, .php6, .php7, .php8, .phtml, .pht, .phps, .env, .git ``` **ブロックされるパス:** ``` /wp-admin, /wp-content, /wp-includes, /wp-json ``` これらのリクエストは、環境が休止中でもアプリを復帰させることなく応答を返します。 Laravel の通常のルーティングでは `.php` 拡張子付きのリクエストは使用しないため、これらをブロックしてもアプリの動作に影響はありません。 ## 旧 Hibernation が向いているユース・不向きなユース * **ステージング・開発環境** — 常時稼働は不要なのでコストを大幅に削減できる * **個人ブログ・ポートフォリオ** — トラフィックが少なくて済む用途 * **デモ・試験用アプリ** — 必要なときだけ復帰すれば十分 * **低頻度の社内ツール** — 使用時間が限られている * **タスクスケジュールを使う用途** — 休止中は `schedule:run` が実行されない * **定期的なキュージョブが必要な用途** — 休止中はキューも停止する * **起動レイテンシが許容できない本番環境** — 復帰に 5〜20 秒かかる * **WebSocket を使うアプリ** — 接続が維持できず休止も遅延する場合がある タスクスケジューラーで定期的に処理を実行したい場合(メール配信、データ集計など)は、Hibernation を有効にすると処理が実行されなくなります。このような用途では Hibernation をオフにするか、外部のスケジューラー(GitHub Actions など)からリクエストを送って環境を起こし続ける設計が必要です。 新しい Scale to Zero ではこの制約は緩和されています。Scheduled Tasks はスリープ中でも自動実行され、キュー処理も Managed Queues を使えば App cluster の休止に影響されません。 ## まとめ Hibernation は Legacy Flex では今でもコスト削減に使えますが、現在は Scale to Zero のほうが実用的です。 * Legacy Flex では稼働時間のみ課金。タイムアウト後に自動休止。 * 旧 Hibernation の復帰時間は 5〜20 秒。 * 新しい Scale to Zero は 500ms 未満で起動する。 * Scale to Zero では Scheduled Tasks と Managed Queues がスリープ中でも動作する。 * Path Blocking でボットによる不要な復帰を防ぐ。 * Flex コンピュートのみ対応。 ## 関連ページ Task Scheduler の設定と Laravel Cloud での動作を確認します。 Queue Worker の設定と Laravel Cloud での運用方法を確認します。 Managed Queues、Scale to Zero、新しい料金プランをまとめて確認します。 # Laravel Doctor — アプリケーション診断ツール Source: https://kawax.biz/jp/blog/laravel-doctor-introduction laravel/doctorの紹介。設定・環境・インフラの一般的な問題を診断し、安全なものは自動修正するLaravel公式パッケージ。php artisan doctorコマンドで実行。2026年7月28日リリース。 ## はじめに [laravel/doctor](https://github.com/laravel/doctor) は、Laravelアプリケーションにおける一般的な設定・環境・インフラストラクチャの問題を診断する公式パッケージです。2026年7月28日にv0.1.0がリリースされました。 それぞれの診断(diagnostic)は単一のチェックです。例えば「Laravelがstorageディレクトリに書き込めるか」を検査し、複数のステータスのいずれかを報告します。安全かつ決定的に修復できる場合は自動修正も提供され、アセットビルド失敗のように自動修復できない問題には対応手順(remediation)が提示されます。 ```bash theme={null} composer require laravel/doctor --dev ``` ## 実行方法 インストール後、`doctor` Artisanコマンドが登録されます。 ```bash theme={null} php artisan doctor ``` 修正可能な問題が見つかると、Doctorは問題を報告したうえで修正の確認を求めます。 ```text theme={null} Storage is writable: The application cannot write to every required storage directory. Make the storage directories writable? (yes/no) [yes] ``` 確認なしで修正を適用したい場合は `--fix` オプションを使います。 ```bash theme={null} php artisan doctor --fix ``` 標準の修正は、`.env` の作成、`APP_KEY` の生成、本番環境でのデバッグモード無効化、`.env` の `.gitignore` 追加、`storage:link` の作成、storageディレクトリの書き込み権限修復など、決定的なローカル修復をカバーします。 修正機能はCLIとagent出力形式でのみ利用可能です。JSONおよびGitHubレポート形式では、機械可読なレポートがアプリケーションを変更しないよう`--fix`は拒否されます。 `--bail` を使うと、失敗またはエラーになった最初の診断で実行を停止します。 ```bash theme={null} php artisan doctor --bail ``` ## 診断ステータス 各診断は次のいずれかのステータスを返します。 | ステータス | 意味 | 終了コードに影響 | | - | - | - | | `pass` | チェックに成功し問題なし | なし | | `notice` | 開発者に伝える価値のある情報 | なし | | `warn` | 対応が不要な場合もある潜在的な問題 | `--fail-on=warn` の場合のみ | | `fail` | 解決すべき問題を検出 | あり | | `skip` | 現在の環境には該当しない | なし | | `error` | 診断の実行中に例外が発生 | あり | デフォルトでは `fail` または `error` があると失敗ステータスで終了します。`--fail-on=warn` で警告でも失敗させたり、`--fail-on=never` で問題の報告のみに留めることもできます。 ## 診断の選択 クラス名・グループ・パッケージ・パッケージのワイルドカードで診断を選択・除外できます。 ```bash theme={null} php artisan doctor --only=storage php artisan doctor --only=StorageIsWritable php artisan doctor --except=laravel/* ``` 設定ファイルを公開すれば、永続的な選択設定も可能です。 ```bash theme={null} php artisan vendor:publish --tag=doctor-config ``` ## 環境モード `sync` キューはローカル開発では妥当なデフォルトですが、本番環境ではキュージョブがWebリクエスト内で同期実行されてしまうことを意味します。このような判断のため、Doctorはアプリケーションを `local` または `production` の2つのモードに解決します。 | モード | 期待される状態 | | - | - | | `local` | 開発中。デバッグモード、`sync` キュー、未キャッシュのbootstrapファイルは正常 | | `production` | 実トラフィックを処理。デバッグモードはセキュリティリスク、キューは非同期実行、bootstrapファイルはキャッシュ済みであるべき | Laravel標準の `local`・`production`・`staging` という環境名は自動認識されます。それ以外の名前を使う場合は設定ファイルでモードにグループ分けします。 ```php theme={null} 'environments' => [ 'local' => ['local', 'dev'], 'production' => ['production', 'staging', 'qa'], ], ``` ## 標準診断 Doctorには以下を含む診断スイートが標準で搭載されています。 * **環境** — `.env` の存在、`APP_KEY`、PHPバージョン、必要な拡張機能、タイムゾーン * **Composer** — 依存関係のインストール状態、オートロード最適化、`composer.lock` の自動修復 * **設定** — 設定ファイルの読み込み・キャッシュ可否、有効なドライバーが必要とする設定値 * **データベース** — 接続の到達性、SQLiteファイルの存在、保留中マイグレーションの自動適用 * **キャッシュ・キュー・スケジューラ・セッション** — 設定済みドライバーの到達性、本番環境外の `sync` キュー検出 * **ストレージ** — デフォルトディスクの到達性、必要ディレクトリの書き込み権限、`storage:link` の存在 * **セキュリティ** — デバッグモードと環境の整合性、`.env` の `.gitignore` 登録、Composer依存関係の監査 ## 独自診断の作成 `Laravel\Doctor\Diagnostic` を継承し `check()` メソッドを実装するだけで独自の診断クラスを作成できます。`make:diagnostic` Artisanコマンドでスキャフォールドも可能です。 ```bash theme={null} php artisan make:diagnostic HorizonIsRunning --fixable ``` 以下は `APP_KEY` の設定を確認し、未設定であれば自動生成する診断の例です。 ```php theme={null} namespace App\Doctor\Diagnostics; use Illuminate\Support\Facades\Artisan; use Laravel\Doctor\Contracts\Fixable; use Laravel\Doctor\Diagnostic; use Laravel\Doctor\EnvironmentMode; use Laravel\Doctor\Results\DiagnosticResult; use Laravel\Doctor\Results\FixResult; class ApplicationKeyIsSet extends Diagnostic implements Fixable { public string $name = 'App key is set'; public string $group = 'environment'; protected function messages(): array { return [ 'configured' => 'The application key is configured.', 'missing' => 'The application key is not configured.', 'generated' => 'The application key was generated.', ]; } public function check(): DiagnosticResult { $key = config('app.key'); if (is_string($key) && trim($key) !== '') { return $this->pass('configured'); } return $this->fail('missing')->fixable(EnvironmentMode::Local); } public function fix(DiagnosticResult $result): FixResult { Artisan::call('key:generate', ['--force' => true]); return $this->fixed('generated'); } } ``` 選択肢のある修正が妥当な場合は、`fixOptions()` で選択肢を宣言できます。CLIはこれを選択リストとして表示し、選ばれた値が `fix()` に渡されます。 ```php theme={null} return $this->fail('unreachable') ->fixable(EnvironmentMode::Local) ->fixOptions(['file' => 'File', 'redis' => 'Redis']); ``` 選択リストの末尾には常に修正を見送る選択肢が追加されます(デフォルトは `Skip — leave unfixed`)。現在の選択を保つ表現の方が分かりやすい場合は `decline` ラベルを指定できます。 ```php theme={null} ->fixOptions(['file' => 'File'], decline: 'Keep Redis (repair it manually)'); ``` ### 診断ヘルパー 多くのアプリケーションやパッケージは同じような種類のチェックを繰り返し書くことになるため、Doctorは `Laravel\Doctor\Support` 名前空間で頻出パターン向けのヘルパーを提供しています。 `Configured` ヘルパーは設定値を防御的に読み取ります。診断は設定が壊れたアプリケーションも報告前に例外を投げずに検査できる必要があるため、これらのメソッドは設定リポジトリの型付きアクセサとは違い、想定外の型でも例外を投げません。 ```php theme={null} use Laravel\Doctor\Support\Configured; $connection = Configured::string('queue.default', 'database'); $missing = Configured::missing([ 'services.stripe.key', 'services.stripe.secret', ]); ``` `ActiveDrivers` ヘルパーは、デフォルトのログチャンネルが `stack` であったりメーラーが `failover` であったりするラッパードライバーを、実際に使われている具体的なチャンネルやメーラーに解決します。 ```php theme={null} use Laravel\Doctor\Support\ActiveDrivers; $channels = ActiveDrivers::logChannels(Configured::string('logging.default', 'stack')); $mailers = ActiveDrivers::mailers(Configured::string('mail.default', 'log')); ``` `Details` ヘルパーは `withDetails()` に添付する証拠情報を整形します。`Details::bullets()` は文字列のリストを箇条書きに、`Details::failures()` はキー付き失敗メッセージを、`Details::processOutput()` は完了したプロセスから最も有用な出力ストリームを選択します。 ```php theme={null} use Laravel\Doctor\Support\Details; Details::bullets(['services.stripe.key', 'services.stripe.secret']); Details::failures(['media' => 'The disk root is not writable.']); ``` パッケージも同じAPIでサービスプロバイダーから診断を登録できます。 ```php theme={null} use Laravel\Doctor\Facades\Doctor; use Vendor\Package\Diagnostics\HorizonIsRunning; public function boot(): void { Doctor::diagnostic(HorizonIsRunning::class); } ``` レポートには診断の提供元パッケージが表示されます。 ```text theme={null} [fail] Storage is writable (laravel/doctor): The application cannot write to every required storage directory. [warn] Horizon is running (laravel/horizon): Horizon is not currently running. ``` ## プログラムからの実行 Artisanコマンドを使わずに `Doctor::run()` を呼び出すこともできます。 ```php theme={null} use Laravel\Doctor\Facades\Doctor; $report = Doctor::only('security') ->except(SomeDiagnostic::class) ->run(); if ($report->hasFailures()) { // ... } ``` プログラム実行で修正も適用したい場合は `fixUsing` を設定します。コールバックは修正を提供する失敗診断を受け取り、`false` でスキップ、`true` で通常の修正を適用、または修正オプションの値を返してその選択で修正を適用します。修正が適用されると、Doctorはレポートに反映させるため診断を再実行します。 ```php theme={null} $report = Doctor::fixUsing( fn ($outcome) => $outcome->fixRequiresOption() ? false : true, )->run(); $report->fixes(); ``` ## 出力形式とAIエージェント対応 Doctorはデフォルトで読みやすいCLI出力を行いますが、JSONやGitHub Actionsアノテーション形式も選択できます。 ```bash theme={null} php artisan doctor --format=json php artisan doctor --format=github ``` [Laravel Agent Detector](https://github.com/laravel/agent-detector) を使ってClaude CodeやCursorのようなAIコーディングエージェント内での実行を検出すると、[Laravel PAO](https://github.com/laravel/pao) と同じ規約に従ったエージェント最適化フォーマットがデフォルトになります。 ```json theme={null} {"tool":"doctor","result":"failed","diagnostics":27,"failed":1,"warnings":1,"notices":0,"passed":19,"skipped":6,"issues":[{"name":".env file exists","status":"fail","summary":"The application does not have an environment file.","fix":"Run `cp .env.example .env`, then review the copied values.","fixable":true}]} ``` `fixable: true` の問題は `--fix` の再実行で修正できます。エージェント外でこのフォーマットを試す場合は `AI_AGENT=test php artisan doctor` を実行します。 ## まとめ `laravel/doctor` は、`php artisan doctor` の実行だけでアプリケーションの設定・環境・インフラの問題を素早く洗い出せるツールです。AIコーディングエージェントとの親和性も高く、Laravel PAOと同じ規約でエージェントが解釈しやすい出力を返すため、CI/CDやAIエージェントによる自動修復ワークフローへの組み込みも検討する価値があります。 ソースコードと最新情報はこちら。 # Laravel新コード解析エコシステム — surveyor / ranger / roster Source: https://kawax.biz/jp/blog/laravel-ecosystem-analysis Laravelが公開した静的解析エコシステムを構成する3つのパッケージ(surveyor・ranger・roster)を紹介します。PHPコード解析からパッケージ検出まで、パッケージ開発者・ツール作者が知っておくべき新ツール群を解説します。 この記事はGitHubリポジトリのREADMEとソースコードに基づく情報です。3パッケージはいずれも正式リリース前のBeta版です(2026年4月時点)。 surveyor・ranger・rosterはいずれも**Betaリリース中**です。v1.0.0リリース前にAPIが変更される可能性があります。本番環境での利用は慎重に判断してください。 ## 最新リリース状況(2026年4月) | パッケージ | 最新リリース | 補足 | | - | - | - | | `laravel/roster` | `v0.5.1` | `laravel/boost` の `composer.json` で `^0.5.0` が要求され、`BoostServiceProvider` で `Roster::scan(base_path())` が実行される | | `laravel/ranger` | `v0.1.12` | `composer.json` で `laravel/surveyor:^0.1.0` に依存 | | `laravel/surveyor` | `v0.1.9` | `ranger` の解析基盤として利用される低レイヤー | ## 概要 Laravelは2026年に入り、コード解析に関連する3つのパッケージを公開しました。これらは単独でも使えますが、相互に連携した**コード解析エコシステム**を形成しています。 ```mermaid theme={null} graph TD A["laravel/roster
    パッケージ検出"] --> B["laravel/ranger
    高レベルイントロスペクション"] B --> C["laravel/surveyor
    低レベル静的解析エンジン"] D["Boost
    AI開発支援ツール"] --> A E["ツール・パッケージ開発者"] --> A E --> B E --> C ``` | パッケージ | 役割 | インストール | | - | - | - | | `laravel/surveyor` | PHPコードの静的解析エンジン | `composer require laravel/surveyor` | | `laravel/ranger` | Laravelアプリ全体のイントロスペクション | `composer require laravel/ranger` | | `laravel/roster` | エコシステムパッケージの検出 | `composer require laravel/roster --dev` | 各パッケージの詳細を順に見ていきましょう。 *** ## laravel/surveyor — 静的解析エンジン [laravel/surveyor](https://github.com/laravel/surveyor) は、PHPファイルをパースしてクラス・メソッド・プロパティ・戻り値の型などの詳細なメタ情報を構造化形式で提供する静的解析ツールです。他のツールやパッケージが利用できる形で情報を抽出することに特化しています。 ### インストール ```bash theme={null} composer require laravel/surveyor ``` ### 基本的な使い方 #### ファイルを解析する ```php theme={null} use Laravel\Surveyor\Analyzer\Analyzer; $analyzer = app(Analyzer::class); // ファイルパスで解析 $result = $analyzer->analyze('/path/to/your/File.php'); // 解析済みスコープへアクセス $scope = $result->analyzed(); // クラス結果へアクセス $classResult = $result->result(); ``` #### クラスを直接解析する ```php theme={null} $result = $analyzer->analyzeClass(\App\Models\User::class); $classResult = $result->result(); ``` ### ClassResultで取得できる情報 ```php theme={null} $classResult = $analyzer->analyzeClass(App\Models\User::class)->result(); // クラス情報 $name = $classResult->name(); // 'App\Models\User' $namespace = $classResult->namespace(); // 'App\Models' $filePath = $classResult->filePath(); // 継承関係 $extends = $classResult->extends(); $implements = $classResult->implements(); // メソッド情報 $method = $classResult->getMethod('store'); $returnType = $method->returnType(); $parameters = $method->parameters(); $rules = $method->validationRules(); // バリデーションルールも取得可能 // プロパティ情報 $property = $classResult->getProperty('email'); $type = $property->type; $visibility = $property->visibility; // 'public', 'protected', 'private' // 公開メソッド・プロパティの一覧 $publicMethods = $classResult->publicMethods(); $publicProperties = $classResult->publicProperties(); ``` ### 型システム Surveyorは充実した型システムを持っており、PHPの型を構造化して扱えます。 ```php theme={null} use Laravel\Surveyor\Types\Type; // 各種型の生成 $stringType = Type::string(); $intType = Type::int(); $boolType = Type::bool(); $nullType = Type::null(); // ユニオン型(string|null など) $unionType = Type::union(Type::string(), Type::null()); // 型の判定 use Laravel\Surveyor\Types\StringType; if (Type::is($returnType, StringType::class)) { // 文字列型の処理 } ``` ### キャッシュ設定 解析結果をキャッシュして繰り返し実行のパフォーマンスを改善できます。 ```php theme={null} use Laravel\Surveyor\Analyzer\AnalyzedCache; // ディスクキャッシュを有効にする AnalyzedCache::enableDiskCache(storage_path('surveyor-cache')); // キャッシュをクリアする AnalyzedCache::clear(); ``` 環境変数でも設定できます。 ```env theme={null} SURVEYOR_CACHE_ENABLED=true SURVEYOR_CACHE_DIR=/path/to/cache ``` ### Eloquentモデルの解析 Surveyorはデータベースへの接続を試みてEloquentモデルを特別に解析します。モデルのリレーション・属性・アクセサ・キャストも検出されます。 ```php theme={null} $result = $analyzer->analyzeClass(App\Models\User::class)->result(); // データベース属性が自動検出される $emailProperty = $result->getProperty('email'); // リレーションメソッドを判定 $method = $result->getMethod('posts'); if ($method->isModelRelation()) { // このメソッドはリレーション } ``` SurveyorはEloquentモデルの解析時にデータベースへの接続を試みるため、完全な静的解析ではありません。また、パフォーマンスとメモリ使用量は現在改善中で、コントリビューションを歓迎しています。 *** ## laravel/ranger — 高レベルイントロスペクション [laravel/ranger](https://github.com/laravel/ranger) はsurveyorをラップし、Laravelアプリケーション全体をウォークスルーしてルート・モデル・Enum・ブロードキャストイベント・環境変数・Inertiaコンポーネントなどの情報を収集する高レベルライブラリです。 ### インストール ```bash theme={null} composer require laravel/ranger ``` ### 基本的な使い方 コールバック形式で、各コンポーネントが発見されたときの処理を記述します。 ```php theme={null} use Laravel\Ranger\Ranger; use Laravel\Ranger\Components; use Illuminate\Support\Collection; $ranger = app(Ranger::class); // ルートが発見されるたびに呼ばれる $ranger->onRoute(function (Components\Route $route) { echo $route->uri(); }); // モデルが発見されるたびに呼ばれる $ranger->onModel(function (Components\Model $model) { foreach ($model->getAttributes() as $name => $type) { // 属性名と型の処理 } }); // Enumが発見されるたびに呼ばれる $ranger->onEnum(function (Components\Enum $enum) { // }); // ブロードキャストイベントが発見されるたびに呼ばれる $ranger->onBroadcastEvent(function (Components\BroadcastEvent $event) { // }); // すべてのルートが収集された後に一度だけ呼ばれる $ranger->onRoutes(function (Collection $routes) { // }); // すべてのモデルが収集された後に一度だけ呼ばれる $ranger->onModels(function (Collection $models) { // }); // アプリケーション全体をウォークスルーしてコールバックを発火する $ranger->walk(); ``` ### Rangerが収集するコンポーネント | コレクター | 説明 | | - | - | | **Routes** | 全登録ルート(URI・パラメーター・HTTPメソッド・コントローラー・バリデーションルール・レスポンス) | | **Models** | Eloquentモデルと属性・型・リレーション | | **Enums** | PHPのBacked Enum(整数・文字列を持つEnum)とケース・値 | | **BroadcastEvents** | `ShouldBroadcast`を実装したイベントとペイロード | | **BroadcastChannels** | 登録済みブロードキャストチャンネル | | **EnvironmentVariables** | `.env`ファイルで定義された環境変数 | | **Inertia Shared Data** | グローバルに共有されたInertia.js props | | **Inertia Components** | Inertia.jsのページコンポーネントと期待されるprops | *** ## laravel/roster — パッケージ検出ツール [laravel/roster](https://github.com/laravel/roster) はプロジェクトにどのLaravelエコシステムパッケージがインストールされているかを検出するツールです。パッケージ開発者やツール作者が「このプロジェクトはInertiaを使っているか?」「Livewireのバージョンは?」といった情報を簡単に取得できます。 ### インストール ```bash theme={null} composer require laravel/roster --dev ``` ### 基本的な使い方 ```php theme={null} use Laravel\Roster\Roster; use Laravel\Roster\Packages; // ディレクトリをスキャンしてRosterを取得 $roster = Roster::scan($directory); // インストール済みパッケージの一覧 $roster->packages(); // 本番環境用パッケージのみ $roster->packages()->production(); // 開発専用パッケージのみ $roster->packages()->dev(); // 特定パッケージの有無を確認 $roster->uses(Packages::INERTIA); // bool $roster->uses(Packages::LIVEWIRE); // bool // バージョン条件を指定して確認 $roster->usesVersion(Packages::INERTIA, '2.0.0', '>='); // Inertia 2.0.0以上か? $roster->usesVersion(Packages::LIVEWIRE, '3.0.0', '>='); // Livewire 3.0.0以上か? // JavaScriptパッケージマネージャーの検出 $packageManager = $roster->nodePackageManager(); // 'npm', 'yarn', 'bun' など ``` *** ## 実際のユースケース ### AIガイドライン生成ツール(Boost) [Laravel Boost](https://github.com/laravel/boost) はrosterを使って、インストール済みのパッケージ構成を把握し、AIエージェント(GitHub CopilotやClaudeなど)向けのガイドラインとスキルの生成を自動調整しています。「このプロジェクトはInertiaを使っているか?」「Livewireは入っているか?」という情報を元に、適切なガイドラインファイルを選択してAIエージェントに提供します。 ```mermaid theme={null} sequenceDiagram participant Boost as Laravel Boost participant Roster as laravel/roster participant AI as AIエージェント Boost->>Roster: Roster::scan(base_path()) Roster->>Boost: packages() [Inertia, Livewire, Pint...] Boost->>Boost: パッケージごとのガイドラインを選択 Boost->>AI: Inertiaガイドライン、Livewireガイドライン... ``` ### パッケージ互換性チェッカーを作る ユーザーのプロジェクトにインストールされているパッケージに応じて動作を変えるパッケージを開発する場合、rosterが非常に有効です。 ```php theme={null} use Laravel\Roster\Roster; use Laravel\Roster\Packages; $roster = Roster::scan(base_path()); if ($roster->uses(Packages::INERTIA)) { // Inertia向けの処理 if ($roster->usesVersion(Packages::INERTIA, '2.0.0', '>=')) { // Inertia v2以上向けの処理 } } if ($roster->uses(Packages::LIVEWIRE)) { // Livewire向けの処理 } // JavaScriptパッケージマネージャーに応じてインストールコマンドを変える $pm = $roster->nodePackageManager(); echo "Run: {$pm} install your-package"; ``` ### アプリケーションドキュメント自動生成 rangerを使うと、Laravelアプリのルート・モデル・Enumを自動的に収集してドキュメントを生成するツールを構築できます。 ```php theme={null} use Laravel\Ranger\Ranger; use Laravel\Ranger\Components; $ranger = app(Ranger::class); $docs = []; $ranger->onRoute(function (Components\Route $route) use (&$docs) { $docs['routes'][] = [ 'uri' => $route->uri(), // ルートのバリデーションルール・レスポンス型なども取得可能 ]; }); $ranger->onModel(function (Components\Model $model) use (&$docs) { $docs['models'][] = [ 'attributes' => $model->getAttributes(), ]; }); $ranger->walk(); // $docs にアプリ全体の構造情報が収集される ``` *** ## まとめ surveyor・ranger・rosterは、Laravelエコシステムにおける**プログラムによるコード解析**の新しい基盤を提供します。 ```mermaid theme={null} graph LR A["surveyor
    PHPファイルをパース
    クラス・メソッド・型を抽出"] --> B["ranger
    Laravelアプリ全体を走査
    ルート・モデル・Enumを収集"] B --> C["roster
    インストール済みパッケージを検出
    バージョン・種類を確認"] ``` これらのパッケージは主にパッケージ開発者・ツール作者を対象としており、エンドユーザーが直接使うものではありません。しかし、Laravel BoostのようにLaravel公式ツールの内部でも積極的に活用されており、今後のエコシステム拡大が期待されます。 PHPコードの静的解析エンジン 高レベルイントロスペクションライブラリ パッケージ検出ツール # Laravel LSP — Language Server Protocol による IDE 機能拡張 Source: https://kawax.biz/jp/blog/laravel-lsp-introduction Laravel 公式の Language Server Protocol 実装。エディタに Framework 認識の補完やホバー機能を提供。 この記事はv0.0.27、2026年7月時点の情報です。Packagistに登録され、`composer global require laravel/lsp` でインストールできるようになりました。 ## Laravel LSP とは **Laravel LSP**(Language Server Protocol)は、Laravel フレームワークを認識した IDE 機能をエディタに提供する公式ツールです。[Language Server Protocol](https://microsoft.github.io/language-server-protocol/) は LSP クライアント(エディタなど)と LSP サーバー(Laravel LSP)の間で通信する標準プロトコルで、これにより複数のエディタで統一された開発体験が実現します。 Laravel LSP が提供する機能: * **補完** — ルート、ビュー、設定キー、翻訳キー、Livewire コンポーネント等の自動補完 * **ホバー情報** — カーソルを重ねると説明やドキュメント、コンテキスト情報を表示 * **診断** — コード上の問題をリアルタイム検出 * **ドキュメントリンク** — ファイルやリソース間のリンク機能 * **クイックフィックス** — 一般的な問題の自動修正提案 * **定義へのジャンプ** — シンボルの定義位置への移動 ## なぜ必要か エディタ標準の PHP 補完では、Laravel Framework の抽象化レイヤーを理解できません。例えば: * `Route::get()` の第一引数に URI パターンを入力しても補完がない * `view('users.index')` のビュー名を入力しても、実際のビューファイル名が補完されない * 設定ファイルのキー(`config('app.name')`)や翻訳キー(`trans('messages.welcome')`)は補完の対象外 * Blade テンプレート内での補完や検証もエディタ標準では対応していない Laravel LSP はこれらの「Framework 固有のコンテキスト」を理解し、正確な補完や診断を提供します。 ## インストール ### グローバルインストール Composer を使ってグローバルにインストール: ```bash theme={null} composer global require laravel/lsp ``` Composer の global bin ディレクトリが `PATH` に含まれていることを確認し、以下のコマンドで起動できます: ```bash theme={null} laravel-lsp ``` ### ソースからのインストール 開発版を使う場合は、リポジトリをクローンして実行できます: ```bash theme={null} gh repo clone laravel/lsp cd lsp composer install php server ``` シェルエイリアスを設定して `laravel-lsp` コマンドを使用できるようにします: ```bash theme={null} # Zsh の場合 echo 'alias laravel-lsp="php /path/to/lsp/server"' >> ~/.zshrc source ~/.zshrc # Bash の場合 echo 'alias laravel-lsp="php /path/to/lsp/server"' >> ~/.bashrc source ~/.bashrc ``` ## エディタ別設定ガイド Laravel LSP はスタンダードな LSP プロトコルを使用しているため、LSP をサポートするあらゆるエディタで動作します。主なエディタの設定方法を紹介します。 ### Sublime Text 公式の [Laravel Sublime Text extension](https://github.com/laravel/sublime-extension) をインストールして設定します。 ### Zed 公式の [Laravel Zed extension](https://github.com/laravel/zed-extension) をインストールして設定します。 ### VS Code 公式の [Laravel VS Code extension](https://github.com/laravel/vs-code-extension) をインストールして設定します。 ### Cursor Cursor は VS Code 拡張機能に対応しているため、上記の [Laravel VS Code extension](https://github.com/laravel/vs-code-extension) がそのまま使えます。 ### Neovim Neovim 0.11 以上では、カスタム LSP 設定を直接追加できます: ```lua theme={null} vim.lsp.config("laravel_lsp", { cmd = { "laravel-lsp" }, filetypes = { "php", "blade" }, root_markers = { "artisan", "composer.json", ".git" }, }) vim.lsp.enable("laravel_lsp") ``` `nvim-lspconfig` を使う場合は、以下のように登録: ```lua theme={null} local lspconfig = require("lspconfig") local configs = require("lspconfig.configs") if not configs.laravel_lsp then configs.laravel_lsp = { default_config = { cmd = { "laravel-lsp" }, filetypes = { "php", "blade" }, root_dir = lspconfig.util.root_pattern("artisan", "composer.json", ".git"), }, } end lspconfig.laravel_lsp.setup({}) ``` ### OpenCode `opencode.json` で LSP サポートを有効化し、Laravel LSP をカスタムサーバーとして設定: ```json theme={null} { "$schema": "https://opencode.ai/config.json", "lsp": { "laravel-lsp": { "command": ["laravel-lsp"], "extensions": [".php", ".blade.php"] } } } ``` ## GitHub Copilot CLI での設定 GitHub Copilot CLI を使用している場合は、`~/.copilot/lsp-config.json` でグローバル設定できます。別途のエディタ設定は不要です: ```json theme={null} { "lspServers": { "laravel-lsp": { "command": "laravel-lsp", "fileExtensions": { ".php": "php", ".blade.php": "blade" } } } } ``` Copilot CLI の LSP サーバー設定は `initializationOptions` にも対応しているため、後述の詳細設定をすべて使用できます。 ## 設定オプション LSP クライアントは `initializationOptions` 経由で詳細設定を Laravel LSP に渡せます。 ### PHP 環境検出 `phpEnvironment` オプションで、プロジェクトデータのインデックス作成に使用する PHP コマンドを制御します。既定は `auto` で自動検出: | 値 | PHP コマンドの動作 | | - | - | | `auto` | Herd → Valet → Sail → Lando → DDEV → ローカル PHP の順で検出 | | `herd` | `herd which-php` を使用 | | `valet` | `valet which-php` を使用 | | `sail` | Sail 実行中に `./vendor/bin/sail php` を使用 | | `lando` | `lando php` を使用 | | `ddev` | `ddev php` を使用 | | `local` | ローカルの PHP バイナリを直接使用 | 検出に失敗したか不正な値が渡された場合は、`php` にフォールバックします。 ### 基本設定例 ```json theme={null} { "phpEnvironment": "auto", "phpCommand": ["php"], "definitionProvider": false } ``` ### Pest ヘルパードキュメントブロック v0.0.27 で追加されたオプションです。Pest のテストや Composer autoload ファイルが変更された時に、Pest ヘルパー用の docblock を自動生成・更新します: | オプション | 型 | 既定値 | 説明 | | - | - | - | - | | `pestGenerateDocBlocks` | `boolean` | `true` | Pest ヘルパーの docblock を生成し、更新し続けるか | | `pestHelperFilePath` | `string` | `"storage/framework/testing/_pest.php"` | Pest ヘルパーの出力先パス(プロジェクトルートからの相対パス) | ```json theme={null} { "pestGenerateDocBlocks": true, "pestHelperFilePath": "storage/framework/testing/_pest.php" } ``` ### 機能ごとの設定 各機能は個別に有効/無効を切り替えられます。接尾辞は `Completion`、`Diagnostics`、`Hover`、`Link` など: ```json theme={null} { "routeCompletion": true, "routeDiagnostics": true, "viewDiagnostics": false, "translationHover": true, "configLink": true, "envCompletion": true, "bladeComponentLink": true } ``` ## 提供される機能一覧 | 機能領域 | 補完 | ホバー | 診断 | リンク | クイックフィックス | | - | - | - | - | - | - | | ルート | ✓ | ✓ | ✓ | ✓ | - | | ビュー & Blade | ✓ | ✓ | ✓ | ✓ | ✓ | | 翻訳 | ✓ | ✓ | - | - | - | | 設定 | ✓ | ✓ | ✓ | ✓ | - | | 環境変数 | ✓ | ✓ | ✓ | ✓ | ✓ | | アセット & Mix | ✓ | ✓ | ✓ | ✓ | - | | Middleware | ✓ | ✓ | ✓ | ✓ | - | | Inertia | ✓ | - | ✓ | ✓ | - | | Livewire | ✓ | ✓ | - | ✓ | - | | Auth & Policies | ✓ | ✓ | ✓ | ✓ | - | | Container Bindings | ✓ | ✓ | ✓ | ✓ | - | | Validation | ✓ | - | - | - | - | | Controller Actions | ✓ | - | ✓ | ✓ | - | | Eloquent | ✓ | - | - | - | - | ## クイックスタート 1. **インストール** — `composer global require laravel/lsp` 2. **エディタ設定** — 上記のエディタ別ガイドを参照 3. **Laravel プロジェクトを開く** — サーバーがルートから routes、views、translations、config などのプロジェクトデータをインデックス化します 4. **補完を使用** — PHP ファイルや Blade テンプレート内で、Framework 認識の補完が自動で動作します ## まとめ Laravel LSP は、開発者体験を大きく向上させるツールです。エディタの標準機能では対応できない Framework 固有のコンテキストを理解し、より正確で有益な補完・診断を提供します。 **必要要件:** * PHP 8.2 以上 * Composer * LSP 対応エディタ (Sublime Text、Neovim、Cursor、VS Code 等) **参考資料:** * [Laravel LSP GitHub リポジトリ](https://github.com/laravel/lsp) * [Language Server Protocol 公式](https://microsoft.github.io/language-server-protocol/) * [GitHub Copilot CLI LSP 設定ガイド](https://docs.github.com/en/copilot/how-tos/copilot-cli/set-up-copilot-cli/add-lsp-servers) # Laravel Pennant 実践ユースケース Source: https://kawax.biz/jp/blog/laravel-pennant Laravel Pennantの基本を超えた実践的なパターンを解説します。チーム単位のスコープ、緊急キルスイッチ、ダークローンチ、管理コマンドなど、公式ドキュメントだけでは分からないノウハウを紹介します。 基本的な使い方は[ガイドページ](/jp/pennant)を参照してください。このページでは公式ドキュメントには載っていない実践的なパターンを紹介します。 *** ## マルチテナント SaaS でのチームスコープ 個人ユーザーではなくチーム(テナント)単位でフラグを管理したい場合は、デフォルトスコープをチームに変更します。 ```php theme={null} // AppServiceProvider Feature::resolveScopeUsing(fn () => Auth::user()?->currentTeam); ``` これだけで `Feature::active('billing-v2')` が自動的に現在のチームを対象にします。チームメンバーが誰であっても同じチームに属する限り同じ結果が返るため、UI の一貫性が保たれます。 チームのサインアップ日に応じた段階的ロールアウトの例です。 ```php theme={null} use App\Models\Team; use Illuminate\Support\Carbon; use Illuminate\Support\Lottery; use Laravel\Pennant\Feature; Feature::define('new-billing', function (Team $team) { // 2024年以降に登録したチームは即時有効 if ($team->created_at->isAfter(new Carbon('2024-01-01'))) { return true; } // 2022〜2023年登録は10%ずつロールアウト if ($team->created_at->isAfter(new Carbon('2022-01-01'))) { return Lottery::odds(1 / 10); } // 古いチームは慎重に1%から return Lottery::odds(1 / 100); }); ``` 特定チームのみ有効化したい場合(エンタープライズ顧客への先行提供など): ```php theme={null} // デプロイ後に特定チームだけ手動で有効化 Feature::for($earlyAccessTeam)->activate('new-billing'); ``` *** ## 緊急キルスイッチ(`before` メソッドの活用) 本番でバグが発覚したとき、コードロールバックなしで即座に機能を無効化できます。クラスベースのフィーチャーに `before` メソッドを追加すると、ストレージの値より先にチェックが走ります。 ```php theme={null} isAdmin()) { return true; } return null; // null を返すと通常の resolve() へ進む } public function resolve(User $user): mixed { return $user->isPremium() || Lottery::odds(1 / 5); } } ``` 環境変数 `FEATURES_NEW_CHECKOUT_DISABLED=true` を設定するだけで、DBを触らずに機能を止められます。デプロイ不要のキルスイッチです。 `before` が `null` を返すと `resolve()` が実行されます。`false` を返すと即座に非アクティブとして扱われます。緊急時以外は `null` を返しておきましょう。 *** ## スケジュールされたロールアウト 特定日時に自動的に全ユーザーへ公開したいケース。`before` メソッドで実装できます。 ```php theme={null} public function before(User $user): mixed { $rolloutDate = Config::get('features.new_api.rollout_date'); if ($rolloutDate && Carbon::parse($rolloutDate)->isPast()) { return true; // ロールアウト日を過ぎたら全員に公開 } return null; // まだの場合は通常の resolve() へ } public function resolve(User $user): mixed { // ロールアウト前は社内メンバーのみ return $user->isInternalTeamMember(); } ``` ```ini theme={null} # .env FEATURES_NEW_API_ROLLOUT_DATE=2025-04-01 ``` これで `2025-04-01` を過ぎると自動的に全ユーザーに公開されます。デプロイなし、DBなし、Artisanコマンドなしで自動化できます。 *** ## ダークローンチ(Shadow Mode) 新しいロジックをユーザーに見せずに本番データで動かし、旧ロジックと結果を比較するパターンです。問題がなければフラグをオンにするだけでリリース完了です。 ```php theme={null} class RecommendationController { public function index(Request $request) { $legacyResult = $this->getLegacyRecommendations($request->user()); // shadow モードでは新アルゴリズムを実行して比較するが、表示は旧ロジック if (Feature::for($request->user())->active('recommendation-v2-shadow')) { try { $newResult = $this->getNewRecommendations($request->user()); // 差異をログに記録(ユーザーには見せない) if ($legacyResult !== $newResult) { Log::channel('shadow_mode')->info('recommendation diff', [ 'user_id' => $request->user()->id, 'legacy' => $legacyResult, 'new' => $newResult, ]); } } catch (\Throwable $e) { Log::error('shadow mode error', ['error' => $e->getMessage()]); } } // レスポンスは常に旧ロジックの結果 return response()->json($legacyResult); } } ``` ログを確認して差異がなくなったら、フラグを `recommendation-v2` に切り替えるだけです。 *** ## イベントを使った A/B テスト結果の収集 公式ドキュメントには `FeatureRetrieved` イベントの言及はありますが、実際のA/Bテスト集計パターンは示されていません。 `FeatureResolved` イベントはフィーチャーの値が**初めて**解決されたときのみ発火します。これを使ってユーザーのバリアント割り当てを記録します。 ```php theme={null} use Laravel\Pennant\Events\FeatureResolved; use Illuminate\Support\Facades\Event; // AppServiceProvider や EventServiceProvider に登録 Event::listen(function (FeatureResolved $event) { if ($event->feature !== 'purchase-button') { return; } // ユーザーへのバリアント割り当て時点で記録 analytics()->identify($event->scope?->id, [ 'ab_purchase_button' => $event->value, ]); }); ``` コンバージョン発生時に別途記録すれば、バリアントごとのコンバージョン率を集計できます。 ```php theme={null} // 購入完了時 Event::dispatch(new PurchaseCompleted($user, $product)); ``` ```php theme={null} // PurchaseCompleted リスナー analytics()->track('purchase_completed', [ 'user_id' => $user->id, 'ab_purchase_button' => Feature::for($user)->value('purchase-button'), ]); ``` `FeatureResolved` と `FeatureRetrieved` の違い: `FeatureResolved` は初回評価時のみ発火、`FeatureRetrieved` は毎回のチェックで発火します。割り当て記録には `FeatureResolved`、ページビュー追跡には `FeatureRetrieved` が適しています。 *** ## キューイングされたジョブでのスコープ キューのジョブでは認証済みユーザーが存在しないため、フィーチャーチェックが想定外の挙動をすることがあります。ジョブにスコープを明示的に持たせましょう。 ```php theme={null} class SendWeeklyDigest implements ShouldQueue { public function __construct( private readonly User $user ) {} public function handle(): void { // NG: 認証ユーザーが存在しないため常に false になる // if (Feature::active('new-digest-layout')) { ... } // OK: ユーザーを明示的に渡す if (Feature::for($this->user)->active('new-digest-layout')) { $this->sendNewLayout($this->user); } else { $this->sendLegacyLayout($this->user); } } } ``` ジョブのディスパッチ時にフラグを評価してジョブに渡す方法もあります。 ```php theme={null} // コントローラーで評価してジョブに渡す $useNewLayout = Feature::for($user)->active('new-digest-layout'); SendWeeklyDigest::dispatch($user, $useNewLayout); ``` *** ## Artisan コマンドによる管理 UI フラグを管理する簡単な Artisan コマンドを作れば、デプロイなしで本番フラグを操作できます。 ```php theme={null} argument('feature'); match ($this->argument('action')) { 'activate' => $this->activate($feature), 'deactivate' => $this->deactivate($feature), 'status' => $this->status($feature), default => $this->error('不明なアクションです'), }; } private function activate(string $feature): void { Feature::activateForEveryone($feature); $this->info("✓ {$feature} を全ユーザーに対して有効化しました"); } private function deactivate(string $feature): void { Feature::deactivateForEveryone($feature); $this->info("✓ {$feature} を全ユーザーに対して無効化しました"); } private function status(string $feature): void { $count = \DB::table('features') ->where('name', $feature) ->where('value', json_encode(true)) ->count(); $this->line("{$feature}: {$count} スコープで有効"); } } ``` ```shell theme={null} php artisan feature activate new-checkout php artisan feature deactivate new-checkout php artisan feature status new-checkout ``` *** ## フィーチャー名の安全なリファクタリング(`Name` アトリビュート) クラスベースのフィーチャーをリネームする際、DBに保存済みのフラグ名が変わると全ユーザーのフラグがリセットされてしまいます。`Name` アトリビュートで保存名を固定しましょう。 ```php theme={null} use Laravel\Pennant\Attributes\Name; // 旧クラス名: CheckoutV2 → 新クラス名: NewCheckoutExperience // DBには常に 'checkout-v2' として保存される #[Name('checkout-v2')] class NewCheckoutExperience { public function resolve(User $user): mixed { return $user->isPremium(); } } ``` これでクラス名を自由にリファクタリングしても、DBのデータはそのまま維持されます。 *** ## まとめ 公式ドキュメントの基本を押さえたら、以下のパターンで Pennant の真価が発揮されます。 | パターン | 使いどころ | | - | - | | チームスコープ | マルチテナント SaaS | | `before` キルスイッチ | 本番バグの緊急対応 | | スケジュールロールアウト | 日時指定のリリース自動化 | | ダークローンチ | 安全な新アルゴリズムの本番検証 | | `FeatureResolved` イベント | A/B テスト結果の正確な収集 | | ジョブの明示的スコープ | キュー環境での正確なフラグ評価 | | 管理 Artisan コマンド | デプロイなしの本番フラグ操作 | | `Name` アトリビュート | 安全なクラスリファクタリング | インストールから基本的な使い方まではガイドページを参照してください。 # ⚡Livewire 4入門 — JavaScriptなしでリアクティブなUIを作る Source: https://kawax.biz/jp/blog/livewire-introduction LaravelのファーストパーティパッケージLivewire 4を使って、JavaScriptを書かずにインタラクティブなUIを構築する方法を、インストールから実践的なコード例まで解説します。 ## Livewireとは Laravelで動的なUIを作ろうとすると、以前はVue.jsやReactなどのJavaScriptフレームワークを組み合わせるか、自分でAJAXリクエストを書くしかありませんでした。 **Livewire**はその問題を解決するLaravel用パッケージです。PHPとBladeだけでリアクティブなUIを構築できます。フォームのリアルタイムバリデーション、ライブ検索、カウンターなど、従来であればJavaScriptが必要だった機能を、すべてPHPで実装できます。 Livewire 4はLaravel 10以降、PHP 8.1以降で動作します。現在の最新バージョンはLivewire 4です。 ### 仕組みの概要 Livewireコンポーネントはサーバー上のPHPクラスとBladeテンプレートの組み合わせです。ユーザーがボタンをクリックしたりフォームに入力したりすると、Livewireは裏側でAJAXリクエストを送り、PHPを実行し、変更された部分だけをページに反映します。開発者はこの仕組みを意識することなく、PHPのみで記述できます。 *** ## インストール Laravelアプリケーションのルートディレクトリで以下のコマンドを実行します。 ```shell theme={null} composer require livewire/livewire ``` Laravelのパッケージ自動検出が有効なので、追加の設定は不要です。 ### レイアウトファイルの作成 フルページコンポーネントとして使う場合はレイアウトファイルが必要です。以下のArtisanコマンドで生成できます。 ```shell theme={null} php artisan livewire:layout ``` `resources/views/layouts/app.blade.php` が生成されます。 ```blade theme={null} {{ $title ?? config('app.name') }} @vite(['resources/css/app.css', 'resources/js/app.js']) @livewireStyles {{ $slot }} @livewireScripts ``` `@livewireStyles` と `@livewireScripts` が、LiverwireとAlpine.jsのアセットを自動で読み込みます。 *** ## 最初のコンポーネント Livewireコンポーネントを生成するArtisanコマンドが用意されています。シンプルなカウンターを作ってみましょう。 ```shell theme={null} php artisan make:livewire Counter ``` このコマンドで `resources/views/components/⚡counter.blade.php` という単一ファイルコンポーネントが生成されます。 ファイル名の ⚡ はLivewireコンポーネントをひと目で識別するためのものです。エディタでの視認性が上がります。不要であれば設定で無効にできます。 生成されたファイルを次のように編集します。 ```blade theme={null} count++; } public function decrement(): void { $this->count--; } }; ?>

    カウント: {{ $count }}

    ``` Bladeテンプレートにこのコンポーネントを埋め込むには、通常のBladeコンポーネント構文を使います。 ```blade theme={null} ``` ボタンをクリックするたびにページリロードなしでカウントが更新されます。`wire:click` がJavaScriptの代わりにPHPメソッドを呼び出す仕組みです。 *** ## プロパティとアクション ### プロパティ — `wire:model` `wire:model` ディレクティブで、入力要素とコンポーネントのプロパティを双方向バインドできます。 ```blade theme={null}

    こんにちは、{{ $name }}さん ({{ $email }})

    ``` デフォルトでは `wire:model` はアクション(フォーム送信など)が実行されたときにのみサーバーと同期します。入力のたびに同期させたい場合は `.live` 修飾子を追加します。 | 記法 | 挙動 | | - | - | | `wire:model` | アクション実行時(フォーム送信など)にのみ同期(デフォルト) | | `wire:model.live` | 入力のたびにリクエストを送信 | | `wire:model.blur` | フォーカスを外れたときに同期(リクエストは発生しない) | | `wire:model.live.blur` | フォーカスを外れたときにリクエストを送信 | ### アクション — `wire:click`、`wire:submit` `wire:click` はクリックイベント、`wire:submit` はフォームの送信イベントにメソッドを紐付けます。 ```blade theme={null} $this->taskName]); $this->taskName = ''; } public function render() { return $this->view([ 'tasks' => Task::latest()->get(), ]); } }; ?>
      @foreach ($tasks as $task)
    • {{ $task->name }}
    • @endforeach
    ``` *** ## リアルタイムバリデーション Livewire 4では `#[Validate]` アトリビュートでプロパティにバリデーションルールを直接定義できます。 ```blade theme={null} validate(); Post::create([ 'title' => $this->title, 'content' => $this->content, ]); $this->reset(['title', 'content']); session()->flash('message', '記事を保存しました。'); } }; ?>
    @if (session('message'))
    {{ session('message') }}
    @endif
    @error('title') {{ $message }} @enderror
    @error('content') {{ $message }} @enderror
    ``` `#[Validate]` を付けたプロパティは更新のたびに自動バリデーションが走ります。`wire:model.live.blur` と組み合わせることで、フォーカスを外れた瞬間にエラーメッセージが表示されるリアルタイムバリデーション体験を実現できます。 `$this->validate()` はフォーム送信時に全プロパティをまとめて検証します。`#[Validate]` による自動バリデーションと二段階で使うのが推奨パターンです。 *** ## ライフサイクルフック Livewireコンポーネントにはいくつかのライフサイクルフックがあります。 | フック | タイミング | | - | - | | `mount()` | コンポーネントが最初に生成されたとき(一度だけ) | | `boot()` | 毎リクエストの開始時(初回・後続リクエスト両方) | | `updating($property, $value)` | プロパティが更新される直前 | | `updated($property)` | プロパティが更新された直後 | | `rendering()` | ビューのレンダリング前 | | `rendered()` | ビューのレンダリング後 | | `dehydrate()` | 毎リクエストの終了時 | ### `mount()` — 初期化 `mount()` はコンポーネントの初期化に使います。コンストラクタの代わりです。 ```blade theme={null} name = Auth::user()->name; $this->email = Auth::user()->email; } }; ?>

    名前: {{ $name }}

    メール: {{ $email }}

    ``` ### `updated()` — プロパティ変更後の処理 プロパティが更新されたあとに処理を挟みたい場合は `updated()` を使います。特定のプロパティに絞る場合はメソッド名に含めます。 ```blade theme={null} username = strtolower($this->username); } }; ?>

    ユーザー名: {{ $username }}

    ``` 入力するたびに自動的に小文字へ変換されます。 *** ## Laravelとの統合 ### Eloquentモデルの活用 Livewireはプロパティに直接Eloquentモデルを保持できます。 ```blade theme={null} user = $user; } #[Validate('required|min:2')] public string $name = ''; public function save(): void { $this->validate(); $this->user->update(['name' => $this->name]); session()->flash('message', 'プロフィールを更新しました。'); } public function render() { return $this->view(); } }; ?>
    @if (session('message'))
    {{ session('message') }}
    @endif
    @error('name') {{ $message }} @enderror
    ``` ### フォームオブジェクト 複雑なフォームはフォームオブジェクトに切り出すことで、コンポーネントをシンプルに保てます。 ```shell theme={null} php artisan livewire:form PostForm ``` ```php theme={null} namespace App\Livewire\Forms; use Livewire\Attributes\Validate; use Livewire\Form; use App\Models\Post; class PostForm extends Form { #[Validate('required|min:3')] public string $title = ''; #[Validate('required|min:10')] public string $content = ''; public function store(): void { Post::create($this->only(['title', 'content'])); } } ``` フォームオブジェクトをコンポーネントから使います。 ```blade theme={null} form->validate(); $this->form->store(); $this->form->reset(); session()->flash('message', '投稿を作成しました。'); } }; ?>
    @error('form.title') {{ $message }} @enderror @error('form.content') {{ $message }} @enderror
    ``` *** ## まとめ Livewireが特に向いているユースケースをまとめます。 | ユースケース | Livewireで実現できること | | - | - | | フォーム処理 | リアルタイムバリデーション、エラー表示 | | データ一覧 | ライブ検索、ソート、ページネーション | | カウンターやトグル | ページリロードなしのUI更新 | | 管理画面 | Eloquentとの直接連携でCRUD操作 | | ウィザード型フォーム | ステップ管理をPHPで実装 | Livewireは「Bladeを使ったことがある」Laravelエンジニアなら今日から使い始められます。JavaScriptフレームワークの学習コストなしで、インタラクティブなUIを実現できるのが最大の強みです。 SPA並みの高度なインタラクションが必要な場面ではInertia.jsが向いていますが、フォームや管理画面、データテーブルのような用途であればLivewireの方がシンプルに実装できることが多いです。まず小さなコンポーネント一つから試してみてください。 Livewireの全機能(ファイルアップロード、ページネーション、テストなど)については公式ドキュメントを参照してください。 # HomebrewでPHPとComposerとNode.jsをインストールする(macOS) Source: https://kawax.biz/jp/blog/mac-setup macOSでHomebrewを使いPHP・Composer・Node.jsをインストールし、Laravel開発環境を整えるステップバイステップガイドです。 ## はじめに Laravel チームは初心者向けに [Herd](https://herd.laravel.com/) や `php.new` を用意していますが、シニアエンジニアはパッケージマネージャーで各ツールを個別に管理することを好みます。自分でバージョンを制御でき、複数プロジェクトで異なる PHP バージョンを切り替えるのも簡単です。 このガイドでは **Homebrew** を使って PHP・Composer・Node.js(nvm 経由)をインストールし、`laravel new` が動く状態にするまでを解説します。 対象環境: macOS 13 Ventura 以降。Apple Silicon(M1/M2/M3)・Intel どちらにも対応しています。 *** ## Homebrew のインストール Homebrew は macOS 向けのパッケージマネージャーです。まだインストールしていない場合は、ターミナルで以下を実行します。 ```bash theme={null} /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" ``` インストール後、Apple Silicon の場合はシェル設定ファイル(`~/.zprofile` など)に以下の行が自動追加されます。手動で追加した場合は、設定を再読み込みしてください。 ```bash theme={null} eval "$(/opt/homebrew/bin/brew shellenv)" ``` インストールを確認します。 ```bash theme={null} brew --version ``` *** ## PHP のインストール Homebrew の `php` フォーミュラは常に最新の安定バージョンを指しています。Laravel 13 は PHP 8.3 以上が必要です。 ```bash theme={null} brew install php ``` ```bash theme={null} php --version ``` `PHP 8.3.x` 以上が表示されれば成功です。 複数の PHP バージョンを管理したい場合は、バージョン付きフォーミュラをインストールします。 ```bash theme={null} brew install php@8.3 ``` バージョンを切り替えるには `brew link` を使います。 ```bash theme={null} brew unlink php brew link php@8.3 --force --overwrite ``` 現在リンクされているバージョンを確認します。 ```bash theme={null} php --version ``` *** ## Composer のインストール Homebrew から直接インストールできます。 ```bash theme={null} brew install composer ``` ```bash theme={null} composer --version ``` `composer global require` でインストールしたパッケージ(Laravel インストーラーなど)を使えるように、`~/.composer/vendor/bin` を `PATH` に追加します。 `~/.zshrc`(または `~/.bashrc`)に以下を追記します。 ```bash theme={null} export PATH="$HOME/.composer/vendor/bin:$PATH" ``` 設定を反映します。 ```bash theme={null} source ~/.zshrc ``` *** ## nvm のインストール Node.js は nvm(Node Version Manager)を使ってユーザー単位でインストールします。`root` 権限なしで管理できるため、グローバルな npm パッケージを使う AI ツールや CLI との相性が良いです。 最新のインストールスクリプトは [nvm の GitHub リポジトリ](https://github.com/nvm-sh/nvm) で確認できます。 ```bash theme={null} curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash ``` インストールスクリプトは自動的に `~/.zshrc`(または `~/.bashrc`)に以下を追記します。追記されていない場合は手動で追加してください。 ```bash theme={null} export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" ``` 設定を反映します。 ```bash theme={null} source ~/.zshrc ``` ```bash theme={null} nvm --version ``` *** ## Node.js のインストール ```bash theme={null} nvm install --lts ``` 新しいターミナルセッションでも自動的に使われるよう、デフォルトバージョンを設定します。 ```bash theme={null} nvm alias default node ``` ```bash theme={null} node --version npm --version ``` *** ## Laravel のインストール確認 ```bash theme={null} composer global require laravel/installer ``` ```bash theme={null} laravel new my-app ``` 対話的なセットアップが始まります。スターターキットや認証の有無などを選択できます。 ```bash theme={null} cd my-app php artisan serve ``` ブラウザで `http://localhost:8000` にアクセスし、Laravel のウェルカムページが表示されれば完了です。 *** ## Herd との使い分け | | Homebrew 手動構築 | Herd | | - | - | - | | **対象者** | バージョン管理や設定を細かく制御したい開発者 | 手早く始めたい初心者・個人開発者 | | **PHP バージョン切り替え** | `brew link` で手動 | GUI で簡単に切り替え | | **ローカルドメイン** | `/etc/hosts` を手動編集 | 自動で `.test` ドメインを設定 | | **サービス管理** | `brew services` | GUI から起動・停止 | | **カスタマイズ性** | 高い | 限定的 | チーム開発や CI 環境と設定を合わせたい場合や、複数バージョンの PHP を頻繁に切り替える場合は Homebrew による手動構築が適しています。 # Laravel Maestro — スターターキット開発のオーケストレーター Source: https://kawax.biz/jp/blog/maestro-introduction Laravel公式の新リポジトリ「Maestro」を調査。複数のスターターキットバリアントを一つのモノレポで管理し、コントリビューションを効率化するオーケストレーターの仕組みを解説します。 この記事はソースコード調査に基づく情報です。公式ドキュメントはまだ存在せず、正式リリース前のリポジトリとなります(2026年4月時点)。 ## Maestroとは [Laravel Maestro](https://github.com/laravel/maestro) は、Laravelの[スターターキット](https://laravel.com/starter-kits)群を一元管理するためのモノレポ型オーケストレーターです。 Laravelのスターターキットには、React・Vue・Svelte(Inertia)とLivewireという複数のスタックがあり、さらにそれぞれに認証方式(Fortify・WorkOS)やオプション(Teams・Blank)を組み合わせた15種類以上のバリアントが存在します。Maestroはこれらすべてのバリアントを単一リポジトリで管理し、変更を各スターターキットリポジトリへ自動的に反映する仕組みを提供します。 ```mermaid theme={null} graph TD A["laravel/maestro
    (モノレポ)"] --> B["orchestrator/
    ビルドツール"] A --> C["kits/
    ソースファイル"] A --> D["browser_tests/
    ブラウザテスト"] B --> E["build コマンドで
    バリアントを組み立て"] E --> F["build/
    ビルド済みキット"] F --> G["composer kit:run
    で起動・ウォッチ"] G --> H["変更を kits/ へ
    自動反映"] ``` ## どんな問題を解決するか スターターキットが多数のバリアントに分かれていると、一つの修正を全バリアントに反映する作業が煩雑になります。例えば認証フォームのバリデーション修正を React・Vue・Svelte・Livewire のそれぞれに個別にPRを出すのは非効率です。 Maestroはこの問題を「共有レイヤーとバリアントレイヤーの階層構造」で解決します。変更をどのレイヤーに加えるべきかをオーケストレーターが判断し、最も適切な場所に自動的に適用されます。 ## スターターキットのバリアント Maestroが管理するスターターキットは二つのスタックで構成されます。 ### Livewire スタック(6バリアント) | バリアント | 説明 | | - | - | | Blank | 認証なしの最小構成 | | Fortify | Laravel Fortifyによる認証 | | Fortify (Multi-file Components) | Bladeビューをコンポーネントファイルに分離した構成 | | Fortify (Teams) | Fortify認証 + Teamsサポート | | WorkOS | WorkOSによる認証 | | WorkOS (Teams) | WorkOS認証 + Teamsサポート | ### Inertia スタック(15バリアント) React・Vue・Svelteの3フレームワーク × Blank・Fortify・WorkOS × Teams有無の組み合わせで、合計15バリアントが存在します。 ## リポジトリの構造 ``` laravel/maestro/ ├── orchestrator/ # ビルドを管理するLaravelアプリ │ ├── app/Console/Commands/BuildCommand.php │ ├── app/Enums/StarterKit.php │ └── scripts/ ├── kits/ # スターターキットのソースファイル │ ├── Shared/ # 全バリアント共通ファイル │ ├── Livewire/ # Livewire固有ファイル │ └── Inertia/ # Inertia固有ファイル(React/Vue/Svelte) └── browser_tests/ # ブラウザテスト ├── bootstrap/ ├── common/ └── teams/ ``` `orchestrator` ディレクトリ自体がLaravelアプリケーションになっており、スターターキットのビルドと実行を管理します。 ## ファイル階層システム Maestroの核心は「レイヤーを重ねてスターターキットを組み立てる」仕組みです。Livewire(Fortify)の場合、以下の順でファイルがコピーされ、後のレイヤーが前のレイヤーを上書きします。 ```mermaid theme={null} graph LR A["Shared/Blank
    共通ブランク"] --> B["Livewire/Blank
    Livewire最小構成"] B --> C["Shared/Base
    共通ベース"] C --> D["Livewire/Base
    Livewireベース"] D --> E["Shared/Fortify
    共通Fortify"] E --> F["Livewire/Fortify
    Livewire Fortify"] F --> G["build/
    完成したキット"] ``` この階層のおかげで「すべてのキットに共通する修正は `Shared/` に、Livewire固有の修正は `Livewire/` に加える」という明確なルールができています。 ## コントリビューションの流れ スターターキットへのコントリビューションは、個別のスターターキットリポジトリではなく、このMaestroリポジトリで行います。 ```bash theme={null} cd orchestrator php artisan build ``` インタラクティブなプロンプトで対象キットと認証バリアントを選択します。フラグを使って直接指定することも可能です。 ```bash theme={null} php artisan build --kit=vue php artisan build --kit=react --workos php artisan build --kit=livewire --teams php artisan build --kit=vue --workos --teams ``` ```bash theme={null} composer kit:run ``` Laravelの開発サーバーとファイルウォッチャーが同時に起動します。`build/` ディレクトリで行った変更は自動的に `kits/` の適切な場所へコピーされます。 `build/` ディレクトリ内のファイルを編集します。ウォッチャーが変更を検知し、自動的に `kits/` ディレクトリへ反映します。 `kits/` ディレクトリへの変更をコミットしてPRを作成します。マージされると、Maestroが自動的に変更を各スターターキットリポジトリへプッシュします。 `build/` ディレクトリはgitignoreされています。変更はすべて `build/` 内で行い、ウォッチャーに `kits/` へ同期させてからコミットしてください。 ## その他の開発コマンド ### リント ```bash theme={null} # kitsとbrowser_testsにPintを実行(PHPのみ) composer kits:pint # PHPリント + 各Inertiaバリアントのフロントエンドリント composer kits:lint # 特定のフレームワークのみを対象にする composer kits:lint -- --vue composer kits:lint -- --react --svelte ``` ### ブラウザテスト ```bash theme={null} # 全バリアントのブラウザテストを実行 composer kits:browser-tests # 特定のフレームワーク・バリアントに絞り込む composer kits:browser-tests -- --vue composer kits:browser-tests -- --livewire --fortify ``` ブラウザテストはPest + Playwrightで動作します。`browser_tests/` ディレクトリに `bootstrap/`(共通設定)、`common/`(Fortify向け)、`teams/`(Teams向け)の3層構造でテストが整理されています。 ### フラグによる絞り込み 各コマンドは `--livewire`・`--react`・`--svelte`・`--vue` フラグと `--blank`・`--fortify`・`--workos`・`--teams` フラグを組み合わせて対象を絞り込めます。 ```bash theme={null} # VueとSvelteのFortifyバリアントのみチェック composer kits:check -- --vue --svelte --fortify # 全フレームワークのWorkOSバリアントのみ composer kits:check -- --workos ``` ## WorkOS環境変数の設定 WorkOSバリアントをビルドして実行する場合は、`orchestrator/.env` にWorkOSのクライアントIDとAPIキーを設定します。ビルド時にこれらの値が `build/` ディレクトリの `.env` ファイルにコピーされます。 ```bash theme={null} # orchestrator/.env WORKOS_CLIENT_ID=your_client_id WORKOS_API_KEY=your_api_key ``` ## 現在の開発状況 * **GitHubリポジトリ**: [laravel/maestro](https://github.com/laravel/maestro) * **正式リリース**: なし(バージョンタグ・リリースは未公開) * **最終コミット**: 2026年4月(アクティブに開発中) * **必要なPHPバージョン**: ^8.2 * **Laravelバージョン**: ^13.0 Maestroはエンドユーザー向けのパッケージではなく、Laravelチームおよびコントリビューター向けの**開発インフラ**として機能しています。スターターキット本体(`laravel/starter-kit-react` など)はMaestroによってこのモノレポから生成・管理されます。 スターターキットへのコントリビューションに興味がある方は、まずMaestroのREADMEをご確認ください。 スターターキット自体の使い方については公式ドキュメントを参照してください。 # Laravel Multiplex — ローカル開発プロセスをまとめて管理するTUI Source: https://kawax.biz/jp/blog/multiplex-introduction laravel/multiplexの初期調査。Laravel 13のphp artisan devが使う、複数プロセスのタブ表示・検索・自動再起動に対応したターミナルUIを解説します。 ## はじめに [`laravel/multiplex`](https://github.com/laravel/multiplex) は、複数のコマンドを同時に実行するためのターミナルUI(TUI)です。Laravel 13の`php artisan dev`から利用され、PHP開発サーバー、キューワーカー、ログ監視、Viteなど、ローカル開発に必要なプロセスを1つのターミナルで管理できます。 タブごとにプロセスの出力を表示でき、出力の検索とスクロールにも対応します。終了時には各プロセスの出力が通常のターミナルのスクロールバックへ書き戻されるため、TUIを終了した後もログを確認できます。 ## インストールと直接実行 Laravelアプリケーションでは`php artisan dev`が必要な設定を行うため、通常はMultiplexを直接インストールする必要はありません。単独で使う場合は、Node.js 22.13以降を用意してnpmでインストールします。 ```bash theme={null} npm install -g @laravel/multiplex ``` コマンドは`label,command`形式で渡します。 ```bash theme={null} multiplex 'server,php artisan serve' 'queue,php artisan queue:listen' 'vite,npm run dev' ``` ## TUIとインラインモード 標準入力と標準出力がTTYの場合はタブ形式のTUIが起動します。`s`でストリーム表示に切り替え、`/`で出力を検索し、`r`で選択中のプロセスを再起動できます。`q`で終了します。 TTYがないCIやパイプ処理では、インラインモードに自動的に切り替わります。各行にプロセスのラベルが付き、出力が到着した順に表示されます。明示的にインラインモードを使う場合は`-i`を指定します。 ```bash theme={null} multiplex -i 'build,npm run build' 'test,npm test' ``` インラインモードでは最後のプロセスが終了すると実行も終了し、コマンドの失敗を終了コードに反映します。JSON形式で処理結果を扱う場合は`--json`を使います。 ```bash theme={null} multiplex --json 'build,npm run build' 'test,npm test' ``` ## 自動再起動と終了処理 クラッシュしたプロセスは1秒待ってから最大5回まで自動的に再起動されます。起動から1秒以内に終了したプロセスは、実行ファイルの不足やポート競合など、最初から起動できていない可能性があるため再試行されません。ビルドやマイグレーションのような一度だけ実行するコマンドでは`--no-restart`を指定します。 ```bash theme={null} multiplex --no-restart 'build,npm run build' ``` Multiplexは終了時やシグナル受信時に子プロセスのプロセスグループを終了させます。開発サーバーだけが残ってポートを占有する状態を防ぐため、複数プロセスをまとめて停止できます。 ## 注意点 子プロセスには標準入力が渡されません。そのため、`php artisan tinker`やマイグレーションの確認プロンプトなど、入力を要求するコマンドはMultiplexから実行できません。対話的なコマンドは別のターミナルで実行してください。 MultiplexのプログラムAPIも提供されていますが、コマンドは`sh -c`で実行されます。設定ファイルやリクエストなど、信頼できない値をコマンド文字列に埋め込まないでください。 ## 参考資料 * [Laravel公式ドキュメント: Artisan - The Dev Command](https://laravel.com/docs/13.x/artisan#the-dev-command) * [laravel/multiplex README](https://github.com/laravel/multiplex) # Laravel Nightwatch 入門 Source: https://kawax.biz/jp/blog/nightwatch-introduction LaravelのホストSaaSアプリケーション監視プラットフォーム「Nightwatch」の概要、Telescope・Pulseとの違い、インストール方法、そして無料プランで実用的に使うための設定を解説します。 ## Laravel Nightwatchとは [Laravel Nightwatch](https://nightwatch.laravel.com) は、Laravelアプリケーションの本番環境を継続的に監視するための**ホスト型SaaSプラットフォーム**です。HTTPリクエスト、SQLクエリ、例外、キュージョブ、ログ、スケジュールタスクなど、アプリケーション全体のテレメトリーデータをリアルタイムに収集・可視化します。 Nightwatchは**有料サービス**(月額課金)です。無料プランでも利用できますが、収集できるイベント数に上限があります。無料プランで実用的に使うには、後述するサンプリングとフィルタリングの設定が必須です。 *** ## Telescope・Pulseとの違い Nightwatchは、Laravelエコシステムにある既存の監視・デバッグツールと目的が異なります。それぞれの用途を整理します。 ```mermaid theme={null} graph TD A["Laravel 監視ツール"] --> B["Telescope
    開発環境のデバッグ"] A --> C["Pulse
    本番環境のパフォーマンス集計"] A --> D["Nightwatch
    本番環境のホスト型監視"] B --> B1["自前ホスティング
    開発時のみ
    無料・OSS"] C --> C1["自前ホスティング
    リアルタイムダッシュボード
    無料・OSS"] D --> D1["SaaSプラットフォーム
    エージェント常駐が必要
    有料サービス"] ``` | ツール | 用途 | ホスティング | 対象環境 | | - | - | - | - | | **Telescope** | 開発時のデバッグ・クエリ調査 | 自前(Laravelアプリ内) | ローカル開発 | | **Pulse** | パフォーマンス指標の集計・表示 | 自前(Laravelアプリ内) | 本番・ステージング | | **Nightwatch** | 本番アプリのリアルタイム監視 | Laravelホスト型SaaS | 本番環境 | Telescopeとほぼ同じ情報を収集しますが、Nightwatchはエージェントプロセスを常駐させてデータをクラウドに送信します。そのため、サーバーが落ちていても履歴データを確認でき、複数サーバー・複数アプリケーションを一元管理できます。 *** ## アーキテクチャ Nightwatchは、LaravelアプリとNightwatchクラウドの間にエージェントプロセスを挟む構成を採っています。 ```mermaid theme={null} sequenceDiagram participant App as Laravelアプリ participant Agent as Nightwatchエージェント
    (php artisan nightwatch:agent) participant Cloud as Nightwatchクラウド participant Dashboard as ダッシュボード
    (nightwatch.laravel.com) App->>Agent: イベントデータを送信
    (127.0.0.1:2407) Agent->>Cloud: バッチ送信 Cloud->>Dashboard: 可視化・アラート ``` エージェントはローカル(`127.0.0.1:2407`)で待機し、Laravelアプリからのイベントを受け取ってクラウドに転送します。このため、エージェントプロセスを常時稼働させる必要があります。 *** ## インストールと初期設定 ### 1. アカウント・アプリケーションの作成 [nightwatch.laravel.com](https://nightwatch.laravel.com) でアカウントを作成し、組織とアプリケーションを登録します。アプリケーション登録後に**環境トークン**が発行されます。 ### 2. パッケージのインストール ```bash theme={null} composer require laravel/nightwatch ``` Telescopeとは異なり、`--dev` フラグは**不要**です。Nightwatchは本番環境での使用を前提としています。 ### 3. トークンの設定 `.env` ファイルにトークンを追加します。 ```ini theme={null} NIGHTWATCH_TOKEN=your-api-key ``` ### 4. エージェントの起動 ```bash theme={null} php artisan nightwatch:agent ``` エージェントはバックグラウンドで常時稼働させる必要があります。Laravel Cloud、Laravel Forge、Laravel Vapor には専用のガイドが用意されています。Forgeの場合、公式インテグレーションを使うと自動設定されます。 エージェントの状態確認: ```bash theme={null} php artisan nightwatch:status ``` ### 5. テスト環境での無効化 テスト実行時はNightwatchを無効にすることを推奨します。 ```ini theme={null} # .env NIGHTWATCH_ENABLED=false ``` `phpunit.xml` でも設定できます。 ```xml theme={null} ``` *** ## 無料プランで実用的に使うための設定 **無料プランには月間イベント数の上限があります。** デフォルト設定(サンプリングレート100%・全データ収集)のままでは、トラフィックの多いアプリケーションではすぐに上限に達してしまいます。以下の設定を必ず適用してください。 ### サンプリングレートを下げる `NIGHTWATCH_REQUEST_SAMPLE_RATE` はデフォルトが `1.0`(全リクエスト収集)です。無料プランでは `0.1`(10%)程度に抑えることを推奨します。 ```ini theme={null} # .env NIGHTWATCH_REQUEST_SAMPLE_RATE=0.1 ``` 例外とコマンドは引き続き全件収集する設定例: ```ini theme={null} NIGHTWATCH_REQUEST_SAMPLE_RATE=0.1 # リクエストは10%のみ NIGHTWATCH_EXCEPTION_SAMPLE_RATE=1.0 # 例外は全件(デフォルト) NIGHTWATCH_COMMAND_SAMPLE_RATE=1.0 # コマンドは全件(デフォルト) ``` ### クエリ収集を無効にする データベースクエリはイベント数の大きな割合を占めます。無料プランではクエリ収集を無効にすることで、重要なイベント(例外・リクエスト・ジョブ)の保存余地を確保できます。 ```ini theme={null} # .env NIGHTWATCH_IGNORE_QUERIES=true ``` ### その他のフィルタリング設定 必要に応じてキャッシュイベントやメール収集も無効化できます。 ```ini theme={null} NIGHTWATCH_IGNORE_CACHE_EVENTS=true NIGHTWATCH_IGNORE_MAIL=true NIGHTWATCH_IGNORE_NOTIFICATIONS=true NIGHTWATCH_IGNORE_OUTGOING_REQUESTS=true ``` ### 無料プラン推奨設定まとめ ```ini theme={null} # .env(無料プラン推奨設定) NIGHTWATCH_TOKEN=your-api-key NIGHTWATCH_REQUEST_SAMPLE_RATE=0.1 NIGHTWATCH_IGNORE_QUERIES=true ``` この3行を設定するだけで、無料プランでも実用的なアプリケーション監視が実現できます。 *** ## 主要機能 ### リクエスト監視 HTTPリクエストごとにレスポンスタイム、ステータスコード、ルート情報などを収集します。異常に遅いエンドポイントを特定し、パフォーマンスのボトルネックを発見できます。 ### 例外トラッキング 本番環境で発生した未処理の例外をリアルタイムに捕捉します。スタックトレースとソースコードのスニペットが自動的に記録され、原因調査が容易になります。 ```ini theme={null} # 例外のソースコード収集(デフォルト有効) NIGHTWATCH_CAPTURE_EXCEPTION_SOURCE_CODE=true ``` ### ログ収集 Laravelのログシステム(`Log::error()` など)と統合し、構造化ログをNightwatchに送信します。 ```ini theme={null} # 収集するログの最低レベル(デフォルト: debug) NIGHTWATCH_LOG_LEVEL=error ``` ### ジョブ・スケジュールタスク監視 キュージョブやスケジュールタスクの実行履歴、成功・失敗状態、実行時間を追跡します。バッチ処理の問題を素早く特定できます。 ### デプロイメントトラッキング リリースごとの変更点と問題を紐付けます。「このデプロイ後から例外が増えた」という調査が視覚的に行えます。 ### アラートと通知 Slack連携やWebhookを使用して、例外の急増やパフォーマンス劣化を即座に通知できます。 *** ## TelescopeとNightwatchの使い分け ```mermaid theme={null} flowchart TD Q1{"本番環境のデータが
    必要?"} -->|はい| Q2{"自分のサーバーに
    データを置きたい?"} Q1 -->|いいえ| T["Telescope
    (ローカル開発)"] Q2 -->|はい| P["Pulse
    (自前ホスティング)"] Q2 -->|いいえ| N["Nightwatch
    (ホスト型SaaS)"] ``` * **ローカル開発** → Telescope * **本番環境の集計ダッシュボードが欲しい** → Pulse * **本番環境の詳細なトレースやアラートが欲しい** → Nightwatch 3つは相互排他ではなく、PulseとNightwatchを併用することも可能です。 *** ## まとめ Laravel Nightwatchは、本番Laravelアプリケーションの可視性を大幅に向上させる強力なSaaSツールです。導入の際は以下の点を押さえておきましょう。 * エージェントプロセスを常時稼働させる必要がある * 無料プランは `NIGHTWATCH_REQUEST_SAMPLE_RATE=0.1` と `NIGHTWATCH_IGNORE_QUERIES=true` の設定が実用上必須 * Telescope(開発)・Pulse(集計)・Nightwatch(監視)は役割が異なり、併用できる 詳細な設定については[公式ドキュメント](https://nightwatch.laravel.com/docs/start-guide)および[環境変数リファレンス](https://nightwatch.laravel.com/docs/environment-variables)を参照してください。 ローカル開発でのデバッグにはTelescopeを使います。 # Laravel Package Skeleton — 公式パッケージ用スターターテンプレート Source: https://kawax.biz/jp/blog/package-skeleton-introduction laravel/package-skeletonの初期調査。Laravelパッケージ開発のベストプラクティスを集めた公式スターターテンプレート。Pest・Larastan・Pint・Workbench・GitHub Actions CI を含む全部入りの構成。 この記事はソースコード調査に基づく情報です。2026年7月時点で開発中のリポジトリです。 ## Laravel Package Skeletonとは [Laravel Package Skeleton](https://github.com/laravel/package-skeleton) は、Laravelパッケージを作る際のスターターテンプレートです。 ```bash theme={null} git clone https://github.com/laravel/package-skeleton.git my-package cd my-package composer install ``` `composer install` を実行すると対話的な設定スクリプト (`configure.php`) が自動起動し、パッケージ名・ベンダー名・ネームスペース・使用する機能を一問一答で設定します。設定後は即座に開発を始められる状態になります。 ```mermaid theme={null} graph TD A["GitHub で
    Use this template"] --> B["composer install"] B --> C["configure.php が自動実行"] C --> D{"対話的な設定"} D --> E["パッケージ名・ベンダー・ネームスペース"] D --> F["使用する機能を選択"] E --> G["設定完了"] F --> G G --> H["composer test で動作確認"] ``` ## なぜこのスケルトンが必要か Laravelパッケージを「自己流」で作り始めると、テスト環境のセットアップ・静的解析・コードフォーマット・GitHub Actions の設定など、本来のパッケージロジックとは無関係な作業に多くの時間を費やします。 Laravel Package Skeleton は、Laravelチームが実際のパッケージ開発で使うベストプラクティスを一つのテンプレートにまとめたものです。テンプレートを使えば「環境構築」をスキップしてパッケージのロジック実装からすぐに始められます。 ## 含まれるツール | ツール | 役割 | | - | - | | [Pest](https://pestphp.com/) | テストフレームワーク | | [Larastan](https://github.com/larastan/larastan) | Laravel向け静的解析 | | [Pint](https://laravel.com/docs/pint) | コードフォーマッター | | [Orchestra Testbench](https://packages.tools/testbench/) | パッケージテスト環境 | | Workbench | エンドツーエンド開発用アプリ | ## 設定可能なパッケージ機能 `composer install` 時のプロンプトで、必要な機能だけを選択できます。選択しなかった機能に関するスキャフォールドは削除されます。 | 機能 | 説明 | | - | - | | Config file | `config/` に設定ファイルを追加 | | Routes | ルートファイルを追加 | | Views | Blade ビューを追加 | | Translations | 言語ファイルを追加 | | Migrations | マイグレーションファイルを追加 | | Assets | 公開用アセットを追加 | | Commands | Artisan コマンドを追加 | | Facade | ファサードクラスを追加 | | Boost Skill | AI エージェント用スキルを追加 | ## 設定可能なツール 機能と同様に、プロジェクトに応じてツールのオン/オフも選択できます。 | ツール | 説明 | | - | - | | Dependabot | 依存関係の自動更新 PR | | Issue Template | GitHub Issue テンプレート | | Changelog | リリース時の自動 CHANGELOG.md 更新 | | Funding | GitHub Sponsors スポンサーリンク | | Security Policy | セキュリティ脆弱性報告ポリシー | ## セットアップの流れ GitHub の **Use this template** ボタンを押して新しいリポジトリを作成するか、直接クローンします。 ```bash theme={null} git clone https://github.com/laravel/package-skeleton.git my-package cd my-package ``` `composer install` を実行すると `configure.php` が自動起動します。 ```bash theme={null} composer install ``` スクリプトを手動で実行したい場合は `--no-scripts` オプションを使います。 ```bash theme={null} composer install --no-scripts php configure.php ``` 以下の項目を一問一答で設定します。 * Author Name / Email * Vendor 名 (例: `kawax`) * Package 名 (例: `my-package`) * Package 説明文 * Class 名 (例: `MyPackage`) * 使用する機能のマルチセレクト * 使用するツールのマルチセレクト * GitHub リポジトリの自動作成(gh CLI が認証済みの場合) ```bash theme={null} composer test ``` PHPStan → Pint → 型カバレッジ → Pest の順で全チェックを実行します。 ```bash theme={null} composer serve ``` `http://localhost:8000` にパッケージ確認用の簡易 Laravel アプリが起動します。 ## 非対話モード CI やスクリプトからセットアップする場合は `--no-interaction` フラグを使います。 ```bash theme={null} php configure.php --no-interaction --config --routes ``` 機能フラグを指定すると、指定した機能だけが含まれます。フラグを省略するとすべての機能が含まれます。 ## GitHub Actions CI スケルトンに含まれる `tests.yml` は、複数の PHP バージョン・Laravel バージョン・OS の組み合わせでテストを実行するマトリックスビルドです。 ```yaml theme={null} matrix: os: [ubuntu-latest, windows-latest] php: [8.3, 8.4, 8.5] laravel: [12.*, 13.*] stability: [prefer-lowest, prefer-stable] ``` 各ジョブで実行される内容: 1. **PHPStan** (`composer analyse`) — 静的解析 2. **Pint** (`composer lint:check`) — スタイルチェック 3. **型カバレッジ** (`composer test:types`) — 型カバレッジ 100% 確認(Ubuntu のみ) 4. **Pest** (`composer test:unit`) — ユニットテスト(Ubuntu のみ) ## Changelog 自動化 `update-changelog.yml` が GitHub Release をトリガーに `CHANGELOG.md` を自動更新します。`release.yml` で PR ラベル別に変更内容を分類してリリースノートを生成します。 リリースノートのラベル: | ラベル | 分類 | | - | - | | `breaking` | 破壊的変更 | | `enhancement` | 機能追加 | | `bug` | バグ修正 | | `documentation` | ドキュメント | | `dependencies` | 依存関係 | | `maintenance` | メンテナンス | | `skip-changelog` | 変更履歴に含めない | ## configure.php が行うこと `configure.php` は設定スクリプトで一度だけ実行する bootstrap です。実行後は削除されます。 ```mermaid theme={null} graph LR A["configure.php 実行"] --> B["プレースホルダーを置換
    :vendor_slug/:package_slug
    :author_name など"] B --> C["未使用の機能ファイルを削除"] C --> D["README_PACKAGE.md → README.md
    AGENTS_PACKAGE.md → AGENTS.md"] D --> E["CLAUDE.md を AGENTS.md にリンク
    .claude を .agents にリンク"] E --> F["スクリプト自体を削除"] ``` ## インストール後の GitHub 設定 README に記載されているインストール後の推奨設定: * **Dependabot PR は手動レビュー** — 自動マージワークフローは含まれない * **リリースノートラベルの作成** — `breaking`・`enhancement`・`bug`・`documentation`・`dependencies`・`maintenance`・`skip-changelog`・`duplicate` * **`main` ブランチ保護の設定** — Changelog 自動化は GitHub Actions が `CHANGELOG.md` にコミットするため、ブランチ保護ルールの調整が必要 追加のリポジトリシークレットは不要です。含まれているワークフローは GitHub の組み込み `GITHUB_TOKEN` を使用します。 ## 現在の開発状況 * **GitHubリポジトリ**: [laravel/package-skeleton](https://github.com/laravel/package-skeleton) * **公開時期**: 2026年7月(活発に開発中) * **注意**: 開発初期段階のため、API や構成は変更される可能性があります これからLaravelパッケージを作り始める人は、自己流のスケルトンを1から組み立てるよりもこのテンプレートから始めるのが良いでしょう。Laravel チームが長年のパッケージ開発で得たベストプラクティスが詰まっています。 ソースコードと最新情報はこちら。 Laravelパッケージ開発の基礎については公式ドキュメントを参照してください。 # Laravel PAO — AIエージェント向け出力最適化ツール Source: https://kawax.biz/jp/blog/pao-introduction laravel/paoの紹介。PHPUnit・Pest・PHPStan・Artisanの出力をAIエージェント環境でJSON形式に圧縮し、トークン消費を最大99.8%削減するLaravel公式パッケージ。 ## はじめに [laravel/pao](https://github.com/laravel/pao) は、PHP開発ツールの出力をAIエージェント向けに最適化するパッケージです。**PAO(PHP Agent-Optimized output)** は、PHPUnit・Pest・Paratest・PHPStan・Laravel Artisanの冗長な出力を、構造化されたコンパクトなJSONへ自動変換します。 GitHub Copilotを含む多くのAIエージェントがトークン数ベースの料金体系へ移行した結果、ツール出力の削減は直接コスト削減につながるようになりました。PAOはこの課題を解決するために設計されており、Laravel スターターキットのデフォルト依存として組み込まれる予定です。 ```mermaid theme={null} graph TD A["ツール実行
    (PHPUnit/Pest/PHPStan/Artisan)"] --> B{"AIエージェント
    環境?"} B -->|Yes| C["JSON形式に圧縮して出力"] B -->|No| D["通常の出力
    (変更なし)"] C --> E["AIエージェントが
    トークン節約しながら処理"] D --> F["人間が通常どおり確認"] ``` ## なぜ重要か AIエージェントが開発フローに深く組み込まれるにつれて、ツールの出力トークン数がコストと応答速度に直結するようになっています。 テスト数が1,000件あるプロジェクトでは、PHPUnitの出力は数千行になりますが、AIエージェントが必要な情報は「成功・失敗・件数・失敗箇所」だけです。PAOはこの情報を1桁のJSONオブジェクトに圧縮します。 * **テスト出力**: 最大99.8%のトークン削減 * **PHPStan出力**: 構造化JSONで必要な情報だけを提供 * **Artisan出力**: ANSIコード・罫線文字・余白を除去して最大75%削減 ## インストール PHP 8.3以上、PHPUnit 12-13 / Pest 4-5 / Paratest / PHPStan / Laravel 12以上が対象です。 ```bash theme={null} composer require laravel/pao --dev ``` インストールするだけで動作します。PAOはComposerのオートローダーを通じてPHPUnit・Pest・Paratest・PHPStanに自動でフックします。Laravelプロジェクトではサービスプロバイダーが自動検出され、Artisanの出力最適化も有効になります。 PAOはAIエージェント環境を検出した場合のみ有効になります。人間がターミナルで直接実行する場合は完全に無効化され、通常の出力が維持されます。 ## 動作するAIエージェント PAOは以下のAIエージェントを自動検出します。 | エージェント | 検出方法 | | - | - | | GitHub Copilot | `COPILOT_MODEL` などの環境変数 | | Claude Code | `CLAUDECODE` または `CLAUDE_CODE` | | Cursor | `CURSOR_AGENT` | | Gemini CLI | `GEMINI_CLI` | | Devin | `/opt/.devin` ファイル | | Codex | `CODEX_SANDBOX` などの環境変数 | ## Before / After ### テスト出力(PHPUnit / Pest) 1,000件のテストスイートが次のように変換されます。 **Before(PAOなし)** ``` PHPUnit 12.5.14 by Sebastian Bergmann and contributors. ............................................................. 61 / 1002 ( 6%) ............................................................. 122 / 1002 ( 12%) ... .......................... 1002 / 1002 (100%) Time: 00:00.321, Memory: 46.50 MB OK (1002 tests, 1002 assertions) ``` **After(PAOあり)** ```json theme={null} { "tool": "phpunit", "result": "passed", "tests": 1002, "passed": 1002, "duration_ms": 321 } ``` テスト数に関わらず出力は常に一定サイズです。テストが失敗した場合はファイルパス・行番号・失敗メッセージが含まれます。 ### カバレッジなどのプラグイン出力 `--coverage` や `--profile` などのPestプラグインの追加出力はANSIコード・装飾を除去したうえで `raw` 配列に含まれます。 ```json theme={null} { "tool": "pest", "result": "passed", "tests": 1002, "passed": 1002, "duration_ms": 1520, "raw": [ "Http/Controllers/Controller 100.0%", "Models/User 0.0%", "Total: 33.3 %" ] } ``` ### PHPStan出力 ```json theme={null} { "tool": "phpstan", "result": "failed", "errors": 2, "error_details": { "/app/Http/Controllers/Controller.php": [ { "line": 9, "message": "Method Controller::index() should return int but returns string.", "identifier": "return.type" }, { "line": 14, "message": "Call to an undefined method Controller::doesNotExist().", "identifier": "method.notFound" } ] } } ``` ### Laravel Artisan出力 `php artisan about` などのコマンド出力からANSIコード・罫線文字・ドット区切り・余白が除去されます。 **Before(PAOなし) — 2,111文字** ``` Environment ................................................................ Application Name ................................................... Laravel Laravel Version ..................................................... 13.3.0 PHP Version .......................................................... 8.5.4 Debug Mode ......................................................... ENABLED ``` **After(PAOあり) — 535文字** ``` Environment .. Application Name .. Laravel Laravel Version .. 13.3.0 PHP Version .. 8.5.4 Debug Mode .. ENABLED ``` `about`・`db:show`・`migrate:status` などのコマンドで最大75%のトークン削減が見込めます。 ## Laravel スターターキットへの統合 PAOはLaravelスターターキットのデフォルト依存として組み込まれる予定です。新規プロジェクトを作成すれば自動的にインストールされ、AIエージェントを使った開発でトークンコストを抑えた状態ですぐに始められます。 ## まとめ `laravel/pao` はインストールするだけで動作し、AIエージェント環境でのみ有効になるため、既存の開発ワークフローに一切影響を与えません。AIエージェントを使った開発コストを下げたいすべてのPHP・Laravelプロジェクトで導入を検討する価値があるパッケージです。 ソースコードと最新の対応ツール・エージェント一覧はこちら。 # Laravel Passkeys 初期調査(passkeys-server + @laravel/passkeys) Source: https://kawax.biz/jp/blog/passkeys-introduction laravel/passkeys-server(PHP)と @laravel/passkeys(npm)を初期調査。インストール、Userモデル統合、ルート、設定、イベント、フロントエンドAPI、型付きエラー、SSR対応まで整理します。 この記事は `laravel/passkeys-server` と `laravel/passkeys` の README に基づく初期調査です。両パッケージはまだタグなしの開発段階です(2026年4月時点)。 ## これは何のパッケージか Laravel の公式リポジトリで、パスワードレス認証(WebAuthn / パスキー)を実装するための 2 つのパッケージが公開されています。 * サーバー側(PHP): [`laravel/passkeys-server`](https://github.com/laravel/passkeys-server) * クライアント側(JavaScript): [`@laravel/passkeys`](https://github.com/laravel/passkeys) この 2 つを組み合わせると、Laravel アプリでパスキー登録とパスキーログインを一貫して実装できます。 ## サーバー側: `laravel/passkeys-server` ### インストールと初期セットアップ ```bash theme={null} composer require laravel/passkeys ``` ```bash theme={null} php artisan vendor:publish --tag=passkeys-migrations php artisan migrate ``` ```php theme={null} use Laravel\Passkeys\Contracts\PasskeyUser; use Laravel\Passkeys\PasskeyAuthenticatable; class User extends Authenticatable implements PasskeyUser { use PasskeyAuthenticatable; } ``` 必要に応じて設定ファイルも公開できます。 ```bash theme={null} php artisan vendor:publish --tag=passkeys-config ``` ### 自動登録されるルート | 区分 | メソッド | ルート | 役割 | | - | - | - | - | | Login (guest) | `GET` | `/passkeys/login/options` | 認証オプション取得 | | Login (guest) | `POST` | `/passkeys/login` | パスキー検証とログイン | | Confirm (auth) | `GET` | `/passkeys/confirm/options` | 確認用オプション取得 | | Confirm (auth) | `POST` | `/passkeys/confirm` | パスキー確認 | | Management (auth) | `GET` | `/user/passkeys/options` | 登録オプション取得 | | Management (auth) | `POST` | `/user/passkeys` | 新規パスキー保存 | | Management (auth) | `DELETE` | `/user/passkeys/{passkey}` | パスキー削除 | ### `config/passkeys.php` の主要オプション * `relying_party_id` * `allowed_origins` * `user_handle_secret` * `timeout` * `guard` * `middleware` * `throttle` * `redirect` ### イベント パッケージは以下のイベントを発火します。 * `PasskeyRegistered` * `PasskeyVerified` * `PasskeyDeleted` ### カスタマイズポイント `Passkeys::authorizeLoginUsing()` で、検証成功後のログイン許可を制御できます。停止したい場合は `false` を返すか、`ValidationException` を投げます。 `GenerateRegistrationOptions` / `GenerateVerificationOptions` / `StorePasskey` / `VerifyPasskey` / `DeletePasskey` を継承してサービスコンテナへバインドし、挙動を差し替えできます。 `PasskeyLoginResponse` などのコントラクトを独自実装して、成功時レスポンス(JSON・リダイレクトなど)をアプリ要件に合わせて変更できます。 ## クライアント側: `@laravel/passkeys` `@laravel/passkeys` はブラウザ側 WebAuthn ceremony を扱う JavaScript クライアントです。 ### インストール ```bash theme={null} npm install @laravel/passkeys ``` ### 基本 API ```js theme={null} import { Passkeys } from "@laravel/passkeys"; await Passkeys.register({ name: "My MacBook" }); await Passkeys.verify(); ``` ### フレームワークヘルパー * React: `@laravel/passkeys/react` の `usePasskeyVerify`, `usePasskeyRegister` * Vue: `@laravel/passkeys/vue` の `usePasskeyVerify`, `usePasskeyRegister` * Svelte: `@laravel/passkeys/svelte` の `usePasskeyVerify`, `usePasskeyRegister` ### パスキーオートフィル `autofill: true` を指定すると、`autocomplete="... webauthn"` を持つ input にブラウザのパスキーピッカーを表示できます。サポートされない環境やユーザーキャンセル時は通常フローへフォールバックします。 ### 型付きエラー README では次のエラークラスが公開されています。 * `NotSupportedError` * `UserCancelledError` * `PasskeyExistsError` * `PasskeyError`(ベースクラス) ### SSR 対応 WebAuthn はブラウザ API ですが、各フレームワーク用フックは SSR セーフです。サーバー側では `isSupported` が `false` として扱われ、マウント後に実ブラウザ状態へ更新されます。 ## まとめ * `laravel/passkeys-server`(PHP)と `@laravel/passkeys`(JS)を組み合わせると、Laravel で完結したパスキー認証スタックを構築できます。 * どちらもタグなしの開発段階ですが、Laravel 公式エコシステムとして今後の標準認証手段になる可能性が高いです。 * 早期採用する場合は、README のルート・設定・エラーハンドリング・カスタマイズ拡張点を前提に設計すると安全です。 その後パスキーは **Laravel Fortify の機能として正式に追加されました**。Fortify 経由でパスキーを有効化する場合は、`laravel/passkeys-server` を直接インストールするのではなく `Features::passkeys()` を使います。詳細は [Laravel Fortify とスターターキット](/jp/advanced/fortify) を参照してください。 サーバー側パッケージの最新 README と実装を確認します。 クライアント側パッケージの最新 README と API を確認します。 Fortify 経由でのパスキー有効化手順と、スターターキットとの関係を解説します。 # Pest PHPで始めるLaravelテスト Source: https://kawax.biz/jp/blog/pest-introduction Laravel 11からデフォルトになったPest PHPをシニアエンジニア視点で紹介します。PHPUnitとの違い・移行コスト・`arch()`テストの活用まで、実際のプロジェクト導入を想定して解説します。 ## なぜPestがLaravelのデフォルトになったか Laravel 11より、`laravel new` でプロジェクトを作成する際に **Pest** がデフォルトのテストフレームワークとして選択されるようになりました。PHPUnit は長年 PHP の標準的なテストツールとして使われてきましたが、Pest はその PHPUnit をベースにしながら、より簡潔で読みやすいテスト記法を提供します。 PestはPHPUnit上で動作するため、既存のPHPUnitテストはそのまま実行できます。移行は段階的に進められます。 公式が Pest を採用した背景には、テストを書く際の摩擦を減らすという目標があります。クラスを定義してメソッドを書いてという儀式的な手順をなくし、「テストしたいこと」そのものに集中できる記法を提供することで、テストを書く習慣が自然と身につくようになります。 *** ## PHPUnitとの主な違い ### テストの記法 最もわかりやすい違いはテストの書き方です。 ```php theme={null} test('ユーザーがログインできる', function () { $user = User::factory()->create(); $response = $this->post('/login', [ 'email' => $user->email, 'password' => 'password', ]); $response->assertRedirect('/dashboard'); }); ``` ```php theme={null} class AuthTest extends TestCase { public function test_user_can_login(): void { $user = User::factory()->create(); $response = $this->post('/login', [ 'email' => $user->email, 'password' => 'password', ]); $response->assertRedirect('/dashboard'); } } ``` Pest の `test()` 関数はクロージャを受け取ります。クラスやメソッドの定義が不要なため、テストの意図が一行目から明確です。`it()` も同様に使えます。英語で書く場合は `it('can login', ...)` という自然な文として読めます。 ### `expect()` API Pest の最大の特徴は `expect()` を使ったチェーン形式のアサーションです。 ```php theme={null} test('投稿一覧APIがJSON形式で返る', function () { $posts = Post::factory(3)->create(); $response = $this->getJson('/api/posts'); expect($response->status())->toBe(200); expect($response->json('data'))->toHaveCount(3); expect($response->json('data.0.title'))->not->toBeEmpty(); }); ``` `expect($value)->toBe()`, `->toBeNull()`, `->toContain()`, `->toHaveCount()` など、英語として自然に読める形式でアサーションを書けます。複数のアサーションをメソッドチェーンでまとめることもできます。 ```php theme={null} expect($user) ->name->toBe('田中太郎') ->email->toBe('tanaka@example.com') ->is_admin->toBeFalse(); ``` ### セットアップとティアダウン PHPUnit の `setUp()` に相当するのが `beforeEach()`、`tearDown()` に相当するのが `afterEach()` です。 ```php theme={null} beforeEach(function () { $this->user = User::factory()->create(); $this->actingAs($this->user); }); test('認証済みユーザーはプロフィールにアクセスできる', function () { $response = $this->get('/profile'); $response->assertOk(); }); test('認証済みユーザーはプロフィールを更新できる', function () { $response = $this->patch('/profile', ['name' => '新しい名前']); $response->assertRedirect('/profile'); }); ``` ### データセット — テーブルドリブンテスト 同じテストロジックを複数のデータで実行したい場合は `dataset` を使います。 ```php theme={null} test('無効なメールアドレスはバリデーションエラーになる', function (string $email) { $response = $this->postJson('/api/users', [ 'name' => '田中太郎', 'email' => $email, ]); $response->assertUnprocessable(); })->with([ 'メールなし' => [''], '形式不正' => ['notanemail'], '二重@' => ['a@@example.com'], ]); ``` 各データセットのラベル(`'メールなし'` など)がテスト名に付加されるため、どのパターンで失敗したかが一目でわかります。 *** ## `arch()` テスト — アーキテクチャの自動チェック Pest の `arch()` はコードベースの構造を検証するテストです。「コントローラーがモデルに直接依存していないか」「モデルが Eloquent を継承しているか」といったアーキテクチャルールを自動で検証できます。 ```php theme={null} test('モデルはEloquentを継承している', function () { arch()->expect('App\Models') ->toExtend(Illuminate\Database\Eloquent\Model::class); }); test('コントローラーはfinalで定義されている', function () { arch()->expect('App\Http\Controllers') ->toBeFinal(); }); test('サービスクラスはコントローラーに依存しない', function () { arch()->expect('App\Services') ->not->toUse('App\Http\Controllers'); }); ``` `arch()` テストはソースコードを静的解析して実行されます。実際にHTTPリクエストを送ったりデータベースにアクセスしたりしないため、非常に高速です。 Pest にはよく使われるアーキテクチャルールのプリセットも用意されています。 ```php theme={null} test('Laravelのアーキテクチャルールに従っている', function () { arch()->preset()->laravel(); }); ``` `laravel()` プリセットはモデルの命名・コントローラーの継承・ミドルウェアの構造など、Laravelの一般的な慣習に沿ったルールをまとめて検証します。 *** ## Laravelプロジェクトでの実践例 ### テストの作成 ```shell theme={null} php artisan make:test UserTest ``` Laravel 11 以降でデフォルト設定のまま実行すると、Pest 形式のテストファイルが生成されます。 ```php theme={null} toBeTrue(); }); ``` ### Laravelのテストヘルパーをそのまま使う Pest は Laravel の `TestCase` を継承しているため、`actingAs()`、`assertDatabaseHas()`、HTTP テストヘルパーなど、すべての Laravel テストヘルパーがそのまま使えます。 ```php theme={null} use App\Models\Post; use App\Models\User; test('ユーザーは自分の投稿を削除できる', function () { $user = User::factory()->create(); $post = Post::factory()->for($user)->create(); $response = $this ->actingAs($user) ->delete("/posts/{$post->id}"); $response->assertRedirect('/posts'); $this->assertModelMissing($post); }); test('他ユーザーの投稿は削除できない', function () { $user = User::factory()->create(); $otherUser = User::factory()->create(); $post = Post::factory()->for($otherUser)->create(); $this ->actingAs($user) ->delete("/posts/{$post->id}") ->assertForbidden(); $this->assertModelExists($post); }); ``` ### ファクトリーとデータベーストランザクション `RefreshDatabase` や `DatabaseTransactions` トレイトも `uses()` で簡単に適用できます。ファイル先頭に書けばそのファイル全体に適用されます。 ```php theme={null} uses(Tests\TestCase::class, Illuminate\Foundation\Testing\RefreshDatabase::class)->in('Feature'); ``` `tests/Pest.php` に一度設定しておくと、すべての Feature テストで `RefreshDatabase` が自動的に有効になります。個別のテストファイルで上書きすることも可能です。 *** ## 既存PHPUnitテストとの共存方法 Pest と PHPUnit は同じプロジェクト内で共存できます。既存の PHPUnit テストを書き換える必要はなく、新しいテストから Pest 形式で書き始めればよいだけです。 ```shell theme={null} ./vendor/bin/pest # Pestで全テストを実行(PHPUnitテストも含む) ./vendor/bin/pest --filter="ユーザー" # テスト名でフィルタリング php artisan test # ArtisanコマンドでもPestが動く ``` `php artisan test` は Laravel 11 以降、Pest がインストールされている場合は自動的に Pest を使って実行します。 ### 移行の進め方 1. まず `tests/Pest.php` に `uses()` の設定を追加する 2. 新しく追加するテストは Pest 形式で書く 3. 既存の PHPUnit テストは動作を確認しながら少しずつ移行する 急いで全テストを書き換える必要はありません。Pest のシンタックスは PHPUnit と比べて学習コストが低いため、チームへの浸透も比較的スムーズです。 *** ## まとめ | 比較項目 | PHPUnit | Pest | | - | - | - | | テスト定義 | クラス + メソッド | `test()` / `it()` 関数 | | アサーション | `$this->assert*()` | `expect()->to*()` | | セットアップ | `setUp()` | `beforeEach()` | | データ駆動 | `@dataProvider` | `->with()` | | アーキテクチャ検証 | なし | `arch()` | | 実行速度 | 標準 | 同等(PHPUnit上で動作) | Pest は PHPUnit の置き換えではなく、PHPUnit をラップした上位レイヤーです。Laravel との統合も深く、スターターキット生成時からデフォルトで選択されるほどになっています。既存プロジェクトへの導入コストは低く、新規のテストから試し始めるだけで恩恵を受けられます。 テストを書く量が増えれば、バグの発見が早くなり、リファクタリングへの心理的ハードルも下がります。Pest はその「テストを書くこと自体の摩擦」を減らすための道具です。 データセット、カバレッジ、並列実行など、Pestの全機能については公式ドキュメントを参照してください。 # React入門 — Inertia × Laravel で使う基礎知識 Source: https://kawax.biz/jp/blog/react-introduction Laravel + Inertia.js で React を使う入門ガイド。laravel/ui から現行スターターキットまでの歴史的経緯と、useForm・usePage などの Inertia React フックを実践的に解説します。 ## Reactとは React は Meta(旧 Facebook)が開発・維持するユーザーインターフェース構築のための JavaScript ライブラリです。宣言的な UI 記述と **コンポーネントベース**のアーキテクチャが特徴で、小規模なウィジェットからフル SPA まで幅広く活用されています。 Reactの核心は**仮想 DOM** を介した効率的な再レンダリングです。状態(state)が変化すると、React は差分だけを DOM に反映するため、手動で DOM を操作する必要がありません。 このページで解説するのは React 19 と Inertia v3 の組み合わせです。Laravel 13 のスターターキットはこの構成をデフォルトで使用します。 ### JSX と TSX React コンポーネントは **JSX**(JavaScript XML)という構文で書きます。HTML に似た記法を JavaScript の中に直接書けます。 ```jsx theme={null} // JSX の例 function Greeting({ name }) { return

    こんにちは、{name}さん

    } ``` スターターキットでは **TypeScript**(`.tsx`)が標準採用されています。型定義により IDE の補完が強化され、バグを早期に発見できます。 ```tsx theme={null} // TSX の例(TypeScript) type Props = { name: string } function Greeting({ name }: Props) { return

    こんにちは、{name}さん

    } ``` Laravel の React スターターキットは TypeScript + TSX が標準です。本ページの例もすべて TSX で記述します。 *** ## Laravel でのポジション ### 歴史 React と Laravel の関係は Vue よりやや浅めですが、現在では同等以上の扱いになっています。 ```mermaid theme={null} timeline title Laravel と React の歩み 2019 : Laravel 6 — laravel/ui に React スキャフォールドを追加 2021 : Laravel 8 — Breeze に Inertia React スタック追加 2022 : Laravel 9 — Vite へ移行 2025 : Laravel 12 — スターターキットを刷新(React / Vue) 2026 : Laravel 13 — Inertia v3 対応スターターキット ``` **Laravel 6(2019年)** で認証スキャフォールドが `laravel/ui` パッケージとして切り離され、Vue と同時に React 版スキャフォールドも提供されました。ただし当時は Vue が主流で React 版の存在感は薄い状況でした。 **Laravel Breeze(2021年)** に Inertia + React スタックが追加されたことで本格的な採用が始まり、**Laravel 12(2025年)** のスターターキット刷新で React が Vue と完全に同列(むしろ最初に表示される)扱いになりました。 ### 現在の主流スタイル:Inertia × React 現在の Laravel における React の使い方の中心は **Inertia × React** です。Inertia は API を設計せずに Laravel のコントローラーから直接 React コンポーネントにデータを渡せる「モダンモノリス」アーキテクチャを実現します。 ```mermaid theme={null} graph LR Browser["ブラウザ"] Inertia["Inertia.js
    (アダプター層)"] Laravel["Laravel
    (コントローラー)"] React["React
    (ページコンポーネント)"] Browser <-->|XHR / フルページロード| Inertia Inertia <-->|Inertia レスポンス| Laravel Inertia -->|props| React React -->|レンダリング| Browser ``` *** ## セットアップ ### スターターキット経由(推奨) 新規プロジェクトで始める場合はスターターキットを使うのが最も手軽です。 ```shell theme={null} laravel new my-app ``` 対話式プロンプトで **React** を選ぶと、以下がすべて自動でセットアップされます。 * `inertiajs/inertia-laravel`(サーバーサイドアダプター) * `@inertiajs/react`(クライアントアダプター) * `react` + `react-dom`(React 19 本体) * `@vitejs/plugin-react`(Vite プラグイン) * TypeScript + `@types/react` * Tailwind CSS + shadcn/ui コンポーネントライブラリ * `HandleInertiaRequests` ミドルウェア * ログイン・登録などの認証画面(Inertia + React + TypeScript で実装済み) ### 手動インストール 既存プロジェクトに追加する場合は、サーバーサイドとクライアントサイドを別々にインストールします。 ```shell theme={null} # サーバーサイド(PHP) composer require inertiajs/inertia-laravel # クライアントサイド(JavaScript) npm install @inertiajs/react react react-dom npm install --save-dev @vitejs/plugin-react @types/react @types/react-dom typescript ``` 次に、`vite.config.ts` に React プラグインを追加します。 ```ts theme={null} import { defineConfig } from 'vite' import laravel from 'laravel-vite-plugin' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [ laravel({ input: ['resources/css/app.css', 'resources/js/app.tsx'], refresh: true, }), react(), ], }) ``` `resources/js/app.tsx` で Inertia アプリを起動します。 ```tsx theme={null} import { createInertiaApp } from '@inertiajs/react' import { createRoot } from 'react-dom/client' import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers' createInertiaApp({ resolve: (name) => resolvePageComponent( `./pages/${name}.tsx`, import.meta.glob('./pages/**/*.tsx'), ), setup({ el, App, props }) { createRoot(el).render() }, }) ``` 手動インストールの詳細(ルートテンプレートの設定やミドルウェアの登録など)は [Inertia 公式ドキュメント](https://inertiajs.com/installation) を参照してください。 *** ## ディレクトリ構造 スターターキットでは React のページコンポーネントを `resources/js/pages/` ディレクトリに配置します。 ``` resources/js/ ├── app.tsx # Inertia アプリの起点 ├── bootstrap.ts ├── components/ # 再利用可能な UI コンポーネント │ ├── ui/ # shadcn/ui コンポーネント │ └── ... ├── hooks/ # カスタム React フック ├── layouts/ # レイアウトコンポーネント │ ├── app-layout.tsx │ └── auth-layout.tsx ├── lib/ # ユーティリティ関数・設定 ├── pages/ # Inertia ページコンポーネント(コントローラー名に対応) │ ├── auth/ │ │ ├── login.tsx │ │ └── register.tsx │ ├── dashboard.tsx │ └── posts/ │ ├── index.tsx │ ├── create.tsx │ └── show.tsx └── types/ # TypeScript 型定義 ``` `Inertia::render('posts/index', [...])` と書くと `resources/js/pages/posts/index.tsx` が対応するコンポーネントになります。 *** ## JSX 構文の基礎 React は JSX を使います。JavaScript の中に HTML ライクな構文を書くスタイルで、スターターキットのコードを読むために最低限知っておきたいパターンを紹介します。 #### `{}` — 変数展開 JSX 内では `{}` を使って JavaScript の値や式を埋め込みます。 ```tsx theme={null} const name = '世界' const count = 3 return (

    こんにちは、{name}!

    2倍は {count * 2}

    ) ``` #### 条件分岐 — `&&` と三項演算子 React には `v-if` に相当するディレクティブがありません。シンプルな条件には `&&` 演算子、if/else には三項演算子 `? :` を使います。 ```tsx theme={null} {/* 条件が true のときだけ表示 */} {isLoggedIn &&

    ようこそ!

    } {/* if / else */} {isLoggedIn ?

    ようこそ!

    : ログイン} {/* if / else if / else */} {isLoggedIn ?

    ようこそ!

    : role === 'admin' ?

    管理者としてログイン中

    : ログイン } ``` #### リスト描画 — `.map()` リストの描画には `Array.map()` を使います。効率的な差分更新のために `key` prop は必ず指定します。 ```tsx theme={null}
      {posts.map((post) => (
    • {post.title}
    • ))}
    ``` Vue の `v-for :key` や Svelte の `{#each}` に相当します。 #### `className` — CSS クラス名 JSX は JavaScript にコンパイルされるため、`class` は予約語です。CSS クラスには `className` を使います。 ```tsx theme={null} {/* HTML:
    */}
    ...
    ``` #### イベントハンドラー — キャメルケース JSX のイベント属性はキャメルケースで、関数の参照を渡します。 ```tsx theme={null} {/* フォーム送信のデフォルト動作をキャンセル */}
    { e.preventDefault(); handleSubmit() }}> ...
    ``` *** ## ページコンポーネントの基本 Inertia のページコンポーネントは通常の React コンポーネントです。Laravel のコントローラーから渡したデータが props として受け取れます。 ### コントローラー ```php theme={null} // app/Http/Controllers/PostController.php use Inertia\Inertia; use App\Models\Post; class PostController extends Controller { public function index() { return Inertia::render('posts/index', [ 'posts' => Post::latest()->paginate(10), ]); } } ``` ### React ページコンポーネント ```tsx theme={null} // resources/js/pages/posts/index.tsx import { Link } from '@inertiajs/react' type Post = { id: number title: string created_at: string } type Props = { posts: { data: Post[] // paginate(10) はページネーション情報も含むオブジェクトを返す // current_page, last_page, per_page, total なども利用可能 } } export default function PostsIndex({ posts }: Props) { return (

    投稿一覧

    {posts.data.map((post) => (

    {post.title}

    {post.created_at}

    ))}
    ) } ``` コンポーネントの引数として props を受け取るだけで、コントローラーから渡したデータをそのまま使えます。REST API を定義する必要はありません。 *** ## `Link` コンポーネント `@inertiajs/react` が提供する `` コンポーネントを使うと、ページ遷移が XHR で行われ、ブラウザのフルリロードを回避できます。 ```tsx theme={null} import { Link } from '@inertiajs/react' export default function PostsIndex() { return (
    {/* 基本的なリンク */} 投稿一覧 {/* POST メソッドでリンク(削除など) */} 削除 {/* プリロード(ホバー時に事前取得) */} 投稿を見る
    ) } ``` 通常の `` タグと同じように書けますが、裏側で Inertia がページコンポーネントだけを差し替えるため SPA のような操作感になります。 *** ## `Form` コンポーネント `@inertiajs/react` が提供する `
    ` コンポーネントは、スターターキットの認証画面で使われているフォーム送信の推奨スタイルです。`action` と `method` を props で指定し、children 関数(render prop)で `errors` と `processing` を受け取ります。 ### 基本的な使い方 ```tsx theme={null} import { Form } from '@inertiajs/react' export default function PostCreate() { return ( {({ errors, processing }) => ( <>
    {errors.title &&

    {errors.title}

    }
    {#if errors.content}

    {errors.content}

    {/if}
    {/snippet}
    ``` `{#snippet children({ errors, processing })}` は Svelte のスニペット構文で、コンポーネントに渡すコンテンツブロックです(Vue のスロットや React の render props に相当)。`errors` と `processing` は `Form` コンポーネントが自動で算出して渡してくれます。フォームフィールドには `bind:value` ではなく HTML ネイティブの `name` 属性を使い、ブラウザの標準フォームデータ収集が機能します。 ### スターターキットのパターン スターターキットは [Wayfinder](/jp/blog/wayfinder-introduction) を使ってルートをオブジェクトとして管理しています。`store.form()` はルートオブジェクトの `action` と `method` を含むオブジェクトを返し、`
    ` に spread します。 ```svelte theme={null} {#snippet children({ errors, processing })} {/snippet}
    ``` `resetOnSuccess` に指定したフィールドは、送信成功時に自動でリセットされます。パスワードフィールドなど送信後に空にしたいフィールドに指定します。 Wayfinder を使わない場合は `action="/login"` のように直接 URL を渡せば同じように動作します。 *** ## `useForm` フック フォーム処理には `@inertiajs/svelte` の `useForm` フックを使います。フォームの状態管理・送信・バリデーションエラー表示がシンプルに実装できます。 ### コントローラー側 ```php theme={null} // app/Http/Controllers/PostController.php class PostController extends Controller { public function store(Request $request) { $validated = $request->validate([ 'title' => ['required', 'string', 'max:255'], 'content' => ['required', 'string'], ]); Post::create($validated + ['user_id' => auth()->id()]); return redirect()->route('posts.index') ->with('success', '投稿を作成しました。'); } } ``` ### Svelte フォームコンポーネント ```svelte theme={null}
    {#if form.errors.title}

    {form.errors.title}

    {/if}
    {#if form.errors.content}

    {form.errors.content}

    {/if}
    ``` `useForm` が返すオブジェクトの主なプロパティをまとめます。 | プロパティ / メソッド | 説明 | | - | - | | `form.data` | フォームのデータオブジェクト | | `form.errors` | バリデーションエラー(フィールド名でアクセス) | | `form.processing` | 送信中は `true`(ボタン無効化に使う) | | `form.isDirty` | 初期値から変更されている場合 `true` | | `form.post(url)` | POST リクエストで送信 | | `form.put(url)` | PUT リクエストで送信(更新) | | `form.delete(url)` | DELETE リクエストで送信 | | `form.reset()` | フォームを初期値にリセット | バリデーションエラーが返ったとき、`useForm` は入力内容を保持したままエラーを表示します。`bind:value` との組み合わせでシームレスなフォーム体験を実現できます。 *** ## 共有データ(Shared Data) すべてのページで共通して必要なデータ(ログイン中のユーザー情報・フラッシュメッセージなど)は `HandleInertiaRequests` ミドルウェアの `share()` メソッドで定義します。 ```php theme={null} // app/Http/Middleware/HandleInertiaRequests.php use Illuminate\Http\Request; use Inertia\Middleware; class HandleInertiaRequests extends Middleware { public function share(Request $request): array { return array_merge(parent::share($request), [ 'auth' => [ 'user' => $request->user() ? $request->user()->only('id', 'name', 'email') : null, ], 'flash' => [ 'success' => $request->session()->get('success'), 'error' => $request->session()->get('error'), ], ]); } } ``` Svelte コンポーネントから共有データにアクセスするには `usePage()` を使います。 ```svelte theme={null}
    {#if page.props.auth.user} {page.props.auth.user.name} {:else} ゲスト {/if}
    {#if page.props.flash.success}
    {page.props.flash.success}
    {/if} ``` 共有データはすべてのリクエストに含まれるため、必要最低限のデータに絞ることを推奨します。`fn()` を使ったレイジー評価にすると、実際にアクセスされたときだけ評価されます。 *** ## Svelte 5 のリアクティビティ(ルーン) Inertia × Svelte で開発するうえで知っておくべき Svelte 5 のルーン構文を紹介します。 ### `$state` — リアクティブな状態 ```svelte theme={null}

    {count}

    ``` `$state` で宣言した変数は自動的にリアクティブになります。値が変わると DOM が自動更新されます。Vue の `ref` や React の `useState` に相当しますが、`.value` プロパティへのアクセスが不要で、代入するだけで状態を更新できます。 ### `$derived` — 派生した値 ```svelte theme={null}

    {summary}

    ``` `$derived` は依存する値が変わると自動的に再計算されます。Vue の `computed` や React の `useMemo` に相当します。 ### `$effect` — 副作用の処理 ```svelte theme={null} ``` `$effect` は依存する `$state` 値が変化するたびに実行されます。React の `useEffect` に相当しますが、依存配列を明示する必要がなく、使用した `$state` 変数が自動的に追跡されます。 *** ## shadcn-svelte コンポーネント スターターキットには [shadcn-svelte](https://www.shadcn-svelte.com/) が組み込まれています。shadcn-svelte は React 向けの shadcn/ui と同じ設計思想を持つコンポーネントライブラリで、コードをプロジェクトにコピーして自由にカスタマイズできます。 ### コンポーネントの追加 ```shell theme={null} npx shadcn-svelte@latest add button npx shadcn-svelte@latest add input npx shadcn-svelte@latest add card ``` コマンドを実行すると `resources/js/components/ui/` にコンポーネントのソースコードが配置されます。 ### 使い方 ```svelte theme={null} ログイン
    {#if form.errors.email}

    {form.errors.email}

    {/if}
    ``` スターターキットには Button・Input・Card・Dialog・Dropdown など、よく使うコンポーネントがあらかじめ含まれています。追加したいコンポーネントは `npx shadcn-svelte@latest add` コマンドで随時追加できます。 *** ## まとめ Svelte は Laravel との組み合わせで、特に Inertia を経由した「モダンモノリス」構成で力を発揮します。コンパイラベースの設計によりバンドルサイズが小さく、ルーン構文によりリアクティビティが明示的で理解しやすいのが特徴です。 | 要素 | 役割 | | - | - | | Laravel コントローラー | ルーティング・データ取得・バリデーション | | `Inertia::render()` | コントローラーから Svelte コンポーネントへデータを渡す | | Svelte ページコンポーネント | `$props()` で props を受け取り UI をレンダリング | | `useForm` | フォームの状態管理・送信・エラー表示 | | `Link` コンポーネント | フルリロードなしのページ遷移 | | `usePage().props` | 共有データへのアクセス | | shadcn-svelte | 標準コンポーネントライブラリ | Inertia × Svelte を使うと、Laravel バックエンドのシンプルさと Svelte のコンパクトな記述スタイルを組み合わせた開発体験が得られます。スターターキットでプロジェクトを作成すれば、認証画面も含めてすぐに開発を始められます。 Inertia v3 の全機能については公式ドキュメントを参照してください。 # laravel/symfony-on-cloud — Symfony アプリを Laravel Cloud で動かす Source: https://kawax.biz/jp/blog/symfony-on-cloud-introduction Laravel 公式の新リポジトリ調査。Symfony Messenger のマネージドキューを Laravel Cloud 上で利用できる公式 Symfony バンドルを解説します。 この記事は 2026年6月時点の初期調査です。`laravel/symfony-on-cloud` は開発段階のリポジトリで、今後機能が追加される予定です。 ## laravel/symfony-on-cloud とは [laravel/symfony-on-cloud](https://github.com/laravel/symfony-on-cloud) は、**Symfony アプリケーションに Laravel Cloud の機能をもたらす**公式 Symfony バンドルです。2026年6月27日に公開された非常に新しいリポジトリです。 ```mermaid theme={null} flowchart LR A["Symfony アプリ"] --> B["laravel/symfony-on-cloud
    バンドル"] B --> C["Laravel Cloud
    マネージドキュー"] C --> D["AWS SQS"] C --> E["Cloud ダッシュボード
    メトリクス表示"] ``` Laravel Cloud はこれまで Laravel アプリケーション専用のプラットフォームでしたが、このパッケージにより **Symfony 開発者も Laravel Cloud のインフラ(マネージドキュー等)を活用**できるようになります。 最初の機能は **Symfony Messenger を使ったマネージドキュー**です。README には「more Laravel Cloud capabilities will follow」とあり、今後もキュー以外の機能が追加される予定です。 ## インストール ```bash theme={null} composer require laravel/symfony-on-cloud ``` 次に `config/bundles.php` にバンドルを登録します(Symfony Flex のレシピは未対応のため手動で追加します)。 ```php theme={null} return [ // ... Laravel\Cloud\Symfony\LaravelCloudBundle::class => ['all' => true], ]; ``` ## マネージドキュー ### 基本設定 バンドルは `cloud` という名前の既成トランスポートを提供します。Laravel Cloud 環境では接続設定を自動で注入するので、DSN を手動設定する必要はありません。 ```yaml theme={null} # config/packages/messenger.yaml framework: messenger: routing: '*': cloud ``` 既存の Symfony Messenger アプリを Laravel Cloud に移行する場合も簡単です。Cloud が `MESSENGER_TRANSPORT_DSN=laravel-cloud://managed-queue` を自動注入するため、従来の `async` トランスポート(`dsn: '%env(MESSENGER_TRANSPORT_DSN)%'`)をそのまま使っているアプリはコード変更なしで動作します。 ### 複数キュー Laravel Cloud 環境では複数のマネージドキュー(例:`default` と `critical`)を作成できます。各キューは独立した SQS キューとワーカーで動作します。`CloudQueueStamp` を使って特定のキューに dispatch します。 ```php theme={null} use Laravel\Cloud\Symfony\Queue\Messenger\CloudQueueStamp; use Symfony\Component\Messenger\MessageBusInterface; $bus->dispatch(new ProcessReport($report), [new CloudQueueStamp('critical')]); ``` スタンプなしで dispatch するとデフォルトキューに送られます。 ### FIFO キュー 名前が `.fifo` で終わるマネージドキューは [FIFO キュー](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues.html) として扱われます。メッセージが厳密な順序で配信され、重複が排除されます。 ```php theme={null} use Laravel\Cloud\Symfony\Queue\Messenger\CloudQueueStamp; $bus->dispatch(new ProcessOrder($order), [new CloudQueueStamp('orders.fifo')]); ``` デフォルトでは、グループ ID にキュー名、重複排除 ID に一意の値が自動設定されます。より細かい制御が必要な場合は `CloudFifoStamp` を追加します。 ```php theme={null} use Laravel\Cloud\Symfony\Queue\Messenger\CloudFifoStamp; use Laravel\Cloud\Symfony\Queue\Messenger\CloudQueueStamp; $bus->dispatch(new ProcessOrder($order), [ new CloudQueueStamp('orders.fifo'), new CloudFifoStamp( messageGroupId: 'customer-'.$order->customerId, messageDeduplicationId: 'order-'.$order->id, ), ]); ``` ### フェアキュー [SQS フェアキュー](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-fair-queues.html) は、一つのテナントが大量のジョブを投入しても他のテナントを圧迫しない仕組みです。標準キューでメッセージグループ ID を付与することで、SQS がテナント間で処理能力を公平に分配します。 ```php theme={null} use Laravel\Cloud\Symfony\Queue\Messenger\CloudMessageGroupStamp; use Laravel\Cloud\Symfony\Queue\Messenger\CloudQueueStamp; $bus->dispatch(new ProcessOrder($order), [ new CloudQueueStamp('orders'), new CloudMessageGroupStamp('customer-'.$order->customerId), ]); ``` `CloudMessageGroupStamp` は**標準キュー専用**で FIFO キューでは使えません。FIFO キューでのグループ指定は `CloudFifoStamp` を使います。 ### 遅延 `DelayStamp` を使うと標準キューに送信遅延を設定できます。ただし SQS の仕様により **最大 15 分**の制限があります。15 分を超えるとエラーになります(サイレントに 15 分で実行されることはありません)。 FIFO キューでは遅延がサポートされていないため、`DelayStamp` 自体がエラーになります。 ### リトライ ハンドラーが失敗した場合、バンドルはメッセージを SQS に返してビジビリティタイムアウト経由で再配信します。送信制限(15 分)ではなく SQS のビジビリティタイムアウト上限(**12 時間**)まで待機できます。 リトライ設定は Symfony Messenger 標準の `retry_strategy` を使います。 ```yaml theme={null} # config/packages/messenger.yaml framework: messenger: transports: cloud: retry_strategy: max_retries: 3 # 3回リトライ(計4回実行) delay: 1000 # 最初のバックオフ(ミリ秒) multiplier: 2 # バックオフ倍率 max_delay: 0 # 上限なし(SQS の 12 時間制限が有効) ``` `UnrecoverableExceptionInterface` を実装した例外はリトライなしで即座に失敗として記録されます。 ## ローカル開発 `cloud` トランスポートはアプリで上書きできます。ローカルでは `sync://` を使うとジョブが即時同期実行されます。 ```yaml theme={null} # config/packages/messenger.yaml when@dev: framework: messenger: transports: cloud: 'sync://' ``` 機能を完全に無効化するには `laravel_cloud.queue.enabled: false` を設定します。 ## まとめ `laravel/symfony-on-cloud` は Laravel Cloud のインフラを Symfony アプリでも活用できるようにする公式バンドルです。まず**マネージドキュー**が実装されており、Symfony Messenger との統合、複数キュー、FIFO/フェアキュー、リトライ設定など実用的な機能が揃っています。 | スタンプ | 用途 | | - | - | | `CloudQueueStamp` | 特定のキューに dispatch | | `CloudFifoStamp` | FIFO キューの順序・重複排除制御 | | `CloudMessageGroupStamp` | 標準キューのフェア分配(テナントキー) | 公式の README によれば今後もキュー以外の Laravel Cloud 機能が追加される予定です。Symfony アプリを Laravel Cloud で運用するケースが増えれば非常に価値あるパッケージになりそうです。 ## 関連リンク 公式リポジトリ・README Laravel Cloud の公式サイト Symfony Messenger 公式ドキュメント Laravel Cloud 詳細ドキュメント # Laravel Telescope 実践テクニック Source: https://kawax.biz/jp/blog/telescope-introduction Telescopeをローカル開発で最大限に活かす実践的なパターンを紹介します。N+1検出、タグを使った追跡、カスタムウォッチャー、Dumpウォッチャーの活用など、公式ドキュメントだけでは分からないノウハウをまとめました。 基本的なインストールと設定は[ガイドページ](/jp/telescope)を参照してください。このページでは公式ドキュメントには載っていない、ローカル開発での実践的な活用パターンを紹介します。 Telescopeは**ローカル開発専用**ツールです。本番環境の監視には [Laravel Pulse](/jp/pulse) や [Laravel Nightwatch](/jp/blog/nightwatch-introduction) を使ってください。 *** ## N+1問題を体系的に発見する Telescopeを使ったN+1デバッグの実践的な流れを解説します。単に「同じSELECTが繰り返されている」と気づくだけでなく、修正後の確認まで一連のワークフローとして行えます。 ```mermaid theme={null} sequenceDiagram participant ブラウザ participant Laravel participant Telescope participant DB ブラウザ->>Laravel: GET /posts Laravel->>DB: SELECT * FROM posts (1クエリ) loop 各投稿について Laravel->>DB: SELECT * FROM users WHERE id = ? (N クエリ) end Laravel-->>ブラウザ: レスポンス Laravel-->>Telescope: クエリ履歴を記録 Note over Telescope: 同じSELECT文がN回
    slowタグが付く ``` `/telescope` を開き、Requestsタブで実行時間の長いリクエストを特定します。 リクエストの詳細を開くと、そのリクエスト中に実行されたすべてのSQLクエリが表示されます。同じようなSELECT文が繰り返されていたらN+1です。 コードに `with()` を追加してクエリを再確認します。クエリ数が大幅に減っていれば修正完了です。 ```php theme={null} // N+1が発生するコード $posts = Post::all(); foreach ($posts as $post) { echo $post->user->name; // 投稿の数だけ追加クエリ } // Eager Loadingで解決 $posts = Post::with('user')->get(); ``` デバッグ用の `dd()` やログをコードに追加することなく、この一連の流れをブラウザ上で完結できます。 *** ## タグを使った特定リクエストの追跡 Telescopeには**タグ**機能があります。`Telescope::tag()` でエントリに任意のタグを付けると、ダッシュボードのフィルターでそのタグのエントリだけを素早く絞り込めます。 特定のユーザーIDや注文IDに関連する処理だけを追跡したいケースで非常に有効です。 ```php theme={null} use Laravel\Telescope\IncomingEntry; use Laravel\Telescope\Telescope; // TelescopeServiceProvider の tags メソッドをオーバーライドする Telescope::tag(function (IncomingEntry $entry) { if ($entry->type === 'request') { return array_filter([ 'status:' . $entry->content['response_status'], auth()->check() ? 'user:' . auth()->id() : null, ]); } return []; }); ``` これで `/telescope/requests` 画面のSearchに `user:42` と入力するだけで、ユーザーID=42のリクエストだけを一覧できます。 ### モデルにタグを自動付与する `TelescopeServiceProvider` の `tags` メソッドで、特定のモデルIDをすべてのエントリに付与できます。 ```php theme={null} // TelescopeServiceProvider Telescope::tag(function (IncomingEntry $entry) { return $entry->tags(); }); ``` さらに `HasTags` コントラクトをモデルに実装することで、そのモデルが記録されるとき自動的にタグが付きます。 ```php theme={null} use Laravel\Telescope\Contracts\EntriesRepository; class Order extends Model implements \Laravel\Telescope\Contracts\HasTags { public function telescopeTags(): array { return ['order:' . $this->id]; } } ``` *** ## Dumpウォッチャーの活用 `dump()` を使うとHTMLレスポンスに出力が混入してしまい、APIのデバッグが難しくなることがあります。TelescopeのDumpウォッチャーを使うと、`dump()` の出力をブラウザのレスポンスから切り離してダッシュボードに記録できます。 使い方はシンプルです。`/telescope` の「Dump」タブを開いた状態で `dump()` を呼び出すだけです。 ```php theme={null} // コントローラー public function index() { $users = User::with('posts')->get(); dump($users->first()->toArray()); // Telescopeのdumpタブに表示される return response()->json($users); } ``` ページを開いた状態でのみキャプチャされるため、必要なときだけ監視できます。`dd()` とは違いアプリの実行を止めないので、連続したリクエストを流しながらデバッグする場合に便利です。 *** ## Mailpit連携でメールデバッグを快適にする MailウォッチャーとローカルSMTPサーバーの [Mailpit](https://mailpit.axllent.org/) を組み合わせることで、メール開発のフローが大幅に改善します。 ```ini theme={null} # .env MAIL_MAILER=smtp MAIL_HOST=127.0.0.1 MAIL_PORT=1025 ``` TelescopeのMailタブでは送信メールのHTML/テキスト両方をプレビューできます。Mailpitでは受信したメールをブラウザで確認でき、さらに添付ファイルの確認やスパムスコアの確認も可能です。 テスト中に誤って外部にメールを送らないよう、ローカルでは必ずMailpitやMailhogなどのローカルSMTPを使いましょう。 *** ## イベントとリスナーのデバッグ イベントドリブンな実装のデバッグはしばしば難しくなります。「イベントは発火しているのか」「どのリスナーが呼ばれているのか」をログを見ながら追うのは面倒です。 TelescopeのEventsウォッチャーを使うと、発行されたイベントとそのリスナーが一覧で確認できます。 リスナーが呼ばれない場合、Eventsタブでイベントのエントリを確認します。 * イベントは発行されているがリスナーが表示されない → リスナーが登録されていない(`EventServiceProvider` の確認) * イベント自体が発行されていない → `event()` の呼び出し箇所の確認 ```php theme={null} // テスト代わりにローカルで確認する流れ event(new OrderShipped($order)); // → /telescope/events で OrderShipped が表示されるか確認 // → リスナー SendShippingConfirmation が呼ばれているか確認 ``` *** ## キュージョブのデバッグ 非同期処理のデバッグはログを見るだけでは原因特定が難しいです。TelescopeのJobsウォッチャーは、ジョブのディスパッチから実行結果まで記録します。 失敗したジョブのエントリをクリックすると、スタックトレースと例外メッセージが確認できます。`queue:failed` テーブルとは別に、Telescopeでも確認できるため、失敗の原因調査が素早く行えます。 ```php theme={null} // ジョブのデバッグ時はsyncドライバーで同期実行すると追跡しやすい // .env QUEUE_CONNECTION=sync ``` syncドライバーにすることで、ジョブがリクエスト内で同期実行され、Requestsウォッチャーにジョブの実行結果も含まれます。 *** ## HTTPクライアントのデバッグ 外部APIとの通信をデバッグする際、HTTP Client Watcherが役立ちます。`Http::` ファサードで行われたリクエストと、そのレスポンスが記録されます。 ```php theme={null} $response = Http::get('https://api.example.com/users'); // → /telescope/client-requests で確認できる ``` レスポンスの内容、ステータスコード、実行時間が一目でわかります。`dd($response->json())` を書かなくてもダッシュボードで確認できます。 *** ## まとめ | テクニック | 効果 | | - | - | | N+1ワークフロー | dd()不要でクエリ問題を体系的に発見・修正 | | タグ付け | 特定ユーザー・モデルの記録をすぐに絞り込み | | Dumpウォッチャー | APIレスポンスを汚さずにdump出力を確認 | | Mailpit連携 | ローカルメール開発の快適化 | | Eventsウォッチャー | イベント/リスナーの発火を視覚的に確認 | | Jobs + sync | 非同期処理を同期実行してデバッグ | | HTTPクライアント | 外部API通信の記録と確認 | インストールとウォッチャー設定の詳細はガイドページを参照してください。 本番環境の監視はNightwatchで行います。 # laravel/ui から Fortify への移行ガイド Source: https://kawax.biz/jp/blog/ui-to-fortify laravel/ui の認証スキャフォールドを使う既存プロジェクト向けに、認証バックエンドだけを Fortify へ移行する手順を解説します。 ## なぜ `laravel/ui` から移行するのか `laravel/ui` は Laravel 13 でも動作します。実際に `laravel/ui` の最新リリースは Laravel 13 compatibility を含んでいます。 ただし、現行の公式スターターキットは Fortify ベースです。`laravel/ui` のまま運用を続けると、公式の現在地と少しずつズレていきます。\ あなたが既存の Bootstrap + Blade 構成を維持しながら将来の保守性を上げたいなら、認証バックエンドを Fortify に寄せる移行が有効です。 ## 移行の全体像 この移行は **認証バックエンドだけ** を置き換えます。UI の大規模リニューアルは不要です。 * Fortify で認証ルート・認証処理を提供する * 既存の Bootstrap + Blade テンプレートは活かす * 必要な範囲でフォームの `action` や入力名を調整する Fortify は headless な認証バックエンドです。Fortify 自体は UI を提供しないので、既存の Blade をそのまま活用できます。 ## Fortify のインストール手順 ```bash theme={null} composer require laravel/fortify ``` ```bash theme={null} php artisan fortify:install ``` ```bash theme={null} php artisan migrate ``` `routes/web.php` の `Auth::routes();` や、`Auth::routes([...])` を削除します。Fortify が `/login`、`/register`、`/forgot-password` などのルートを提供するため、重複を避ける必要があります。 ## `FortifyServiceProvider` を設定する `php artisan fortify:install` で公開された `app/Providers/FortifyServiceProvider.php` でビューを紐付けます。\ 既存の `resources/views/auth` をそのまま使う場合は、次の設定が移行のポイントです。 ```php theme={null} use Laravel\Fortify\Fortify; public function boot(): void { Fortify::viewPrefix('auth.'); Fortify::requestPasswordResetLinkView('auth.passwords.email'); Fortify::resetPasswordView('auth.passwords.reset'); Fortify::confirmPasswordView('auth.passwords.confirm'); Fortify::verifyEmailView('auth.verify'); } ``` ## Blade テンプレートを最小修正する 移行後は、既存 Blade で次の点を確認してください。 * フォームの `action` が Fortify のエンドポイントに向いているか(例: `/login`, `/register`, `/forgot-password`)。ほとんどは`route('login')`や`route('register')`のルート名での指定なので修正箇所は少ないはずです。`route(verification.resend')`だけはFortifyでは`route('verification.send')`なので修正が必要です。 `auth/passwords/reset.balde.php`は2箇所修正が必要です。 ``` ``` ↓ ``` ``` ``` {{ $email ?? old('email') }} ``` ↓ ``` {{ old('email', $request->email) }} ``` ## 不要な機能を無効化 `laravel/ui`になかった機能は使えないので`config/fortify.php`で無効化します。新しくviewファイルを用意すれば有効化できます。 ```php theme={null} 'features' => [ Features::registration(), Features::resetPasswords(), // Features::emailVerification(), // Features::updateProfileInformation(), // Features::updatePasswords(), // Features::twoFactorAuthentication([ // 'confirm' => true, // 'confirmPassword' => true, // 'window' => 0, // ]), // Features::passkeys([ // 'confirmPassword' => true, // ]), ], ``` ## 動作確認チェックリスト 移行後は、少なくとも次を確認してください。 既存ユーザーでログインし、セッションが正しく維持されることを確認します。 新規ユーザー登録が通り、想定した画面へリダイレクトされることを確認します。 リセットメール送信、トークン付きリンク、再設定完了まで一連のフローを確認します。 ## 移行後の利点 この移行を行うと、あなたのアプリは現行スターターキットと同じ Fortify ベースに近づきます。\ その結果、将来的に 2FA やパスキー(Passkeys)を有効化しやすくなります。 Fortify の内部設計、2FA、Passkeys の有効化まで進めたい場合はこのページを参照してください。 ## 参考リンク * [Laravel Fortify リポジトリ](https://github.com/laravel/fortify) * [Laravel 13.x Fortify ドキュメント](https://github.com/laravel/docs/blob/13.x/fortify.md) * [React Starter Kit の FortifyServiceProvider](https://github.com/laravel/react-starter-kit/blob/main/app/Providers/FortifyServiceProvider.php) # Laravel 10 から 11 へのアップグレード Source: https://kawax.biz/jp/blog/upgrade-10-to-11 Laravel 10から11へのアップグレード手順と主要な変更点の解説 ## はじめに Laravel 11 は 2024年3月にリリースされました。このガイドでは Laravel 10.x から 11.x へのアップグレード手順を説明します。 アップグレードに必要な推定時間は **約15分** です。ただし、破壊的変更がアプリケーションに与える影響は規模や使用している機能によって異なります。 ### Laravel Shift を使った自動アップグレード [Laravel Shift](https://laravelshift.com/) を使ってアップグレードを自動化することもできます。Shift はアプリケーションの依存関係や設定ファイルを自動的に更新してくれます。 *** ## 影響度別の変更点 ### 影響度: 高 * 依存関係の更新 * アプリケーション構造の変更 * 浮動小数点型の変更 * カラム変更時の属性保持 * SQLite 最小バージョン * Sanctum の更新 ### 影響度: 中 * Carbon 3 * パスワードの再ハッシュ * 秒単位のレート制限 * Spatie Once パッケージ ### 影響度: 低 * Doctrine DBAL の削除 * Eloquent モデルの `casts` メソッド * 空間型の変更 * `Enumerable` コントラクト * `UserProvider` コントラクト * `Authenticatable` コントラクト *** ## アップグレード手順 ### 依存関係の更新 **影響度: 高** `composer.json` の以下の依存関係を更新してください。 ```json theme={null} { "require": { "laravel/framework": "^11.0", "nunomaduro/collision": "^8.1" } } ``` パッケージを使用している場合はあわせて更新してください。 ```json theme={null} { "require": { "laravel/breeze": "^2.0", "laravel/cashier": "^15.0", "laravel/dusk": "^8.0", "laravel/jetstream": "^5.0", "laravel/octane": "^2.3", "laravel/passport": "^12.0", "laravel/sanctum": "^4.0", "laravel/scout": "^10.0", "laravel/spark-stripe": "^5.0", "laravel/telescope": "^5.0", "livewire/livewire": "^3.4", "inertiajs/inertia-laravel": "^1.0" } } ``` 更新後、以下のコマンドで依存関係をインストールします。 ```shell theme={null} composer update ``` Cashier Stripe、Passport、Sanctum、Spark Stripe、Telescope を使用している場合は、マイグレーションをアプリケーションにパブリッシュする必要があります。 ```shell theme={null} php artisan vendor:publish --tag=cashier-migrations php artisan vendor:publish --tag=passport-migrations php artisan vendor:publish --tag=sanctum-migrations php artisan vendor:publish --tag=spark-migrations php artisan vendor:publish --tag=telescope-migrations ``` Laravel インストーラーを使用している場合は、あわせて更新してください。 ```shell theme={null} composer global require laravel/installer:^5.6 ``` 以前 `doctrine/dbal` を手動で追加していた場合は削除できます。Laravel 11 ではこのパッケージへの依存がなくなりました。 *** ## PHPバージョン要件 **影響度: 高** Laravel 11 では **PHP 8.2.0 以上** が必要です。また、Laravel の HTTP クライアントには **curl 7.34.0 以上** が必要です。 *** ## アプリケーション構造の変更 **影響度: 高** Laravel 11 ではデフォルトのアプリケーション構造が簡素化されました。サービスプロバイダー、ミドルウェア、設定ファイルの数が大幅に削減されています。 ただし、Laravel 10 のアプリケーションを Laravel 11 にアップグレードする際に、アプリケーション構造の移行を試みることは **推奨しません**。Laravel 11 は Laravel 10 のアプリケーション構造もサポートするように設計されています。 主な構造的変更点: * `bootstrap/app.php` でミドルウェア、例外ハンドラー、ルーティングを直接設定するように変更 * `app/Http/Kernel.php` が廃止され `bootstrap/app.php` に統合 * デフォルトのサービスプロバイダー数が削減され `bootstrap/providers.php` で管理 * `config/` ディレクトリのファイル数が削減(必要に応じて `php artisan config:publish` で公開可能) *** ## 破壊的変更 (Breaking Changes) ### 認証 #### パスワードの再ハッシュ **影響度: 中** Laravel 11 では、認証時にハッシュアルゴリズムの「ワークファクター」が変更されていた場合、自動的にパスワードを再ハッシュします。 `User` モデルのパスワードフィールドの名前が `password` 以外の場合は、モデルの `authPasswordName` プロパティで指定してください。 ```php theme={null} class User extends Authenticatable { protected $authPasswordName = 'custom_password_field'; } ``` この機能を無効にするには、`config/hashing.php` に以下を追加してください。 ```php theme={null} 'rehash_on_login' => false, ``` #### `UserProvider` コントラクト **影響度: 低** `Illuminate\Contracts\Auth\UserProvider` コントラクトに `rehashPasswordIfRequired` メソッドが追加されました。このインターフェースを実装しているクラスがある場合は、メソッドを追加してください。 ```php theme={null} public function rehashPasswordIfRequired(Authenticatable $user, array $credentials, bool $force = false); ``` #### `Authenticatable` コントラクト **影響度: 低** `Illuminate\Contracts\Auth\Authenticatable` コントラクトに `getAuthPasswordName` メソッドが追加されました。このインターフェースを実装しているクラスがある場合は、メソッドを追加してください。 ```php theme={null} public function getAuthPasswordName() { return 'password'; } ``` *** ### データベース #### SQLite 最小バージョン **影響度: 高** SQLite を使用している場合は **SQLite 3.26.0 以上** が必要です。 なお、Laravel 11 の新規プロジェクトではデフォルトのデータベースドライバーが SQLite に変更されました。 #### カラム変更時の属性保持 **影響度: 高** カラムを変更する際、変更後も保持したいすべての修飾子を明示的に指定する必要があります。指定されていない属性は削除されます。 ```php theme={null} // Laravel 10 では unsigned、default、comment が保持された Schema::table('users', function (Blueprint $table) { $table->integer('votes')->nullable()->change(); }); // Laravel 11 では明示的にすべての属性を指定する必要がある Schema::table('users', function (Blueprint $table) { $table->integer('votes') ->unsigned() ->default(1) ->comment('The vote count') ->nullable() ->change(); }); ``` 既存のマイグレーションをすべて更新したくない場合は、マイグレーションをスカッシュしてください。 ```shell theme={null} php artisan schema:dump ``` #### 浮動小数点型の変更 **影響度: 高** `double` と `float` のマイグレーションカラム型が全データベースで統一されました。 ```php theme={null} // double: 合計桁数・小数点以下桁数の引数が不要に $table->double('amount'); // float: 精度を指定する任意の引数のみ $table->float('amount', precision: 53); ``` `unsignedDecimal`、`unsignedDouble`、`unsignedFloat` メソッドは削除されました。引き続き `unsigned` 属性を使う場合はメソッドチェーンで指定してください。 ```php theme={null} $table->decimal('amount', total: 8, places: 2)->unsigned(); $table->double('amount')->unsigned(); $table->float('amount', precision: 53)->unsigned(); ``` #### Eloquent モデルの `casts` メソッド **影響度: 低** 基底の Eloquent モデルクラスに `casts` メソッドが定義されました。アプリケーションのモデルで `casts` というリレーションを定義している場合は名前が競合するため変更が必要です。 #### MariaDB 専用ドライバー **影響度: 非常に低** Laravel 11 では MariaDB 用の専用データベースドライバーが追加されました。MariaDB に接続する場合は、設定で `mariadb` ドライバーを使用できます。 ```php theme={null} 'driver' => 'mariadb', ``` #### 空間型の変更 **影響度: 低** 空間カラム型が全データベースで統一されました。`point`、`lineString`、`polygon` などのメソッドの代わりに `geometry` または `geography` を使用してください。 ```php theme={null} $table->geometry('shapes'); $table->geography('coordinates'); ``` 型や空間参照システムを明示的に指定する場合は `subtype` と `srid` を渡してください。 ```php theme={null} $table->geometry('dimension', subtype: 'polygon', srid: 0); $table->geography('latitude', subtype: 'point', srid: 4326); ``` #### Doctrine DBAL の削除 **影響度: 低** Laravel は Doctrine DBAL への依存をなくしました。以下のクラスやメソッドが削除されています。 * `Schema\Builder::useNativeSchemaOperationsIfPossible()` * `Connection::getDoctrineConnection()` * `Connection::getDoctrineSchemaManager()` * `Connection::registerDoctrineType()` * `DatabaseManager::registerDoctrineType()` * `Schema\Grammars\ChangeColumn` クラス * `Schema\Grammars\RenameColumn` クラス データベースの検査には `Schema::getTables()`、`Schema::getColumns()`、`Schema::getIndexes()`、`Schema::getForeignKeys()` などの新しいネイティブメソッドを使用してください。 *** ### 日付 #### Carbon 3 **影響度: 中** Laravel 11 は Carbon 2 と Carbon 3 の両方をサポートします。Carbon 3 にアップグレードする場合、`diffIn*` メソッドが浮動小数点数を返すようになり、負の値で時間の方向を表すことに注意してください。 *** ### レート制限 #### 秒単位のレート制限 **影響度: 中** Laravel 11 では分単位ではなく秒単位のレート制限がサポートされました。`GlobalLimit`、`Limit` クラスのコンストラクターが秒単位を受け取るようになっています。 ```php theme={null} // 変更前 (分単位) new GlobalLimit($attempts, 2); // 2分 // 変更後 (秒単位) new GlobalLimit($attempts, 2 * 60); // 120秒 ``` `Limit` クラスの `decayMinutes` プロパティは `decaySeconds` に改名され、秒単位になりました。 `ThrottlesExceptions` と `ThrottlesExceptionsWithRedis` のコンストラクターも秒単位を受け取るようになりました。 ```php theme={null} new ThrottlesExceptions($attempts, 2 * 60); new ThrottlesExceptionsWithRedis($attempts, 2 * 60); ``` *** ### パッケージ #### サービスプロバイダーのパブリッシュ **影響度: 非常に低** Laravel 11 の新規アプリケーションには `config/app.php` の `providers` 配列がありません。パッケージでサービスプロバイダーをパブリッシュしている場合は、`ServiceProvider::addProviderToBootstrapFile` メソッドを使用してください。 ```php theme={null} use Illuminate\Support\ServiceProvider; ServiceProvider::addProviderToBootstrapFile(Provider::class); ``` *** ### Sanctum #### Sanctum のアップデート **影響度: 高** Laravel 11 は Sanctum 3.x をサポートしません。`composer.json` の Sanctum 依存関係を `^4.0` に更新してください。 Sanctum 4.0 はマイグレーションを自動でロードしなくなりました。以下のコマンドでパブリッシュしてください。 ```shell theme={null} php artisan vendor:publish --tag=sanctum-migrations ``` また、`config/sanctum.php` のミドルウェア設定を更新してください。 ```php theme={null} 'middleware' => [ 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, ], ``` *** ### Spatie Once パッケージ **影響度: 中** Laravel 11 は独自の [`once` 関数](https://laravel.com/docs/11.x/helpers#method-once) を提供するようになりました。`spatie/once` パッケージを使用している場合は、競合を避けるために `composer.json` から削除してください。 *** ## まとめ Laravel 11 は大きな構造変更を含むバージョンですが、Laravel 10 のアプリケーション構造はそのまま動作するため、段階的に移行できます。 | 変更点 | 影響度 | 対応 | | - | - | - | | `composer.json` の依存関係更新 | 高 | `laravel/framework ^11.0` に変更 | | PHP 8.2 必須 | 高 | PHP のバージョンを確認 | | カラム変更時の属性保持 | 高 | `change()` 使用箇所を確認 | | 浮動小数点型の変更 | 高 | `double`/`float` カラムの定義を確認 | | SQLite 3.26.0+ 必須 | 高 | SQLite のバージョンを確認 | | Sanctum `^4.0` | 高 | マイグレーションをパブリッシュ | | Carbon 3 | 中 | `diffIn*` メソッドの戻り値を確認 | | 秒単位のレート制限 | 中 | `decayMinutes` を `decaySeconds` に変更 | | Doctrine DBAL の削除 | 低 | ネイティブスキーマメソッドに移行 | *** ## 参考資料 * [公式アップグレードガイド (英語)](https://laravel.com/docs/11.x/upgrade) * [laravel/laravel リポジトリの差分 (10.x → 11.x)](https://github.com/laravel/laravel/compare/10.x...11.x) * [Laravel Shift](https://laravelshift.com) — アップグレードを自動化するコミュニティサービス * [Carbon 3 変更ログ](https://github.com/briannesbitt/Carbon/releases/tag/3.0.0) # Laravel 11から12へのアップグレードガイド Source: https://kawax.biz/jp/blog/upgrade-11-to-12 Laravel 11から12へのアップグレード手順と主な変更点を解説します ## はじめに Laravel 12 は 2025年2月にリリースされました。このガイドでは Laravel 11.x から 12.x へのアップグレード手順を説明します。 アップグレードに必要な推定時間は **約5分** です。ただし、破壊的変更がアプリケーションに与える影響は規模や使用している機能によって異なります。 ### Laravel Shift を使った自動アップグレード [Laravel Shift](https://laravelshift.com/) を使ってアップグレードを自動化することもできます。Shift はアプリケーションの依存関係や設定ファイルを自動的に更新してくれます。 *** ## 影響度別の変更点 ### 影響度: 高 * 依存関係の更新 * Laravel インストーラーの更新 ### 影響度: 中 * Eloquent `HasUuids` トレイトと UUIDv7 ### 影響度: 低 * Carbon 3 * Concurrency の結果インデックスマッピング * コンテナのクラス依存解決 * 画像バリデーションの SVG 除外 * ローカルファイルシステムディスクのデフォルトルートパス * マルチスキーマのデータベースインスペクション * ネストされた配列リクエストのマージ *** ## アップグレード手順 ### 依存関係の更新 **影響度: 高** `composer.json` の以下の依存関係を更新してください。 ```json theme={null} { "require": { "laravel/framework": "^12.0" }, "require-dev": { "phpunit/phpunit": "^11.0", "pestphp/pest": "^3.0" } } ``` 更新後、以下のコマンドで依存関係をインストールします。 ```shell theme={null} composer update ``` *** ### Laravel インストーラーの更新 **影響度: 高** Laravel インストーラー CLI を使って新しい Laravel アプリケーションを作成する場合は、Laravel 12.x および[新しいスターターキット](https://laravel.com/starter-kits)に対応したバージョンに更新してください。 `composer global require` でインストールした場合: ```shell theme={null} composer global update laravel/installer ``` `php.new` でインストールした場合は、OS に合わせて再インストールコマンドを実行してください。 ```shell theme={null} /bin/bash -c "$(curl -fsSL https://php.new/install/mac/8.4)" ``` ```shell theme={null} # 管理者として実行 Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://php.new/install/windows/8.4')) ``` ```shell theme={null} /bin/bash -c "$(curl -fsSL https://php.new/install/linux/8.4)" ``` [Laravel Herd](https://herd.laravel.com) のバンドル版を使用している場合は、Herd 自体を最新リリースに更新してください。 *** ## 新スターターキット Laravel 12 では、従来の Breeze や Jetstream に代わる新しいスターターキットが導入されました。`laravel new` を実行すると、フロントエンドスタックを対話的に選択できます。 利用可能なスターターキット: * **React** — Inertia 2、React 19、TypeScript、Tailwind 4、shadcn/ui * **Vue** — Inertia 2、Vue Composition API、TypeScript、Tailwind 4、shadcn-vue * **Svelte** — Inertia 2、Svelte 5、TypeScript、Tailwind 4、shadcn-svelte * **Livewire** — Livewire、Tailwind 4、Flux UI ```shell theme={null} laravel new my-app ``` コマンドを実行するとスターターキットの選択が促されます。選択後は依存関係をインストールして開発サーバーを起動するだけです。 ```shell theme={null} cd my-app npm install && npm run build composer run dev ``` 既存のアプリケーションをアップグレードする場合、スターターキットを導入し直す必要はありません。新規プロジェクト向けの変更です。 *** ## 破壊的変更 (Breaking Changes) ### Eloquent #### `HasUuids` トレイトと UUIDv7 **影響度: 中** `HasUuids` トレイトが UUID v7 (順序付き UUID) を返すようになりました。以前の順序付き UUID v4 の動作を維持したい場合は、`HasVersion4Uuids` トレイトに切り替えてください。 ```php theme={null} use Illuminate\Database\Eloquent\Concerns\HasUuids; // [tl! remove] use Illuminate\Database\Eloquent\Concerns\HasVersion4Uuids as HasUuids; // [tl! add] ``` 以前から `HasVersion7Uuids` トレイトを使用していた場合は、`HasUuids` トレイトに変更してください(同じ動作になりました)。 *** ### バリデーション #### 画像バリデーションの SVG 除外 **影響度: 低** `image` バリデーションルールがデフォルトで SVG を許可しなくなりました。SVG を許可する場合は明示的に指定してください。 ```php theme={null} use Illuminate\Validation\Rules\File; 'photo' => 'required|image:allow_svg' // または 'photo' => ['required', File::image(allowSvg: true)], ``` *** ### ストレージ #### ローカルファイルシステムディスクのデフォルトルートパス **影響度: 低** `filesystems` 設定で `local` ディスクを明示的に定義していない場合、デフォルトのルートパスが `storage/app` から `storage/app/private` に変更されました。 以前の動作を維持するには、設定ファイルで `local` ディスクを明示的に定義してください。 ```php theme={null} // config/filesystems.php 'local' => [ 'driver' => 'local', 'root' => storage_path('app'), ], ``` *** ### データベース #### マルチスキーマのデータベースインスペクション **影響度: 低** `Schema::getTables()`、`Schema::getViews()`、`Schema::getTypes()` がデフォルトですべてのスキーマの結果を返すようになりました。特定のスキーマのみを対象にするには `schema` 引数を渡してください。 ```php theme={null} // すべてのスキーマのテーブル $tables = Schema::getTables(); // 'main' スキーマのテーブルのみ $tables = Schema::getTables(schema: 'main'); ``` `Schema::getTableListing()` がデフォルトでスキーマ修飾されたテーブル名を返すようになりました。 ```php theme={null} $tables = Schema::getTableListing(); // ['main.migrations', 'main.users', 'blog.posts'] $tables = Schema::getTableListing(schema: 'main', schemaQualified: false); // ['migrations', 'users'] ``` #### データベースコンストラクターシグネチャの変更 **影響度: 非常に低** `Illuminate\Database\Schema\Blueprint` と `Illuminate\Database\Grammar` のコンストラクターが `Connection` インスタンスを第一引数として要求するようになりました。 ```php theme={null} // Laravel <= 11.x $grammar = new MySqlGrammar; $grammar->setConnection($connection); // Laravel >= 12.x $grammar = new MySqlGrammar($connection); ``` また、以下の API が削除または非推奨になりました。 | API | 状態 | | - | - | | `Blueprint::getPrefix()` | 非推奨 | | `Connection::withTablePrefix()` | 削除 | | `Grammar::getTablePrefix()` / `setTablePrefix()` | 非推奨 | | `Grammar::setConnection()` | 削除 | テーブルプレフィックスはコネクションから直接取得してください。 ```php theme={null} $prefix = $connection->getTablePrefix(); ``` *** ### Concurrency #### 結果インデックスマッピング **影響度: 低** `Concurrency::run()` に連想配列を渡した場合、結果がそれぞれのキーと対応して返されるようになりました。 ```php theme={null} $result = Concurrency::run([ 'task-1' => fn () => 1 + 1, 'task-2' => fn () => 2 + 2, ]); // ['task-1' => 2, 'task-2' => 4] ``` *** ### コンテナ #### クラス依存解決のデフォルト値 **影響度: 低** DIコンテナが、クラスインスタンスを解決する際にクラスプロパティのデフォルト値を尊重するようになりました。 ```php theme={null} class Example { public function __construct(public ?Carbon $date = null) {} } $example = resolve(Example::class); // Laravel <= 11.x: Carbon インスタンスを返す // Laravel >= 12.x: null を返す ``` *** ### リクエスト #### ネストされた配列リクエストのマージ **影響度: 低** `$request->mergeIfMissing()` でドット記法を使ったネストされた配列データをマージできるようになりました。以前はドット記法のキーがそのまま最上位のキーとして作成されていました。 ```php theme={null} $request->mergeIfMissing([ 'user.last_name' => 'Otwell', ]); ``` *** ### ルーティング #### ルートの優先順位 **影響度: 低** 同じ名前を持つ複数のルートがある場合の動作が、キャッシュあり・なしで統一されました。キャッシュなしのルーティングでも、最後ではなく最初に登録されたルートが優先されるようになりました。 *** ### 認証 #### `DatabaseTokenRepository` コンストラクターの変更 **影響度: 非常に低** `Illuminate\Auth\Passwords\DatabaseTokenRepository` のコンストラクターが `$expires` パラメーターを分単位ではなく秒単位で受け取るようになりました。 *** ### Carbon 3 **影響度: 低** Carbon 2.x のサポートが削除されました。Laravel 12 のアプリケーションはすべて [Carbon 3.x](https://carbon.nesbot.com/guide/getting-started/migration.html) が必要です。 *** ## まとめ Laravel 12 は比較的スムーズにアップグレードできるバージョンです。主な注意点をまとめます。 | 変更点 | 影響度 | 対応 | | - | - | - | | `composer.json` の依存関係更新 | 高 | `laravel/framework ^12.0` に変更 | | Laravel インストーラーの更新 | 高 | `composer global update` | | `HasUuids` → UUIDv7 | 中 | 必要なら `HasVersion4Uuids` に切り替え | | `image` バリデーションの SVG 除外 | 低 | SVG を許可する場合は `allow_svg` を追加 | | ローカルディスクのデフォルトルート変更 | 低 | 設定ファイルで明示的に指定 | | Carbon 3 必須 | 低 | API 互換性を確認 | *** ## 参考資料 * [公式アップグレードガイド (英語)](https://laravel.com/docs/12.x/upgrade) * [laravel/laravel リポジトリの差分 (11.x → 12.x)](https://github.com/laravel/laravel/compare/11.x...12.x) * [新しいスターターキット](https://laravel.com/starter-kits) * [Laravel Shift](https://laravelshift.com) — アップグレードを自動化するコミュニティサービス * [Carbon 3 移行ガイド](https://carbon.nesbot.com/guide/getting-started/migration.html) # Laravel 12 から 13 へのアップグレードガイド Source: https://kawax.biz/jp/blog/upgrade-12-to-13 Laravel 12 から Laravel 13 へのアップグレード手順、破壊的変更、非推奨機能、および新機能のハイライトを解説します。 ## はじめに Laravel 13 は 2026年3月にリリースされました。このガイドでは Laravel 12.x から 13.x へのアップグレード手順を説明します。 アップグレードに必要な推定時間は **約10分** です。ただし、破壊的変更がアプリケーションに与える影響は規模や使用している機能によって異なります。 ### AIを使ったアップグレード [Laravel Boost](https://github.com/laravel/boost) を使ってアップグレードを自動化することもできます。Boost はファーストパーティの MCP サーバーで、AI アシスタントに段階的なアップグレードプロンプトを提供します。Laravel 12 アプリケーションにインストール後、Claude Code、Cursor、OpenCode、Gemini、VS Code で `/upgrade-laravel-v13` スラッシュコマンドを使用して Laravel 13 へのアップグレードを開始できます。このコマンドには `laravel/boost ^2.0` が必要です。 スラッシュコマンドが使えない AI ツールでも、プロンプトファイルを直接参照することで同じアップグレード手順を実行できます。以下のプロンプトをそのまま AI に貼り付けてください。 ```text プロンプト theme={null} Laravel Boostが提供しているプロンプトを読み込んでLaravel 12から13へのアップグレード作業を行なってください。 https://raw.githubusercontent.com/laravel/boost/refs/heads/main/src/Mcp/Prompts/UpgradeLaravelv13/upgrade-laravel-v13.blade.php - `laravel/laravel`スケルトンの変更点を反映させる。masterではなく13.xブランチを確認する。 - `database/migrations/*_create_cache_table.php`はそのまま変更ではなく新しいマイグレーションを作成。 - `config/session.php`の`serialization`は`php`を設定。`'serialization' => 'php'` - Laravel13で一番アプリにエラーを発生させる変更はキャッシュの動作変更なのでプロジェクト内のキャッシュの使用箇所を検索して許可しても問題なさそうならconfig/cache.phpの`serializable_classes`で許可するように更新。 'serializable_classes' => [ App\Data\CachedDashboardStats::class, App\Support\CachedPricingSnapshot::class, Illuminate\Support\Collection::class, Illuminate\Database\Eloquent\Collection::class, ], ``` *** ## 影響度別の変更点 ### 影響度: 高 * 依存関係の更新 * Laravel インストーラーの更新 * リクエスト偽造防止 (CSRF) ### 影響度: 中 * キャッシュ `serializable_classes` 設定 * セッション `serialization` 設定 ### 影響度: 低 * キャッシュプレフィックスとセッション Cookie 名 * コレクションモデルのシリアライゼーション * `Container::call` と Nullable クラスデフォルト * ドメインルート登録の優先順位 * `JobAttempted` イベントの例外ペイロード * Manager `extend` コールバックのバインディング * MySQL `DELETE` クエリ (JOIN / ORDER BY / LIMIT) * ページネーション Bootstrap ビュー名 * ポリモーフィックピボットテーブル名の生成 * `QueueBusy` イベントのプロパティ名変更 * `Str` ファクトリのテスト間リセット *** ## アップグレード手順 ### 依存関係の更新 **影響度: 高** `composer.json` の以下の依存関係を更新してください。 ```json theme={null} { "require": { "laravel/framework": "^13.0", "laravel/tinker": "^3.0" }, "require-dev": { "phpunit/phpunit": "^12.0", "pestphp/pest": "^4.0" } } ``` Laravel Boost を使用している場合は合わせて更新します。 ```json theme={null} { "require": { "laravel/boost": "^2.0" } } ``` 更新後、以下のコマンドで依存関係をインストールします。 ```shell theme={null} composer update ``` *** ### Laravel インストーラーの更新 **影響度: 高** Laravel インストーラー CLI を使って新しい Laravel アプリケーションを作成する場合は、Laravel 13.x 対応のバージョンに更新してください。 `composer global require` でインストールした場合: ```shell theme={null} composer global update laravel/installer ``` [Laravel Herd](https://herd.laravel.com) のバンドル版を使用している場合は、Herd 自体を最新リリースに更新してください。 *** ## 破壊的変更 (Breaking Changes) ### セキュリティ #### リクエスト偽造防止 **影響度: 高** Laravel の CSRF ミドルウェアが `VerifyCsrfToken` から `PreventRequestForgery` にリネームされました。また、`Sec-Fetch-Site` ヘッダーを使ったリクエスト発信元の検証が追加されています。 `VerifyCsrfToken` と `ValidateCsrfToken` は非推奨のエイリアスとして残っていますが、直接参照している箇所はすべて `PreventRequestForgery` に更新する必要があります。特にテストやルート定義でミドルウェアを除外している場合は注意が必要です。 ```php theme={null} use Illuminate\Foundation\Http\Middleware\PreventRequestForgery; use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; // Laravel <= 12.x ->withoutMiddleware([VerifyCsrfToken::class]); // Laravel >= 13.x ->withoutMiddleware([PreventRequestForgery::class]); ``` ミドルウェア設定 API でも `preventRequestForgery(...)` が使用可能になりました。 *** ### キャッシュ #### キャッシュプレフィックスとセッション Cookie 名 **影響度: 低** Laravel のデフォルトのキャッシュおよび Redis キープレフィックスがハイフン区切りのサフィックスを使用するようになりました。また、デフォルトのセッション Cookie 名が `Str::snake(...)` を使用するようになりました。 多くのアプリケーションでは設定ファイルで明示的に値を設定しているため、この変更の影響を受けません。フレームワークのフォールバック設定に依存しているアプリケーションのみ影響を受けます。 ```php theme={null} // Laravel <= 12.x Str::slug((string) env('APP_NAME', 'laravel'), '_').'_cache_'; Str::slug((string) env('APP_NAME', 'laravel'), '_').'_database_'; Str::slug((string) env('APP_NAME', 'laravel'), '_').'_session'; // Laravel >= 13.x Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'; Str::slug((string) env('APP_NAME', 'laravel')).'-database-'; Str::snake((string) env('APP_NAME', 'laravel')).'_session'; ``` 以前の動作を維持するには、`.env` ファイルで明示的に設定してください。 ```ini theme={null} CACHE_PREFIX=myapp_cache_ REDIS_PREFIX=myapp_database_ SESSION_COOKIE=myapp_session ``` #### キャッシュ `serializable_classes` 設定 **影響度: 中** デフォルトの `cache` 設定に `serializable_classes` オプションが追加され、デフォルトは `false` になりました。これにより、`APP_KEY` が漏洩した場合の PHP デシリアライゼーションガジェットチェーン攻撃を防ぎます。 アプリケーションが意図的にキャッシュに PHP オブジェクトを保存している場合、デシリアライズを許可するクラスを明示的にリストアップする必要があります。 ```php theme={null} // config/cache.php 'serializable_classes' => [ App\Data\CachedDashboardStats::class, App\Support\CachedPricingSnapshot::class, ], ``` 任意のキャッシュオブジェクトをデシリアライズしていた場合は、明示的なクラス許可リストまたは非オブジェクトのキャッシュペイロード(配列など)に移行する必要があります。 #### セッション `serialization` 設定 **影響度: 低** Laravel 13 のアプリケーションスケルトン (`laravel/laravel`) では、`config/session.php` の `serialization` が `json` に設定されるようになりました。ただし、フレームワーク内部のデフォルトは `php` のままです。 この設定を `php` から `json` に変更すると、アクティブなユーザーのセッションがすべて無効になります。また、セッションに PHP オブジェクトを保存しているアプリケーションではエラーが発生する可能性があります。 アップグレード中もアクティブなセッションを維持するには、この設定を `php` にして明示的に指定してください。アプリケーションがセッションに PHP オブジェクトを保存しておらず、ユーザーに再認証を求めても問題ない場合は、セキュリティ向上のため `json` に変更できます。 ```php theme={null} // config/session.php 'serialization' => 'php', ``` JSON シリアライゼーションを有効にする前に、アプリケーションがセッションに PHP オブジェクトを保存していないことを確認してください。 *** ### コンテナ #### `Container::call` と Nullable クラスデフォルト **影響度: 低** `Container::call` が、バインディングが存在しない場合に nullable クラスパラメータのデフォルト値を尊重するようになりました(Laravel 12 でコンストラクタインジェクションに導入された動作と一致します)。 ```php theme={null} $container->call(function (?Carbon $date = null) { return $date; }); // Laravel <= 12.x: Carbon インスタンスを返す // Laravel >= 13.x: null を返す ``` *** ### データベース #### MySQL `DELETE` クエリ **影響度: 低** Laravel が MySQL 文法で `ORDER BY` と `LIMIT` を含む完全な `DELETE ... JOIN` クエリをコンパイルするようになりました。 以前のバージョンでは、JOIN を伴う DELETE で `ORDER BY` / `LIMIT` 句が無視されることがありました。Laravel 13 ではこれらの句が生成される SQL に含まれます。その結果、この構文をサポートしない一部のデータベースエンジンで `QueryException` が発生する可能性があります。 *** ### Eloquent #### ポリモーフィックピボットテーブル名の生成 **影響度: 低** カスタムピボットモデルクラスを使用するポリモーフィックピボットモデルのテーブル名が推測される際、Laravel が複数形の名前を生成するようになりました。 以前の単数形の推測名に依存していた場合は、ピボットモデルでテーブル名を明示的に定義してください。 ```php theme={null} class RoleUser extends MorphPivot { protected $table = 'role_user'; // 明示的に指定 } ``` #### コレクションモデルのシリアライゼーション **影響度: 低** Eloquent モデルコレクションがシリアライズおよびリストア(キュー投入されたジョブなど)される際、Eager ロードされたリレーションがモデルのために復元されるようになりました。 デシリアライゼーション後にリレーションが存在しないことに依存していたコードがある場合は、修正が必要です。 *** ### キュー #### `JobAttempted` イベントの例外ペイロード **影響度: 低** `Illuminate\Queue\Events\JobAttempted` イベントが、以前の boolean の `$exceptionOccurred` プロパティに代わって、例外オブジェクト(または `null`)を `$exception` で公開するようになりました。 ```php theme={null} // Laravel <= 12.x if ($event->exceptionOccurred) { // 例外が発生した } // Laravel >= 13.x if ($event->exception !== null) { // 例外が発生した $exception = $event->exception; } ``` #### `QueueBusy` イベントのプロパティ名変更 **影響度: 低** `Illuminate\Queue\Events\QueueBusy` イベントのプロパティ `$connection` が、他のキューイベントとの一貫性のために `$connectionName` にリネームされました。 ```php theme={null} // Laravel <= 12.x $event->connection; // Laravel >= 13.x $event->connectionName; ``` *** ### ルーティング #### ドメインルート登録の優先順位 **影響度: 低** 明示的なドメインを持つルートが、ルートマッチングで非ドメインルートよりも優先されるようになりました。 これにより、キャッチオールのサブドメインルートが、非ドメインルートが先に登録されている場合でも一貫して動作するようになります。 *** ### サポート #### Manager `extend` コールバックのバインディング **影響度: 低** Manager の `extend` メソッドで登録されたカスタムドライバーのクロージャが、マネージャーインスタンスにバインドされるようになりました。 以前はこれらのコールバック内で別のオブジェクト(サービスプロバイダーインスタンスなど)が `$this` として参照されていた場合、`use (...)` で値をクロージャキャプチャに移動する必要があります。 ```php theme={null} // Laravel <= 12.x Manager::extend('custom', function ($app) { return $this->createCustomDriver($app); // $this はサービスプロバイダー }); // Laravel >= 13.x $provider = $this; Manager::extend('custom', function ($app) use ($provider) { return $provider->createCustomDriver($app); }); ``` #### `Str` ファクトリのテスト間リセット **影響度: 低** Laravel がテストのティアダウン時にカスタム `Str` ファクトリをリセットするようになりました。 カスタム UUID / ULID / ランダム文字列ファクトリがテストメソッド間で持続することに依存していた場合は、関連する各テストまたはセットアップフックで設定するようにしてください。 *** ### ビュー #### ページネーション Bootstrap ビュー名 **影響度: 低** Bootstrap 3 デフォルトの内部ページネーションビュー名が明示的になりました。 ```php theme={null} // Laravel <= 12.x pagination::default pagination::simple-default // Laravel >= 13.x pagination::bootstrap-3 pagination::simple-bootstrap-3 ``` 古いページネーションビュー名を直接参照している場合は、更新してください。 *** ## 非推奨になった機能 | 機能 | 代替手段 | | - | - | | `VerifyCsrfToken` ミドルウェア | `PreventRequestForgery` | | `ValidateCsrfToken` ミドルウェア | `PreventRequestForgery` | | `JobAttempted::$exceptionOccurred` | `JobAttempted::$exception` | | `QueueBusy::$connection` | `QueueBusy::$connectionName` | *** ## コントラクトの追加 **影響度: 非常に低** カスタム実装を持つ場合のみ影響があります。 ### `Dispatcher` コントラクト `Illuminate\Contracts\Bus\Dispatcher` コントラクトに `dispatchAfterResponse($command, $handler = null)` メソッドが追加されました。 ### `ResponseFactory` コントラクト `Illuminate\Contracts\Routing\ResponseFactory` コントラクトに `eventStream` シグネチャが追加されました。 ### `MustVerifyEmail` コントラクト `Illuminate\Contracts\Auth\MustVerifyEmail` コントラクトに `markEmailAsUnverified()` が追加されました。 ### `Queue` コントラクト `Illuminate\Contracts\Queue\Queue` コントラクトに以下のキューサイズ検査メソッドが追加されました(以前はドックブロックのみで宣言されていました)。 * `pendingSize` * `delayedSize` * `reservedSize` * `creationTimeOfOldestPendingJob` ### `Store` / `Repository` コントラクト キャッシュコントラクトに TTL 延長のための `touch` メソッドが追加されました。 ```php theme={null} // Illuminate\Contracts\Cache\Store public function touch($key, $seconds); ``` *** ## 新機能のハイライト ### AIアシストアップグレード (Laravel Boost) Laravel Boost は公式の MCP サーバーです。AI エディターと連携して `/upgrade-laravel-v13` コマンドでアップグレードを半自動化できます。 ### `Sec-Fetch-Site` ヘッダーによるリクエスト発信元検証 `PreventRequestForgery` ミドルウェアが `Sec-Fetch-Site` ヘッダーを使った追加のオリジン検証を行います。これにより CSRF 保護が強化されています。 ### キャッシュの安全なデシリアライゼーション `serializable_classes` 設定により、許可するクラスのみがデシリアライズされるようになりました。PHP デシリアライゼーション攻撃に対するセキュリティが向上しています。 ### SSE (Server-Sent Events) の `eventStream` `ResponseFactory` コントラクトに `eventStream` が追加され、Server-Sent Events のサポートが改善されました。 ### キュー可視性の向上 `Queue` コントラクトの `pendingSize`、`delayedSize`、`reservedSize` などのメソッドによって、キューの状態をより細かく監視できるようになりました。 *** ## よくある移行時の問題と解決策 ### 問題: CSRF 関連のテストが失敗する **症状:** `VerifyCsrfToken` を参照しているテストがクラスが見つからないエラーで失敗する。 **解決策:** すべての参照を `PreventRequestForgery` に更新してください。 ```php theme={null} // Before use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; ->withoutMiddleware([VerifyCsrfToken::class]); // After use Illuminate\Foundation\Http\Middleware\PreventRequestForgery; ->withoutMiddleware([PreventRequestForgery::class]); ``` ### 問題: キャッシュからオブジェクトが復元できない **症状:** キャッシュから取得したデータが `null` になる、または `UnserializationFailedException` が発生する。 **解決策:** `config/cache.php` で `serializable_classes` に使用するクラスを追加するか、キャッシュする値を配列に変換してください。 ```php theme={null} 'serializable_classes' => [ App\Models\User::class, App\Data\SomeData::class, ], ``` ### 問題: `JobAttempted` リスナーが機能しない **症状:** `$event->exceptionOccurred` が `null` や未定義エラーになる。 **解決策:** `$event->exception !== null` に変更してください。 ```php theme={null} // Before if ($event->exceptionOccurred) { ... } // After if ($event->exception !== null) { ... } ``` ### 問題: セッションが無効化される **症状:** アップグレード後にユーザーがログアウトされる。 **解決策:** デフォルトのセッション Cookie 名が変更されています。`.env` で `SESSION_COOKIE` を明示的に設定して以前の値を維持してください。 ```ini theme={null} SESSION_COOKIE=laravel_session ``` ### 問題: キャッシュキーが見つからない **症状:** アップグレード後にキャッシュミスが増加する。 **解決策:** キャッシュプレフィックスが変更されています。`.env` で `CACHE_PREFIX` を設定するか、キャッシュをクリアしてください。 ```shell theme={null} php artisan cache:clear ``` *** ## 参考資料 * [公式アップグレードガイド (英語)](https://laravel.com/docs/13.x/upgrade) * [laravel/laravel リポジトリの差分 (12.x → 13.x)](https://github.com/laravel/laravel/compare/12.x...13.x) * [Laravel Shift](https://laravelshift.com) — アップグレードを自動化するコミュニティサービス * [Laravel Boost](https://github.com/laravel/boost) — AI アシストアップグレード用 MCP サーバー # Laravel 8 から 9 へのアップグレード Source: https://kawax.biz/jp/blog/upgrade-8-to-9 Laravel 8.x から 9.x へのアップグレード手順と主要な変更点の解説 ## はじめに Laravel 9 は 2022年2月8日にリリースされました。このガイドでは Laravel 8.x から 9.x へのアップグレード手順と、影響が大きい変更点を整理します。 アップグレードに必要な推定時間は **約30分** です。ただし、メール送信・ファイルストレージ・独自キャスト・フレームワークのコアクラスのオーバーライド状況によって作業量は増える場合があります。 ### Laravel Shift を使った自動アップグレード [Laravel Shift](https://laravelshift.com/) を使ってアップグレードを自動化することもできます。Shift は `composer.json` や設定ファイルの更新を補助してくれるため、差分確認の出発点として便利です。 *** ## 影響度別の変更点 ### 影響度: 高 * 依存関係の更新 * Flysystem 3.x への移行 * Symfony Mailer への移行 ### 影響度: 中 * `BelongsToMany` の `firstOrNew` / `firstOrCreate` / `updateOrCreate` メソッド * Custom Casts と `null` の挙動 * HTTP クライアントのデフォルトタイムアウト * PHP Return Types の追加 * Postgres の `schema` 設定名変更 * `assertDeleted` メソッドの廃止 * `lang` ディレクトリの移動 * パスワードルールの変更 * `when` / `unless` メソッドの変更 * バリデーションでの未検証配列キーの扱い *** ## アップグレード手順 ### 依存関係の更新 **影響度: 高** Laravel 9 では **PHP 8.0.2 以上** が必要です。まず `composer.json` の依存関係を見直してください。 ```json theme={null} { "require": { "php": "^8.0.2", "laravel/framework": "^9.0", "spatie/laravel-ignition": "^1.0" }, "require-dev": { "nunomaduro/collision": "^6.1" } } ``` 追加で、該当するアプリケーションでは次の更新も必要です。 * `facade/ignition` を削除し、`spatie/laravel-ignition:^1.0` に置き換える * `pusher/pusher-php-server` を使っている場合は `^5.0` へ更新する * 利用中のサードパーティパッケージが Laravel 9 対応版か確認する * Vonage 通知チャンネルを使っている場合は個別のアップグレードガイドも確認する 更新後は依存関係をインストールします。 ```shell theme={null} composer update ``` *** ## PHPバージョン要件 **影響度: 高** Laravel 9 では PHP 8.0.2 以上が必須です。CI、ローカル開発環境、本番環境のすべてで PHP バージョンをそろえてからアップグレードを進めてください。 *** ### Symfony Mailer への移行 **影響度: 高** Laravel 9 の大きな変更のひとつは、2021年12月でメンテナンス終了となった SwiftMailer から Symfony Mailer への移行です。通常の `Mail::to()->send()` だけを使っているアプリケーションは影響が少ない一方で、SwiftMailer の低レベル API を直接触っている場合は確認が必要です。 #### ドライバー依存関係 ```shell theme={null} # Mailgun を使う場合のみ追加 composer require symfony/mailgun-mailer symfony/http-client # Postmark を使う場合は SwiftMailer 向けパッケージを削除して置き換える composer remove wildbit/swiftmailer-postmark composer require symfony/postmark-mailer symfony/http-client ``` #### `withSwiftMessage` から `withSymfonyMessage` へ ```php theme={null} // Laravel 8.x: SwiftMailer ベース $this->withSwiftMessage(function ($message) { $message->getHeaders()->addTextHeader('Custom-Header', 'Header Value'); }); // Laravel 9.x: Symfony Mailer ベース use Symfony\Component\Mime\Email; $this->withSymfonyMessage(function (Email $message) { $message->getHeaders()->addTextHeader('Custom-Header', 'Header Value'); }); ``` `Illuminate\Mail\Mailer` の `send`、`html`、`raw`、`plain` は `void` ではなく `Illuminate\Mail\SentMessage` を返すようになりました。また、`MessageSent` イベントの `message` プロパティには `Swift_Message` ではなく `Symfony\Component\Mime\Email` が入ります。 #### SMTP 設定の見直し Symfony Mailer では SMTP の `stream` オプションが廃止され、サポートされる設定はトップレベルに移動します。 ```php theme={null} return [ 'mailers' => [ 'smtp' => [ // Laravel 8.x の設定 'stream' => [ 'ssl' => [ 'verify_peer' => false, ], ], // Laravel 9.x の設定 'verify_peer' => false, ], ], ]; ``` `auth_mode` の明示設定も不要になりました。無効なメールアドレスを送信後に回収するのではなく、送信前にバリデーションする運用へ寄せるのが安全です。 *** ### Flysystem 3.x への移行 **影響度: 高** Laravel 9 は `Storage` ファサードの内部実装を Flysystem 1.x から 3.x へ更新しました。ファイル操作メソッドはできるだけ互換性を保っていますが、例外・戻り値・アダプター登録周りに差分があります。 #### ドライバーの追加インストール ```shell theme={null} # Amazon S3 ドライバー composer require -W league/flysystem-aws-s3-v3 "^3.0" # FTP ドライバー composer require league/flysystem-ftp "^3.0" # SFTP ドライバー composer require league/flysystem-sftp-v3 "^3.0" ``` #### `Storage` の主な挙動変更 * `put` / `write` / `writeStream` は既存ファイルをデフォルトで上書きする * 書き込み失敗時は例外ではなく `false` を返す * 存在しないファイルの読み込みは例外ではなく `null` を返す * 存在しないファイルの `delete` は `true` を返す * cached adapter は削除されたため、`disk` 設定内の `cache` キーを削除できる 以前のように書き込み失敗時に例外を使いたい場合は、`throw` オプションを設定してください。 ```php theme={null} return [ 'disks' => [ 'public' => [ 'driver' => 'local', // 書き込み失敗時に例外を投げたい場合だけ有効にする 'throw' => true, ], ], ]; ``` 独自の filesystem ドライバーを登録している場合は、`Storage::extend()` のコールバックが `Illuminate\Filesystem\FilesystemAdapter` を直接返す実装へ調整してください。 *** ### `BelongsToMany` の `firstOrNew` / `firstOrCreate` / `updateOrCreate` **影響度: 中** Laravel 8 では、これらのメソッドに渡した第1引数の属性配列が中間テーブルと比較されていました。Laravel 9 では、関連モデルのテーブルと比較されます。 ```php theme={null} // 関連モデルのテーブルに対して name を検索・更新する $user->roles()->updateOrCreate([ 'name' => 'Administrator', ]); ``` また、`firstOrCreate` は第2引数の `$values` を受け取れるようになり、他のリレーションと挙動がそろいました。 ```php theme={null} // 新規作成時だけ created_by をマージする $user->roles()->firstOrCreate([ 'name' => 'Administrator', ], [ 'created_by' => $user->id, ]); ``` *** ### Custom Casts と `null` **影響度: 中** Laravel 9 では、キャスト対象に `null` を代入した場合でも custom cast の `set` メソッドが呼ばれます。`null` を想定していない cast はアップグレード後に例外を出す可能性があります。 ```php theme={null} public function set($model, $key, $value, $attributes) { // Laravel 9 では null が渡される可能性がある if ($value === null) { return [ 'address_line_one' => null, 'address_line_two' => null, ]; } if (! $value instanceof AddressModel) { throw new InvalidArgumentException('AddressModel を渡してください。'); } return [ 'address_line_one' => $value->lineOne, 'address_line_two' => $value->lineTwo, ]; } ``` *** ### HTTP クライアントのデフォルトタイムアウト **影響度: 中** HTTP クライアントのデフォルトタイムアウトは 30 秒になりました。以前は無制限で待ち続けることがありました。 ```php theme={null} use Illuminate\Support\Facades\Http; // 長めの API 呼び出しだけ個別にタイムアウトを延長する $response = Http::timeout(120)->get('https://example.com/api/status'); ``` *** ### PHP Return Types の追加 **影響度: 中** Laravel 9 では、PHP 本体や Symfony の要件に合わせて各種コアクラスへ戻り値型が追加されました。Laravel のコアクラスを継承して `offsetGet`、`offsetSet`、`jsonSerialize`、`open`、`read` などをオーバーライドしている場合は、自前の実装にも同じ戻り値型を追加してください。 ```php theme={null} // コアクラスをオーバーライドしている場合は戻り値型を合わせる public function offsetGet($key): mixed { return parent::offsetGet($key); } ``` *** ### Postgres の `schema` 設定名変更 **影響度: 中** Postgres 接続で検索パスを設定している場合、`config/database.php` のキー名を `schema` から `search_path` へ変更してください。 ```php theme={null} return [ 'pgsql' => [ // Laravel 8.x 'schema' => 'public', // Laravel 9.x 'search_path' => 'public', ], ]; ``` *** ### `assertDeleted` から `assertModelMissing` へ **影響度: 中** モデルの削除確認に使っていた `assertDeleted` は `assertModelMissing` へ置き換えてください。 ```php theme={null} // 変更前 $this->assertDeleted($user); // 変更後 $this->assertModelMissing($user); ``` *** ### `lang` ディレクトリの移動 **影響度: 中** 新規 Laravel 9 アプリケーションでは、言語ファイルの配置先が `resources/lang` ではなくプロジェクトルートの `lang` になりました。既存アプリをそのまま動かすだけなら大きな影響は少ないですが、新しいスケルトンへ寄せる場合やパッケージで翻訳ファイルを公開している場合は見直してください。 ```php theme={null} // 固定パスではなく langPath() を使って公開先を決める $this->publishes([ __DIR__.'/../lang' => app()->langPath('vendor/package-name'), ]); ``` *** ### パスワードルールの変更 **影響度: 中** 現在ログイン中ユーザーのパスワードと一致することを検証する `password` ルールは、`current_password` にリネームされました。 ```php theme={null} // 変更前 'password' => ['required', 'password'], // 変更後 'password' => ['required', 'current_password'], ``` *** ### `when` / `unless` メソッドの変更 **影響度: 中** Laravel 8 では、`when` や `unless` にクロージャを渡すと、そのクロージャ自体が truthy と評価されるため意図せず条件分岐が実行されることがありました。Laravel 9 ではクロージャが実行され、その戻り値が条件として使われます。 ```php theme={null} $collection->when(function ($collection) { // ここで返した値が条件式として評価される return false; }, function ($collection) { // false が返ったのでこの処理は実行されない $collection->merge([1, 2, 3]); }); ``` *** ### 未検証配列キーの扱い **影響度: 中** Laravel 9 では、`validated()` が返す配列から未検証の配列キーが常に除外されます。Laravel 8 の互換挙動を維持したい場合だけ、`includeUnvalidatedArrayKeys()` を明示的に呼び出してください。 ```php theme={null} use Illuminate\Support\Facades\Validator; public function boot() { // Laravel 8 と同じ挙動に戻したい場合のみ有効にする Validator::includeUnvalidatedArrayKeys(); } ``` *** ## まとめ Laravel 8 から 9 へのアップグレードでは、PHP 8.0.2 への更新、Symfony Mailer への移行、Flysystem 3.x への対応が中心です。メール送信・ストレージ・独自キャスト・テストヘルパーを先に点検すると、アップグレード後の不具合を減らせます。 | 変更点 | 影響度 | 対応 | | - | - | - | | PHP 8.0.2 / `laravel/framework:^9.0` | 高 | `composer.json` と実行環境を更新 | | Symfony Mailer への移行 | 高 | SwiftMailer 系 API と mail 設定を確認 | | Flysystem 3.x | 高 | `Storage` の戻り値・例外・ドライバー依存関係を確認 | | `BelongsToMany` の upsert 系メソッド | 中 | 検索対象が関連モデルのテーブルになった点を確認 | | Custom Casts と `null` | 中 | `set()` が `null` を受けても壊れないよう修正 | | HTTP クライアントの 30 秒タイムアウト | 中 | 必要なリクエストだけ `timeout()` を明示 | | `assertDeleted` 廃止 | 中 | `assertModelMissing()` に置き換え | | `lang` ディレクトリ移動 | 中 | 固定パス参照を `app()->langPath()` に変更 | | `password` ルール変更 | 中 | `current_password` へ更新 | | 未検証配列キーの除外 | 中 | 必要時のみ `includeUnvalidatedArrayKeys()` を利用 | *** ## 参考資料 * [公式アップグレードガイド (英語)](https://laravel.com/docs/9.x/upgrade) * [laravel/docs 9.x `upgrade.md`](https://github.com/laravel/docs/blob/9.x/upgrade.md) * [laravel/laravel リポジトリの差分 (8.x → 9.x)](https://github.com/laravel/laravel/compare/8.x...9.x) * [Laravel Shift](https://laravelshift.com) — アップグレードを自動化するコミュニティサービス * [Symfony Mailer ドキュメント](https://symfony.com/doc/6.0/mailer.html) * [Flysystem 3 ドキュメント](https://flysystem.thephpleague.com/v3/docs/) # Laravel 9 から 10 へのアップグレード Source: https://kawax.biz/jp/blog/upgrade-9-to-10 Laravel 9.x から 10.x へのアップグレード手順と主要な変更点の解説 ## はじめに Laravel 10 は 2023年2月14日にリリースされました。このガイドでは Laravel 9.x から 10.x へのアップグレード手順を説明します。 アップグレードに必要な推定時間は **約10分** です。ただし、破壊的変更がアプリケーションに与える影響は規模や使用している機能によって異なります。 ### Laravel Shift を使った自動アップグレード [Laravel Shift](https://laravelshift.com/) を使ってアップグレードを自動化することもできます。Shift はアプリケーションの依存関係や設定ファイルを自動的に更新してくれます。 *** ## 影響度別の変更点 ### 影響度: 高 * 依存関係の更新 * minimum-stability の更新 ### 影響度: 中 * データベース式の変更 * モデルの `$dates` プロパティの削除 * Monolog 3 * Redis キャッシュタグ * サービスモッキングの変更 * 言語ディレクトリ ### 影響度: 低 * クロージャバリデーションルールのメッセージ * フォームリクエストの `after` メソッド * パブリックパスバインディング * `QueryException` コンストラクタ * レートリミッターの戻り値 * `Redirect::home` メソッドの削除 * `Bus::dispatchNow` メソッドの削除 * `registerPolicies` メソッド * ULID カラム *** ## アップグレード手順 ### 依存関係の更新 **影響度: 高** `composer.json` の以下の依存関係を更新してください。 ```json theme={null} { "require": { "laravel/framework": "^10.0", "laravel/sanctum": "^3.2", "doctrine/dbal": "^3.0", "spatie/laravel-ignition": "^2.0" } } ``` パッケージを使用している場合はあわせて更新してください。 ```json theme={null} { "require": { "laravel/passport": "^11.0", "laravel/ui": "^4.0" } } ``` PHPUnit 10 を使用する場合は、以下も更新してください。 ```json theme={null} { "require-dev": { "nunomaduro/collision": "^7.0", "phpunit/phpunit": "^10.0" } } ``` PHPUnit 10 を使用する場合は、`phpunit.xml` の `` セクションから `processUncoveredFiles` 属性を削除してください。 更新後、以下のコマンドで依存関係をインストールします。 ```shell theme={null} composer update ``` Sanctum 2.x から 3.x へのアップグレードには、[Sanctum 3.x アップグレードガイド](https://github.com/laravel/sanctum/blob/3.x/UPGRADE.md)も参照してください。 *** ### minimum-stability の更新 **影響度: 高** `composer.json` の `minimum-stability` を `stable` に更新してください。デフォルト値が `stable` であるため、この設定を削除しても構いません。 ```json theme={null} "minimum-stability": "stable" ``` *** ## PHPバージョン要件 **影響度: 高** Laravel 10 では **PHP 8.1.0 以上** および **Composer 2.2.0 以上** が必要です。 *** ## 破壊的変更 (Breaking Changes) ### アプリケーション #### パブリックパスバインディング **影響度: 低** `path.public` をコンテナにバインドしてパブリックパスをカスタマイズしている場合は、`Illuminate\Foundation\Application` の `usePublicPath` メソッドを使用するよう変更してください。 ```php theme={null} app()->usePublicPath(__DIR__.'/public'); ``` *** ### 認可 #### `registerPolicies` メソッド **影響度: 低** `AuthServiceProvider` の `registerPolicies` メソッドはフレームワークが自動で呼び出すようになりました。`boot` メソッドからこのメソッドの呼び出しを削除できます。 *** ### キャッシュ #### Redis キャッシュタグ **影響度: 中** `Cache::tags()` は Memcached を使用するアプリケーションにのみ推奨されます。Redis をキャッシュドライバーとして使用している場合は、Memcached への移行を検討してください。 *** ### データベース #### データベース式の変更 **影響度: 中** `DB::raw` で生成されるデータベース式が書き直されました。式の生の文字列値を取得するには `getValue(Grammar $grammar)` メソッドを使用する必要があります。`(string)` へのキャストは使用できなくなりました。 ```php theme={null} use Illuminate\Support\Facades\DB; $expression = DB::raw('select 1'); // 変更前 $string = (string) $expression; // 変更後 $string = $expression->getValue(DB::connection()->getQueryGrammar()); ``` エンドユーザーのアプリケーションには通常影響しませんが、データベース式を文字列にキャストしている場合は更新が必要です。 #### `QueryException` コンストラクタ **影響度: 非常に低** `Illuminate\Database\QueryException` のコンストラクタの第一引数に文字列の接続名が追加されました。この例外を手動でスローしている場合はコードを修正してください。 #### ULID カラム **影響度: 低** マイグレーションで引数なしに `ulid` メソッドを呼び出した場合、カラム名は `ulid` になります。以前のリリースでは引数なしで呼び出した場合、誤って `uuid` という名前のカラムが作成されていました。 ```php theme={null} // カラム名が "ulid" になる $table->ulid(); // カラム名を明示的に指定する場合 $table->ulid('ulid'); ``` *** ### Eloquent #### モデルの `$dates` プロパティ **影響度: 中** Eloquent モデルの非推奨プロパティ `$dates` が削除されました。`$casts` プロパティを使用してください。 ```php theme={null} // 変更前 protected $dates = ['deployed_at']; // 変更後 protected $casts = [ 'deployed_at' => 'datetime', ]; ``` *** ### ローカライゼーション #### 言語ディレクトリ **影響度: なし** 既存のアプリケーションには影響しませんが、新規の Laravel アプリケーションのスケルトンにはデフォルトで `lang` ディレクトリが含まれなくなりました。必要な場合は Artisan コマンドで公開できます。 ```shell theme={null} php artisan lang:publish ``` *** ### ログ #### Monolog 3 **影響度: 中** Laravel の Monolog 依存関係が Monolog 3.x に更新されました。アプリケーション内で Monolog を直接使用している場合は、[Monolog のアップグレードガイド](https://github.com/Seldaek/monolog/blob/main/UPGRADE.md)を確認してください。 BugSnag や Rollbar などのサードパーティロギングサービスを使用している場合は、Monolog 3.x と Laravel 10.x をサポートするバージョンへのアップグレードが必要な場合があります。 Monolog 3.x の変更点は [Monolog 3.x アップグレードガイド](https://github.com/Seldaek/monolog/blob/3.x/UPGRADE.md)で確認できます。 *** ### キュー #### `Bus::dispatchNow` メソッドの削除 **影響度: 低** 非推奨の `Bus::dispatchNow` と `dispatch_now` メソッドが削除されました。`Bus::dispatchSync` と `dispatch_sync` メソッドを使用してください。 ```php theme={null} // 変更前 Bus::dispatchNow(new MyJob()); dispatch_now(new MyJob()); // 変更後 Bus::dispatchSync(new MyJob()); dispatch_sync(new MyJob()); ``` *** ### ルーティング #### ミドルウェアエイリアス **影響度: 任意** 新規の Laravel アプリケーションでは、`App\Http\Kernel` クラスの `$routeMiddleware` プロパティが `$middlewareAliases` に改名されました。既存のアプリケーションへの適用は任意です。 #### レートリミッターの戻り値 **影響度: 低** `RateLimiter::attempt` メソッドを呼び出した場合、提供したクロージャの戻り値がそのまま返されるようになりました。何も返さないか `null` を返した場合は `true` が返されます。 ```php theme={null} $value = RateLimiter::attempt('key', 10, fn () => ['example'], 1); $value; // ['example'] ``` #### `Redirect::home` メソッドの削除 **影響度: 非常に低** 非推奨の `Redirect::home` メソッドが削除されました。明示的に名前付きルートへリダイレクトするよう変更してください。 ```php theme={null} // 変更前 return Redirect::home(); // 変更後 return Redirect::route('home'); ``` *** ### テスト #### サービスモッキング **影響度: 中** 非推奨の `MocksApplicationServices` トレイトがフレームワークから削除されました。このトレイトは `expectsEvents`、`expectsJobs`、`expectsNotifications` などのテストメソッドを提供していました。 これらのメソッドを使用している場合は、それぞれ `Event::fake`、`Bus::fake`、`Notification::fake` に移行してください。 ```php theme={null} // 変更前 $this->expectsEvents(OrderShipped::class); // 変更後 Event::fake(); // ... テストコード ... Event::assertDispatched(OrderShipped::class); ``` *** ### バリデーション #### クロージャバリデーションルールのメッセージ **影響度: 非常に低** クロージャベースのカスタムバリデーションルールで `$fail` コールバックを複数回呼び出した場合、メッセージが上書きされるのではなく配列に追加されるようになりました。 また、`$fail` コールバックがオブジェクトを返すようになりました。バリデーションクロージャの戻り値をタイプヒントしている場合は更新が必要です。 ```php theme={null} public function rules() { return [ 'name' => [ function ($attribute, $value, $fail) { $fail('validation.translation.key')->translate(); }, ], ]; } ``` #### フォームリクエストの `after` メソッド **影響度: 非常に低** フォームリクエスト内の `after` メソッドが Laravel によって予約されました。フォームリクエストで `after` メソッドを定義している場合は、新しい「バリデーション後」機能を活用するようリネームまたは変更してください。 *** ## まとめ Laravel 10 は比較的小さな変更で構成されており、アップグレードは短時間で完了できます。 | 変更点 | 影響度 | 対応 | | - | - | - | | `composer.json` の依存関係更新 | 高 | `laravel/framework ^10.0` に変更 | | PHP 8.1 / Composer 2.2 必須 | 高 | バージョンを確認・更新 | | データベース式の変更 | 中 | `(string)` キャストを `getValue()` に変更 | | モデルの `$dates` プロパティ削除 | 中 | `$casts` に移行 | | Monolog 3 | 中 | 直接使用している場合はアップグレードガイドを確認 | | Redis キャッシュタグ | 中 | Memcached への移行を検討 | | `MocksApplicationServices` 削除 | 中 | `Event::fake` 等に移行 | | `Bus::dispatchNow` 削除 | 低 | `Bus::dispatchSync` に変更 | | ULID カラム名 | 低 | マイグレーションの `ulid()` 呼び出しを確認 | | `Redirect::home` 削除 | 非常に低 | `Redirect::route('home')` に変更 | *** ## 参考資料 * [公式アップグレードガイド (英語)](https://laravel.com/docs/10.x/upgrade) * [laravel/laravel リポジトリの差分 (9.x → 10.x)](https://github.com/laravel/laravel/compare/9.x...10.x) * [Laravel Shift](https://laravelshift.com) — アップグレードを自動化するコミュニティサービス * [Sanctum アップグレードガイド (2.x → 3.x)](https://github.com/laravel/sanctum/blob/3.x/UPGRADE.md) * [Monolog 3.x アップグレードガイド](https://github.com/Seldaek/monolog/blob/3.x/UPGRADE.md) # Vue.js入門 — Inertia × Laravel で使う基礎知識 Source: https://kawax.biz/jp/blog/vue-introduction LaravelユーザーにとってもっともなじみのあるJSフレームワーク Vue.js を紹介します。Options API / Composition APIの概要から、Inertia v3 × Vue 3でのページコンポーネント・useForm・共有データまで実践的に解説します。 ## Vue.jsとは Vue.js(以下 Vue)は、ユーザーインターフェースを構築するためのプログレッシブJavaScriptフレームワークです。「プログレッシブ」とは、小さな部分から始めて必要に応じて機能を追加していけるという意味で、既存のHTMLページへの部分的な組み込みも、大規模なSPAの構築にも対応できます。 Vueの核心は**リアクティビティ**です。データが変わるとDOMが自動的に更新されるため、開発者は「いつ、どの要素を更新するか」を手動で管理する必要がありません。 このページで解説するのは Vue 3 と Inertia v3 の組み合わせです。Laravel 13 のスターターキットはこの構成をデフォルトで使用します。 ### Options API と Composition API Vue 3 はコンポーネントを書くスタイルとして **Options API** と **Composition API** の2つを提供しています。 **Options API** は Vue 2 から続く従来のスタイルです。`data`・`methods`・`computed`・`mounted` などのオプションオブジェクトでコンポーネントを定義します。 ```vue theme={null} ``` **Composition API** は Vue 3 で導入された新しいスタイルです。` ``` Inertia × Laravel のスターターキットでは ` ``` #### `v-if` — 条件分岐 ```vue theme={null} ``` Svelte の `{#if}` や React の三項演算子に相当します。 #### `v-for` — リスト描画 ```vue theme={null} ``` 効率的な差分更新のために `:key` は必ず指定します。React の `Array.map()` に相当します。 #### `v-model` — 双方向バインディング `v-model` を使うと、フォーム要素の値とリアクティブ変数を双方向に同期できます。 ```vue theme={null} ``` #### `:` (v-bind) と `@` (v-on) * `:attr="value"` — HTML 属性に動的な値をバインド(`v-bind:attr` の省略形) * `@event="handler"` — イベントリスナーを登録(`v-on:event` の省略形) ```vue theme={null} ``` *** ## ページコンポーネントの基本 Inertia のページコンポーネントは通常の Vue コンポーネントです。Laravelのコントローラーから渡したデータが props として受け取れます。 ### コントローラー ```php theme={null} // app/Http/Controllers/PostController.php use Inertia\Inertia; use App\Models\Post; class PostController extends Controller { public function index() { return Inertia::render('Posts/Index', [ 'posts' => Post::latest()->paginate(10), ]); } } ``` ### Vue ページコンポーネント ```vue theme={null} ``` `defineProps()` で props を宣言するだけで、コントローラーから渡したデータをテンプレートで使えます。REST APIを定義する必要はありません。 *** ## `Link` コンポーネント `@inertiajs/vue3` が提供する `` コンポーネントを使うと、ページ遷移が XHR で行われ、ブラウザのフルリロードを回避できます。 ```vue theme={null} ``` 通常の `
    ` タグと同じように書けますが、裏側で Inertia がページコンポーネントだけを差し替えるため SPA のような操作感になります。 *** ## `Form` コンポーネント `@inertiajs/vue3` が提供する `
    ` コンポーネントは、スターターキットの認証画面で使われているフォーム送信の推奨スタイルです。`action` と `method` を props で指定し、`v-slot` で `errors` と `processing` にアクセスします。 ### 基本的な使い方 ```vue theme={null} ``` `v-slot="{ errors, processing }"` は Vue のスコープ付きスロット構文で、`Form` コンポーネントがこれらの値を自動で算出して渡してくれます。フォームフィールドには `v-model` ではなく HTML ネイティブの `name` 属性を使い、ブラウザの標準フォームデータ収集が機能します。 ### スターターキットのパターン スターターキットは [Wayfinder](/jp/blog/wayfinder-introduction) を使ってルートをオブジェクトとして管理しています。`store.form()` はルートオブジェクトの `action` と `method` を含むオブジェクトを返し、`v-bind` で `` に spread します。 ```vue theme={null} ``` `reset-on-success` に指定したフィールドは、送信成功時に自動でリセットされます。パスワードフィールドなど送信後に空にしたいフィールドに指定します。 Wayfinder を使わない場合は `action="/login"` のように直接 URL を渡せば同じように動作します。 *** ## `useForm` ヘルパー フォーム処理には `@inertiajs/vue3` の `useForm` ヘルパーを使います。フォームの状態管理・送信・バリデーションエラー表示がシンプルに実装できます。 ### コントローラー側 ```php theme={null} // app/Http/Controllers/PostController.php class PostController extends Controller { public function store(Request $request) { $validated = $request->validate([ 'title' => ['required', 'string', 'max:255'], 'content' => ['required', 'string'], ]); Post::create($validated + ['user_id' => auth()->id()]); return redirect()->route('posts.index') ->with('success', '投稿を作成しました。'); } } ``` ### Vue フォームコンポーネント ```vue theme={null} ``` `useForm` が返すオブジェクトの主なプロパティをまとめます。 | プロパティ / メソッド | 説明 | | - | - | | `form.data` | フォームのデータオブジェクト | | `form.errors` | バリデーションエラー(フィールド名でアクセス) | | `form.processing` | 送信中は `true`(ボタン無効化に使う) | | `form.isDirty` | 初期値から変更されている場合 `true` | | `form.post(url)` | POST リクエストで送信 | | `form.put(url)` | PUT リクエストで送信(更新) | | `form.delete(url)` | DELETE リクエストで送信 | | `form.reset()` | フォームを初期値にリセット | バリデーションエラーが返ったとき、`useForm` は入力内容を保持したままエラーを表示します。`v-model` との組み合わせでシームレスなフォーム体験を実現できます。 *** ## 共有データ(Shared Data) すべてのページで共通して必要なデータ(ログイン中のユーザー情報・フラッシュメッセージなど)は `HandleInertiaRequests` ミドルウェアの `share()` メソッドで定義します。 ```php theme={null} // app/Http/Middleware/HandleInertiaRequests.php use Illuminate\Http\Request; use Inertia\Middleware; class HandleInertiaRequests extends Middleware { public function share(Request $request): array { return array_merge(parent::share($request), [ 'auth' => [ 'user' => $request->user() ? $request->user()->only('id', 'name', 'email') : null, ], 'flash' => [ 'success' => $request->session()->get('success'), 'error' => $request->session()->get('error'), ], ]); } } ``` Vue コンポーネントから共有データにアクセスするには `usePage()` を使います。 ```vue theme={null} ``` 共有データはすべてのリクエストに含まれるため、必要最低限のデータに絞ることを推奨します。`fn()` を使ったレイジー評価にすると、実際にアクセスされたときだけ評価されます。 *** ## Vue 3 のリアクティビティ基礎 Inertia × Vue で開発するうえで知っておくべき Vue 3 のリアクティビティ API を紹介します。 ### `ref` — プリミティブなリアクティブ値 ```vue theme={null} ``` ### `computed` — 算出プロパティ ```vue theme={null} ``` ### `onMounted` — マウント後の処理 ```vue theme={null} ``` *** ## まとめ Vue.js は Laravel との親和性が高く、特に Inertia を経由した「モダンモノリス」構成で力を発揮します。 | 要素 | 役割 | | - | - | | Laravel コントローラー | ルーティング・データ取得・バリデーション | | `Inertia::render()` | コントローラーから Vue コンポーネントへデータを渡す | | Vue ページコンポーネント | props を受け取りUIをレンダリング | | `useForm` | フォームの状態管理・送信・エラー表示 | | `Link` コンポーネント | フルリロードなしのページ遷移 | | `usePage().props` | 共有データへのアクセス | Inertia × Vue を使うと、LaravelのバックエンドのシンプルさとVueのリアクティブなUIの両方を享受できます。スターターキットでプロジェクトを作成すれば、認証画面も含めてすぐに開発を始められます。 Inertia v3 の全機能については公式ドキュメントを参照してください。 # Laravel Wayfinderの紹介 Source: https://kawax.biz/jp/blog/wayfinder-introduction ZiggyからInertiaスターターキットで採用された置き換えパッケージ、Laravel Wayfinderを解説します。LaravelバックエンドとTypeScriptフロントエンドを型安全につなぐ仕組みから、Ziggyとの違い、インストール・基本的な使い方、nextブランチで進化する次世代機能まで詳しく紹介します。 ## Wayfinderとは **Laravel Wayfinder** は、LaravelバックエンドとTypeScriptフロントエンドをゼロフリクションで橋渡しするパッケージです。コントローラーとルートから完全に型付けされたTypeScript関数を自動生成するため、フロントエンドのコードからLaravelのエンドポイントを関数として直接呼び出せます。 URLのハードコーディング、ルートパラメーターの手動管理、バックエンド変更の手作業同期——これらがすべて不要になります。 WayfinderはBeta版です(現在 v0.1.x)。v1.0.0 のリリースまでに API が変更される可能性があります。重要な変更はすべて [CHANGELOG](https://github.com/laravel/wayfinder/blob/main/CHANGELOG.md) に記録されます。 *** ## ZiggyとWayfinderの違い ### Ziggyとは [Ziggy](https://github.com/tighten/ziggy) は長年にわたってLaravelエコシステムで広く使われてきたルートヘルパーです。Laravelのルート定義をJavaScript側に公開し、`route('posts.show', { id: 1 })` のような形でURLを生成できました。 ### なぜWayfinderに置き換えられたか Ziggyはルート名とパラメーターを文字列で扱うため、TypeScriptとの相性に限界がありました。ルート名のタイポや間違ったパラメーター名はランタイムエラーにしかなりません。 WayfinderはTypeScriptファーストの設計で、コントローラーのメソッドを **インポート可能な関数** として生成します。 | 比較項目 | Ziggy | Wayfinder | | - | - | - | | ルート参照方法 | `route('posts.show', { id: 1 })` | `import { show } from "@/actions/..."` | | 型安全性 | 型定義が限定的 | 完全な TypeScript 型 | | IDEサポート | 補完は弱い | 補完・型チェック完全対応 | | ツリーシェイキング | 全ルートをバンドルに含む | 使ったルートのみバンドル | | 生成タイミング | ランタイムに注入 | ビルド時に静的生成 | Inertia ベースのLaravelスターターキット(React・Vue・Svelte)では、Wayfinder が標準で採用されています。 *** ## インストール ### 1. Composerでサーバーサイドパッケージをインストール ```bash theme={null} composer require laravel/wayfinder ``` ### 2. NPMでViteプラグインをインストール ```bash theme={null} npm i -D @laravel/vite-plugin-wayfinder ``` ### 3. `vite.config.js` にプラグインを追加 ```ts theme={null} import { wayfinder } from "@laravel/vite-plugin-wayfinder"; import { defineConfig } from "vite"; import laravel from "laravel-vite-plugin"; export default defineConfig({ plugins: [ laravel({ input: ["resources/js/app.ts"], refresh: true, }), wayfinder(), ], }); ``` Viteプラグインを追加すると、開発サーバー起動中にPHPファイルやルートファイルが変更されるたびに自動的にTypeScriptファイルが再生成されます。 *** ## TypeScript定義ファイルの生成 `wayfinder:generate` コマンドで TypeScript ファイルを生成します。 ```bash theme={null} php artisan wayfinder:generate ``` デフォルトでは `resources/js` 以下に3つのディレクトリが生成されます。 ``` resources/js/ ├── actions/ # コントローラーアクションの関数 │ └── App/Http/Controllers/ │ └── PostController.ts ├── routes/ # 名前付きルートの関数 │ └── post.ts └── wayfinder/ # 型定義ファイル └── types.ts ``` 生成されるファイルはビルドのたびに完全に再生成されるため、`.gitignore` に追加することを推奨します。`wayfinder`, `actions`, `routes` の3ディレクトリをまとめて除外してください。 出力先を変更したい場合は `--path` オプションを使います。 ```bash theme={null} php artisan wayfinder:generate --path=resources/js/api ``` コントローラーアクションのみ、またはルートのみ生成することも可能です。 ```bash theme={null} php artisan wayfinder:generate --skip-actions # ルートのみ生成 php artisan wayfinder:generate --skip-routes # アクションのみ生成 ``` *** ## 基本的な使い方 ### アクションのインポートと使用 `PostController` の `show` メソッドに対応するURLを生成する例です。 ```ts theme={null} import { show } from "@/actions/App/Http/Controllers/PostController"; show(1); // { url: "/posts/1", method: "get" } ``` URLだけが必要な場合は `.url()` を使います。 ```ts theme={null} show.url(1); // "/posts/1" ``` 特定の HTTP メソッドを指定することもできます。 ```ts theme={null} show.head(1); // { url: "/posts/1", method: "head" } ``` ### パラメーターの渡し方 Wayfinder の関数はさまざまな形式のパラメーターを受け付けます。 ```ts theme={null} import { update } from "@/actions/App/Http/Controllers/PostController"; // 単一パラメーター show(1); show({ id: 1 }); // 複数パラメーター update([1, 2]); update({ post: 1, author: 2 }); update({ post: { id: 1 }, author: { id: 2 } }); ``` ルートにキーバインディングが指定されている場合 (`/posts/{post:slug}`) はその値を使えます。 ```ts theme={null} // ルートが /posts/{post:slug} の場合 show("my-new-post"); show({ slug: "my-new-post" }); ``` ### コントローラー全体のインポート コントローラー全体をインポートしてメソッドを呼び出すこともできます。 ```ts theme={null} import PostController from "@/actions/App/Http/Controllers/PostController"; PostController.show(1); PostController.index(); ``` コントローラー全体をインポートするとツリーシェイキングが効かず、すべてのアクションがバンドルに含まれます。個別にインポートするほうが最終バンドルサイズを小さく保てます。 ### 単一アクションコントローラー 単一アクションコントローラー(Invokable Controller)は、インポートした関数をそのまま呼び出します。 ```ts theme={null} import StorePostController from "@/actions/App/Http/Controllers/StorePostController"; StorePostController(); // { url: "/posts", method: "post" } ``` ### 名前付きルートのインポート ルート名でアクセスするには `routes/` 配下のファイルを使います。 ```ts theme={null} import { show } from "@/routes/post"; // ルート名が `post.show` の場合 show(1); // { url: "/posts/1", method: "get" } ``` ### クエリパラメーター すべてのWayfinder関数は `query` オプションでクエリパラメーターを追加できます。 ```ts theme={null} import { show } from "@/actions/App/Http/Controllers/PostController"; show(1, { query: { page: 1, sort_by: "name" } }); // { url: "/posts/1?page=1&sort_by=name", method: "get" } ``` 現在のURLのクエリパラメーターとマージするには `mergeQuery` を使います。 ```ts theme={null} // 現在の URL: /posts/1?page=1&sort_by=category&q=shirt show.url(1, { mergeQuery: { page: 2, sort_by: "name" } }); // "/posts/1?page=2&sort_by=name&q=shirt" // パラメーターを削除するには null を指定 show.url(1, { mergeQuery: { sort_by: null } }); // "/posts/1?page=1&q=shirt" ``` ### フォームバリアント 従来のHTMLフォームで使う場合は `--with-form` オプションを付けて生成し、`.form` バリアントを使います。 ```bash theme={null} php artisan wayfinder:generate --with-form ``` ```tsx theme={null} import { store, update } from "@/actions/App/Http/Controllers/PostController"; // React の例 const Page = () => ( {/* */}
    ); const EditPage = () => (
    {/* */}
    ); ``` *** ## InertiaとWayfinderの組み合わせ InertiaのフォームヘルパーとWayfinderを組み合わせると、URLの文字列を一切書かずにフォーム送信ができます。 ```ts theme={null} import { useForm } from "@inertiajs/react"; import { store } from "@/actions/App/Http/Controllers/PostController"; const form = useForm({ name: "My Post" }); form.submit(store()); // POST /posts に送信 ``` `Link` コンポーネントでも同様に使えます。 ```tsx theme={null} import { Link } from "@inertiajs/react"; import { show } from "@/actions/App/Http/Controllers/PostController"; const Nav = () => ( 投稿を見る ); ``` *** ## スターターキットでの採用 `laravel new` で新規プロジェクトを作成し、React・Vue・Svelteを選ぶと、Wayfinderが自動セットアップされた構成が提供されます。スターターキットには以下が含まれます。 * Composerパッケージ `laravel/wayfinder` * NPMパッケージ `@laravel/vite-plugin-wayfinder` * `vite.config.js` にプラグイン設定済み * `.gitignore` に生成ディレクトリ追加済み 既存プロジェクトへの手動導入も上記の手順で対応できます。 *** ## 予約語と競合するメソッド名の処理 `delete` や `import` など、JavaScriptの予約語と同名のコントローラーメソッドには `Method` サフィックスが付きます。 ```ts theme={null} // コントローラーに delete メソッドがある場合 import { deleteMethod } from "@/actions/App/Http/Controllers/PostController"; deleteMethod(1); // { url: "/posts/1", method: "delete" } ``` *** ## 現在の状況(v0.1.x) 現在の安定版は `v0.1.x` ブランチで提供されています。2026年3月時点の最新バージョンは **v0.1.15** です。 ### v0.1.x 系の主な変更履歴 | バージョン | 主な内容 | | - | - | | v0.1.15 | Laravel 13対応、Bladeビュークラッシュ修正 | | v0.1.13 | クエリパラメーターのTypeScript strict互換性改善 | | v0.1.7 | URLデフォルトパラメーターのフロントエンド指定対応 | | v0.1.6 | Viteプラグインの追加 | | v0.1.5 | PHP 8.2サポート、キャッシュ済みルートへの対応 | | v0.1.0 | 初期リリース | *** ## nextブランチで開発中の次世代機能 `next` ブランチでは、現在の v0.1.x から大幅に機能が拡張された次期バージョンが開発されています。 `next` ブランチは `dev-next` 制約でインストールできますが、APIが大きく変わる可能性があります。本番環境での使用は推奨されません。 ```bash theme={null} composer require laravel/wayfinder:dev-next ``` ### 生成される TypeScript の範囲が大幅に拡大 v0.1.x がルートとコントローラーアクションのみを対象としているのに対し、次期バージョンは以下をすべて TypeScript として生成します。 ```mermaid theme={null} graph TD A["Laravel アプリケーション"] --> B["wayfinder:generate"] B --> C["Routes & Actions
    ルートURL関数"] B --> D["Form Requests
    バリデーション型"] B --> E["Eloquent Models
    モデルインターフェース"] B --> F["PHP Enums
    TypeScript定数"] B --> G["Inertia Page Props
    ページプロップ型"] B --> H["Broadcast Channels
    チャンネル型"] B --> I["Broadcast Events
    イベントペイロード型"] B --> J["Environment Variables
    import.meta.env型"] ``` ### Form Request の TypeScript 型生成 ```php theme={null} class StorePostRequest extends FormRequest { public function rules(): array { return [ 'title' => ['required', 'string', 'max:255'], 'content' => ['required', 'string'], 'tags' => ['nullable', 'array'], 'tags.*' => ['string'], ]; } } ``` 上記のForm Requestから以下の型が生成されます。 ```ts theme={null} export type Request = { title: string; content: string; tags?: string[] | null; }; ``` ### Eloquent モデルの型生成 ```php theme={null} class User extends Model { protected $casts = [ 'email_verified_at' => 'datetime', 'is_admin' => 'boolean', ]; public function posts(): HasMany { return $this->hasMany(Post::class); } } ``` 上記のモデルから `types.d.ts` に型が生成されます。 ```ts theme={null} export namespace App.Models { export type User = { id: number; name: string; email: string; email_verified_at: string | null; is_admin: boolean; posts: App.Models.Post[]; }; } ``` ### PHP Enum の TypeScript 変換 ```php theme={null} enum PostStatus: string { case Draft = 'draft'; case Published = 'published'; case Archived = 'archived'; } ``` 型と定数の両方が生成されます。 ```ts theme={null} // 型定義(types.d.ts) export namespace App.Enums { export type PostStatus = "draft" | "published" | "archived"; } // 定数(App/Enums/PostStatus.ts) export const Draft = "draft"; export const Published = "published"; export const Archived = "archived"; export const PostStatus = { Draft, Published, Archived } as const; ``` ### 出力ディレクトリの変更 v0.1.x では `actions/`、`routes/`、`wayfinder/` の3ディレクトリに分かれていましたが、次期バージョンでは `resources/js/wayfinder` 以下にまとめられます。 ``` resources/js/wayfinder/ ├── App/Http/Controllers/ │ └── PostController.ts # アクション関数(actions/ が廃止) ├── routes/ │ └── post.ts # 名前付きルート(同じ) ├── broadcast-channels.ts # ブロードキャストチャンネル ├── broadcast-events.ts # ブロードキャストイベント └── types.d.ts # 全型定義(types.ts から変更) ``` ### v0.1.x から next への主な変更点 * インポートパスが `@/actions/...` から `@/wayfinder/...` に変更 * `--skip-actions`、`--skip-routes`、`--with-form` フラグが廃止され、設定ファイルへ移行 * `types.ts` が `types.d.ts` に変更 *** ## まとめ Laravel Wayfinder は、Ziggyが提供していた「LaravelのルートをJavaScriptから参照する」という機能を、TypeScriptファーストで再設計したパッケージです。生成された関数をインポートして使うアプローチにより、型安全性・IDEサポート・ツリーシェイキングのすべてで大きく改善しています。 現在の v0.1.x でもルートとコントローラーアクションの型安全な参照が実現できており、InertiaベースのLaravelスターターキットで標準採用されています。`next` ブランチで開発中の次期バージョンでは、Form Request・Eloquent モデル・Enum・Inertia ページプロップまで TypeScript として生成する、より包括的な型安全基盤へと進化する予定です。 ソースコード、CHANGELOG、Issue はこちら。 Viteプラグインの設定オプション詳細はこちら。 # WSLでPHPとComposerとNode.jsをインストールする(Windows) Source: https://kawax.biz/jp/blog/windows-setup Windows 10/11でWSL 2を使いPHP・Composer・Node.jsをインストールし、Laravel開発環境を整えるステップバイステップガイドです。 ## はじめに Windows で Laravel を開発するには **WSL(Windows Subsystem for Linux)** を使うのがベストプラクティスです。Linux のツールチェーンをそのまま使えるため、macOS や本番サーバーと同じコマンドが通ります。 このガイドでは WSL 2 上の Ubuntu で PHP・Composer・Node.js(nvm 経由)をインストールし、`laravel new` が動く状態にするまでを解説します。 対象環境: Windows 10(バージョン 2004 以降)または Windows 11。WSL 2 を使います。 *** ## WSL のインストール スタートメニューで「PowerShell」を検索し、「管理者として実行」を選択します。 ```powershell theme={null} wsl --install ``` このコマンドで WSL 2 と Ubuntu(デフォルトディストリビューション)がインストールされます。完了後、PC を再起動します。 再起動後、Ubuntu が自動的に起動します。ユーザー名とパスワードを設定してください。このパスワードは `sudo` コマンドで使います。 パスワードは入力中に画面に表示されません。正確に入力してください。 Ubuntu のターミナルで以下を実行します。 ```bash theme={null} sudo apt update && sudo apt upgrade -y ``` *** ## PHP のインストール Ubuntu の標準リポジトリの PHP は古いバージョンが含まれていることがあります。`ondrej/php` PPA を使って最新バージョンをインストールします。 ```bash theme={null} sudo apt install -y software-properties-common ``` ```bash theme={null} sudo add-apt-repository ppa:ondrej/php sudo apt update ``` Laravel 13 は PHP 8.3 以上が必要です。 ```bash theme={null} sudo apt install -y php8.3 php8.3-cli php8.3-mbstring php8.3-xml php8.3-curl php8.3-zip php8.3-sqlite3 php8.3-mysql ``` ```bash theme={null} php --version ``` `PHP 8.3.x` 以上が表示されれば成功です。 複数の PHP バージョンが必要な場合は `update-alternatives` で切り替えられます。 ```bash theme={null} sudo update-alternatives --config php ``` インストール済みのバージョン一覧が表示されるので、使用するバージョンの番号を入力します。 *** ## Composer のインストール Composer は公式インストーラーを使ってインストールします。 ```bash theme={null} sudo apt install -y curl php8.3-cli unzip ``` ```bash theme={null} curl -sS https://getcomposer.org/installer -o /tmp/composer-setup.php sudo php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer ``` ```bash theme={null} composer --version ``` `composer global require` でインストールしたパッケージ(Laravel インストーラーなど)を使えるように、`~/.composer/vendor/bin` を `PATH` に追加します。 `~/.bashrc`(または `~/.zshrc`)に以下を追記します。 ```bash theme={null} export PATH="$HOME/.composer/vendor/bin:$PATH" ``` 設定を反映します。 ```bash theme={null} source ~/.bashrc ``` *** ## nvm のインストール Node.js は nvm(Node Version Manager)を使ってユーザー単位でインストールします。`sudo` 不要で管理できるため、グローバルな npm パッケージを使う AI ツールや CLI との相性が良いです。 最新のインストールスクリプトは [nvm の GitHub リポジトリ](https://github.com/nvm-sh/nvm) で確認できます。 ```bash theme={null} curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash ``` インストールスクリプトは自動的に `~/.bashrc` に以下を追記します。追記されていない場合は手動で追加してください。 ```bash theme={null} export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" ``` 設定を反映します。 ```bash theme={null} source ~/.bashrc ``` zsh を使っている場合は `~/.zshrc` に同様に追記し、`source ~/.zshrc` で反映します。 ```bash theme={null} nvm --version ``` *** ## Node.js のインストール ```bash theme={null} nvm install --lts ``` 新しいターミナルセッションでも自動的に使われるよう、デフォルトバージョンを設定します。 ```bash theme={null} nvm alias default node ``` ```bash theme={null} node --version npm --version ``` *** ## Laravel のインストール確認 ```bash theme={null} composer global require laravel/installer ``` ```bash theme={null} laravel new my-app ``` 対話的なセットアップが始まります。スターターキットや認証の有無などを選択できます。 ```bash theme={null} cd my-app php artisan serve ``` ブラウザで `http://localhost:8000` にアクセスし、Laravel のウェルカムページが表示されれば完了です。 WSL から Windows のブラウザで `localhost` にアクセスできない場合は、`php artisan serve --host=0.0.0.0` で起動してみてください。 *** ## 補足: 開発効率を上げるツール ### Windows Terminal Windows Terminal を使うと、複数のタブで WSL・PowerShell・コマンドプロンプトを一元管理できます。Microsoft Store から無料でインストールできます。 * タブで複数のシェルを並行操作 * Ubuntu のシェルをデフォルトに設定可能 * フォントや配色を自由にカスタマイズ ### VS Code Remote - WSL 拡張 VS Code の [Remote - WSL](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl) 拡張を使うと、WSL 内のファイルを Windows の VS Code から直接編集できます。 ```bash theme={null} # WSL ターミナルからプロジェクトを VS Code で開く code . ``` 初回実行時に VS Code サーバーが WSL 内に自動インストールされます。以降はネイティブと同等の操作感で開発できます。