Skip to content

コーディング規約

全プロジェクト共通のコーディング規約

技術スタック

カテゴリ技術
BackendLaravel 12
FrontendVue 3 (Composition API) + Inertia.js
CSSTailwind CSS
UIコンポーネントHeadless UI
アイコンHeroicons
DatabaseMySQL 8
認証Laravel Jetstream

推奨ライブラリ

ライブラリ用途URL
Headless UIアクセシブルなUIプリミティブ(モーダル、ドロップダウン等)https://headlessui.com
HeroiconsSVGアイコン(Tailwind公式)https://heroicons.com

PHP / Laravel

命名規則

対象規則
クラスPascalCaseUserController, OrderService
メソッドcamelCasegetById(), createNew()
変数camelCase$totalAmount, $userList
定数UPPER_SNAKE_CASEMAX_RETRY_COUNT, DEFAULT_LIMIT
テーブル名snake_case(複数形)users, orders, order_items
カラム名snake_casecreated_at, total_price

ディレクトリ構成

app/
├── Http/
│   ├── Controllers/        # コントローラー(薄く保つ)
│   ├── Middleware/         # ミドルウェア
│   └── Requests/           # FormRequest(バリデーション)
├── Models/                 # Eloquentモデル
├── Services/               # ビジネスロジック(Eloquent直接操作)
└── Enums/                  # 列挙型

アーキテクチャ方針

Service + Eloquent 直接操作 を採用(Repositoryパターンは使わない)

Controller → Service → Model(Eloquent)
  • LaravelのEloquentは十分に抽象化されている
  • Repositoryは過剰な抽象化になりがち
  • シンプルさと開発速度を優先

コーディングルール

  1. Controllerは薄く: ビジネスロジックは Service クラスに分離
  2. FormRequest必須: バリデーションは必ず FormRequest で行う
  3. Eloquent推奨: 生SQLは極力避ける
  4. 型宣言必須: 引数・戻り値には型を明記
php
// Good
public function store(StoreOrderRequest $request): Order
{
    return $this->orderService->create($request->validated());
}

// Bad
public function store($request)
{
    // ...
}

Serviceクラス

ビジネスロジックは Service クラスに集約する。

基本構成

php
namespace App\Services;

use App\Models\Order;
use Illuminate\Support\Facades\DB;

class OrderService
{
    public function create(array $data): Order
    {
        return DB::transaction(function () use ($data) {
            $order = Order::create($data);
            // 関連処理...
            return $order;
        });
    }
}

トランザクション処理

以下の場合は必ずトランザクションを使用する:

  1. 複数テーブルへの書き込み: 親子関係のあるデータ作成
  2. 更新と作成の組み合わせ: 在庫減少 + 注文作成など
  3. 削除を伴う処理: 関連データの整合性を保つ必要がある場合
php
use Illuminate\Support\Facades\DB;

// 基本形
DB::transaction(function () {
    // 処理
});

// 例外をキャッチして処理する場合
DB::beginTransaction();
try {
    // 処理
    DB::commit();
} catch (\Exception $e) {
    DB::rollBack();
    throw $e;
}

Serviceクラスの原則

  1. 1クラス1責務: OrderService, UserService のように分割
  2. Modelへの直接操作を避ける: Controller から Model を直接操作しない
  3. 例外は上位に投げる: Service 内で握りつぶさない
  4. 戻り値を明確に: Model, Collection, bool などを型宣言

ルーティング

ファイル構成

役割ごとにルートファイルを分割する。

routes/
├── web.php          # 一般ユーザー向け(Inertia)
├── admin.php        # 管理者向け(Inertia)
├── api.php          # API(Axios用、JSON返却)
└── auth.php         # 認証関連(Jetstream)

ルートファイルの登録

bootstrap/app.php でルートを登録:

php
->withRouting(
    web: __DIR__.'/../routes/web.php',
    api: __DIR__.'/../routes/api.php',
    commands: __DIR__.'/../routes/console.php',
    then: function () {
        Route::middleware('web')
            ->prefix('admin')
            ->name('admin.')
            ->group(base_path('routes/admin.php'));
    },
)

URL設計

種別プレフィックス
一般ユーザー//dashboard, /profile
管理者/admin/admin/users, /admin/settings
API/api/api/notifications, /api/search

ルート命名規則

php
// リソースルート(推奨)
Route::resource('users', UserController::class);

// 生成されるルート名
// users.index, users.create, users.store, users.show, users.edit, users.update, users.destroy

// カスタムルート
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
Route::post('/users/{user}/activate', [UserController::class, 'activate'])->name('users.activate');

Inertia と Axios の使い分け

基本方針

種別用途使用技術
ページ遷移画面全体の切り替えInertia(router.visit)
フォーム送信作成・更新・削除Inertia(useForm)
部分更新ページ遷移なしのデータ取得・更新Axios

Inertia の使用(メイン)

vue
<script setup>
import { router, useForm } from '@inertiajs/vue3'

// ページ遷移
const goToUser = (id) => {
  router.visit(`/users/${id}`)
}

// フォーム送信
const form = useForm({
  name: '',
  email: ''
})

const submit = () => {
  form.post('/users')
}

// 部分リロード(同一ページ内でデータ更新)
const refresh = () => {
  router.reload({ only: ['notifications'] })
}
</script>

Axios の使用(部分更新のみ)

以下の場合のみ Axios を使用:

  1. リアルタイム検索: 入力中の検索候補取得
  2. 非同期バリデーション: メールアドレスの重複チェック等
  3. バックグラウンド処理: 通知の既読化、いいね等
  4. 無限スクロール: 追加データの取得
vue
<script setup>
import axios from 'axios'
import { ref } from 'vue'

const suggestions = ref([])

// 検索候補の取得(部分更新)
const search = async (query) => {
  const { data } = await axios.get('/api/search', { params: { q: query } })
  suggestions.value = data
}

// 通知の既読化(バックグラウンド処理)
const markAsRead = async (id) => {
  await axios.post(`/api/notifications/${id}/read`)
}
</script>

API エンドポイント設計

Axios用のAPIは routes/api.php に定義:

php
// routes/api.php
Route::middleware('auth:sanctum')->group(function () {
    Route::get('/search', [SearchController::class, 'index']);
    Route::get('/notifications', [NotificationController::class, 'index']);
    Route::post('/notifications/{notification}/read', [NotificationController::class, 'markAsRead']);
});

APIレスポンス形式

php
// 成功
return response()->json([
    'data' => $users,
    'meta' => [
        'total' => $users->total(),
        'per_page' => $users->perPage(),
    ]
]);

// エラー
return response()->json([
    'message' => 'Validation failed',
    'errors' => $validator->errors()
], 422);

データベース設計

命名規則

対象規則
テーブル名snake_case(複数形)users, orders, order_items
カラム名snake_casecreated_at, total_price, user_id
主キーidid(BIGINT UNSIGNED AUTO_INCREMENT)
外部キー{テーブル単数形}_iduser_id, order_id
中間テーブルアルファベット順で結合order_product, role_user
真偽値is_/has_/can_ 接頭辞is_active, has_verified, can_edit
日時_at 接尾辞created_at, deleted_at, published_at
日付_on または _date 接尾辞birth_date, start_on

必須カラム

すべてのテーブルに以下を含める:

php
$table->id();                    // 主キー
$table->timestamps();            // created_at, updated_at
$table->softDeletes();           // deleted_at(必要に応じて)

設計原則

  1. 正規化を基本とする: 第3正規形を目指す。パフォーマンス上必要な場合のみ非正規化
  2. 外部キー制約を設定: データ整合性を担保
php
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
  1. インデックスを適切に設定: 検索・ソートに使うカラムにはインデックス
php
$table->index('email');
$table->index(['status', 'created_at']);  // 複合インデックス
  1. NULL許容は最小限に: デフォルト値を設定できる場合は設定
php
// Good
$table->string('status')->default('pending');
$table->integer('count')->default(0);

// 本当にNULLが必要な場合のみ
$table->date('deleted_at')->nullable();
  1. ENUM は使わない: 将来の変更が困難。代わりに文字列 + バリデーション、またはマスタテーブル
php
// Bad
$table->enum('status', ['draft', 'published', 'archived']);

// Good
$table->string('status', 20);  // バリデーションで制御
// または
$table->foreignId('status_id')->constrained('statuses');

マイグレーション

  1. 1ファイル1テーブル: テーブル作成は個別のマイグレーションで
  2. ロールバック可能に: down() メソッドを必ず実装
  3. 本番環境では変更のみ: 既存カラムの削除・変更は新しいマイグレーションで
php
// マイグレーション例
public function up(): void
{
    Schema::create('orders', function (Blueprint $table) {
        $table->id();
        $table->foreignId('user_id')->constrained()->cascadeOnDelete();
        $table->string('order_number', 20)->unique();
        $table->string('status', 20)->default('pending');
        $table->decimal('total_amount', 10, 2)->default(0);
        $table->text('notes')->nullable();
        $table->timestamps();
        $table->softDeletes();

        $table->index(['status', 'created_at']);
    });
}

public function down(): void
{
    Schema::dropIfExists('orders');
}

型の選択

用途推奨型
主キー・外部キーbigIncrements / foreignId
金額decimal(10, 2) ※ floatは使わない
短い文字列string(長さ)
長いテキストtext
真偽値boolean
日時timestamp / dateTime
日付のみdate
JSONjson

Vue.js / JavaScript

命名規則

対象規則
コンポーネントPascalCaseUserCard.vue, LoginForm.vue
変数・関数camelCaseisLoading, handleSubmit
定数UPPER_SNAKE_CASEAPI_BASE_URL
PropscamelCaseuserId, isEditable
Emitkebab-case@update-value, @form-submit

ディレクトリ構成

resources/js/
├── Components/
│   ├── Common/             # 共通コンポーネント
│   ├── Forms/              # フォーム部品
│   └── Tables/             # テーブル部品
├── Pages/                  # 画面コンポーネント
├── Composables/            # 再利用可能なロジック
└── Stores/                 # 状態管理(必要に応じて)

コーディングルール

  1. Composition API使用: Options APIは使わない
  2. script setup推奨: <script setup> を使用
  3. TypeScript風の型注釈: defineProps, defineEmitsで型を明記
vue
<script setup>
import { ref, computed } from 'vue'

const props = defineProps({
  userId: {
    type: Number,
    required: true
  },
  initialData: {
    type: Object,
    default: () => ({})
  }
})

const emit = defineEmits(['submit', 'cancel'])

const formData = ref({ ...props.initialData })
const isValid = computed(() => formData.value.name?.length > 0)

const handleSubmit = () => {
  if (isValid.value) {
    emit('submit', formData.value)
  }
}
</script>

Tailwind CSS

クラスの順序

  1. レイアウト(display, position, flex, grid)
  2. サイズ(width, height, padding, margin)
  3. 装飾(background, border, shadow)
  4. タイポグラフィ(font, text)
  5. その他(transition, cursor)
html
<!-- Good -->
<div class="flex items-center justify-between p-4 bg-white rounded-lg shadow text-gray-800">

<!-- Bad (順序がバラバラ) -->
<div class="text-gray-800 flex bg-white p-4 shadow items-center rounded-lg justify-between">

共通クラス

デザインシステム(DESIGN_SYSTEM.md)で定義された共通クラスを使用する。


Git

ブランチ命名

feature/機能名     # 新機能
fix/バグ内容       # バグ修正
refactor/対象      # リファクタリング

コミットメッセージ

[種別] 概要

種別:
- feat: 新機能
- fix: バグ修正
- refactor: リファクタリング
- docs: ドキュメント
- style: コードスタイル
- test: テスト

例:

feat: ユーザー登録機能を追加
fix: ログイン時のバリデーションを修正
refactor: UserServiceのメソッド分割

その他

エラーハンドリング

  • ユーザー向けメッセージは日本語
  • ログは英語
  • 例外は適切にキャッチし、ユーザーに分かりやすいメッセージを表示

セキュリティ

  • SQLインジェクション: Eloquent/クエリビルダを使用
  • XSS: Bladeの二重波括弧(エスケープ出力)を使用。{!! !!}(生出力)は極力避ける
  • CSRF: Laravelのデフォルト機能を使用
  • 認可: Policy/Gateで権限チェック

Gridworks Developer Documentation