この記事の対象
この記事はLaravel 12/13のEloquent、API Resource、HTTP Feature testを対象にします。Laravel 13は現行majorで、ここで扱うEloquentのrelationship serialization、JsonResource、assertJsonPath はLaravel 13.x公式Docsで確認しています。Laravel 12で利用する場合も、プロジェクトの実versionに切り替えた公式Docsと既存テストを確認してください。
掲載コードは、読者自身のLaravelアプリへ合わせるテンプレートです。このWebサイトのリポジトリでLaravelのrouteやFeature testを実行した結果ではありません。
命名規則で混乱しやすい理由
Laravel開発では、データベース、PHP、Eloquent、JSON、JavaScriptという複数の境界を行き来します。それぞれで自然な命名が異なるため、プロジェクト全体を一つの表記へ無理に統一すると、かえって責務が分かりにくくなります。
たとえばDB列は first_name、PHPのローカル変数は $firstName、relationship methodは billingAddress() と書けます。一方、外部へ返すAPIキーは利用者との契約なので、Eloquentの暗黙変換へ任せず明示する方が安全です。
「どちらのcaseが正しいか」ではなく、「どの境界で誰が名前を決めるか」を固定します。
境界ごとの対応表
表は横にスクロールできます
| 境界 | 推奨例 | 自動変換の有無 | 契約として固定する場所 |
|---|---|---|---|
| DB列 | first_name | なし | migration |
| PHP変数 | $firstName | 開発者が選択 | coding standard |
| relation method | billingAddress() | JSON化時はsnake_case | Model |
| APIキー | firstName または first_name | Resourceで明示 | API Resource |
この表の「推奨例」は、境界ごとの役割を示すものです。既存プロジェクトにAPI仕様やcoding standardがある場合は、それを正本にします。DB列と外部APIキーが同じ表記になる必要はありません。
DB列はmigrationで固定する
Laravelのmigrationでは、DB上の物理名を明示します。
Schema::table('users', function (Blueprint $table) {
$table->string('first_name');
});
Eloquentモデルでこの属性へアクセスするときも、基本は物理名のままです。
$firstName = $user->first_name;
左辺の $firstName はPHPコード内の変数、右辺の first_name はDB属性です。見た目を揃えるためだけにAccessorや別名変換を増やすと、migration、validation、Model、Resourceの対応を追いにくくなります。
relationshipを直接JSON化した場合
Laravel 13.x公式のEloquent Serializationでは、読み込み済みrelationshipはモデルをJSONへ変換したときに属性として含まれ、camelCaseで定義したrelationship methodのJSON属性名はsnake_caseになると説明されています。
public function billingAddress(): HasOne
{
return $this->hasOne(Address::class);
}
billingAddress を読み込んだEloquentモデル自体をJSON化すると、relationshipのキーは billing_address です。この自動変換は「全APIキーをcamelCaseからsnake_caseへ変換する」という一般規則ではありません。公式Docsが述べているのは、Eloquentモデルをarray/JSONへserializeするときの、読み込み済みrelationship属性の挙動です。
Eloquentモデルの直接返却とResource経由の返却を同じAPI内で混ぜると、billing_address と billingAddress が混在しやすくなります。外部契約は一つの出力境界へ寄せます。
JsonResourceでAPIキーを明示する
LaravelのAPI Resourcesでは、各Resourceの toArray がJSONレスポンスへ変換する属性配列を返します。ここで外部APIのキーをリテラルとして書けば、DB列やrelationship methodの表記と分離できます。
次はAPIをcamelCaseで返す例です。
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
/**
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'firstName' => $this->first_name,
'billingAddress' => $this->whenLoaded(
'billingAddress',
fn () => [
'city' => $this->billingAddress->city,
],
),
];
}
}
firstName と billingAddress はAPIの外部契約です。snake_caseを採用するAPIなら、同じResourceで first_name と billing_address を明示します。重要なのは、case変換を複数のControllerやフロントエンドへ分散させないことです。
whenLoaded('billingAddress') は、Controllerなどで事前に読み込んだ場合だけrelationshipを含めるための形です。この例では city も外部契約として明示しています。
Feature testでAPIキーを確認する
既存の認証・認可で保護された GET /api/users/{user} が、billingAddress を読み込んで上のResourceを返す前提です。認証とユーザー単位の閲覧権限の検証は維持し、ここではキー名を確認するテストを抜粋します。HTTP TestsのassertJsonPathで、利用側が読むパスと値を固定します。
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
class ShowUserTest extends TestCase
{
use RefreshDatabase;
public function test_user_api_keeps_camel_case_contract(): void
{
$user = User::factory()->create([
'first_name' => 'Test',
]);
$user->billingAddress()->create([
'city' => 'Test City',
]);
Sanctum::actingAs($user);
$response = $this->getJson("/api/users/{$user->id}");
$response
->assertOk()
->assertJsonPath('data.id', $user->id)
->assertJsonPath('data.firstName', 'Test')
->assertJsonPath('data.billingAddress.city', 'Test City')
->assertJsonMissingPath('data.billing_address');
}
}
この例は、billingAddress() が作成可能な city 属性を持つこと、UserPolicy が検出されること、Laravel標準のResource wrappingによりレスポンスが data 配下になることを前提にしています。JsonResource::withoutWrapping() を使うプロジェクトや、別のaddress schema・権限規則を採用しているAPIではfixtureとpathを実仕様へ合わせます。
実際のLaravelアプリのルート、マイグレーション、factoryに合わせ、php artisan testで確認してください。
リクエスト側とレスポンス側を分ける
API入力の firstName をDBの first_name に保存する処理と、DBの first_name を出力の firstName にする処理は別の境界です。入力はForm RequestやDTO、出力はResourceで変換場所を固定すると追跡しやすくなります。
次の状態は避けます。
- validationは
firstName、Controllerはfirst_nameを無条件に参照する - 一部のendpointだけEloquentモデルを直接返す
- Resourceとフロントエンドの両方で同じcase変換を行う
billingAddressとbilling_addressが同じレスポンス階層に混在する- DB列名を変更した際にResourceの対応を更新せず、外部APIのキーや値まで意図せず変える
Postmanで同じpathを手動・自動確認する方法はPostmanでAPIを確認する手順で扱っています。ResourceのFeature testとPostmanのpost-response testを同じAPI仕様から作ると、バックエンド内の契約と疎通時の契約を揃えられます。
チームで維持するルール
命名規則、API仕様、Resource、テストは同じ変更で更新します。DB列名だけの変更なら、Resourceの対応を直して外部APIのキーと値を維持します。APIのキーも変える場合は、利用側への影響を含めて別途合意します。
テストを変更ごとに実行する方法は、GitHub ActionsでLaravelテストを自動化する基本を参照してください。