本文へ移動
株式会社織翔
← コラム一覧に戻る
Laravel9分で読める

Laravelで混乱しやすいスネークケースとキャメルケースの考え方

Laravel開発でよく出てくるスネークケースとキャメルケースの使い分けを、DB・モデル・APIの境界ごとに整理します。

#Laravel#命名規則#スネークケース#キャメルケース

執筆・監修:

この記事の対象

この記事はLaravel 12/13のEloquent、API Resource、HTTP Feature testを対象にします。Laravel 13は現行majorで、ここで扱うEloquentのrelationship serialization、JsonResourceassertJsonPath は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 methodbillingAddress()JSON化時はsnake_caseModel
APIキーfirstName または first_nameResourceで明示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_addressbillingAddress が混在しやすくなります。外部契約は一つの出力境界へ寄せます。

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,
                ],
            ),
        ];
    }
}

firstNamebillingAddress はAPIの外部契約です。snake_caseを採用するAPIなら、同じResourceで first_namebilling_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変換を行う
  • billingAddressbilling_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テストを自動化する基本を参照してください。