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)
{
    // ...
}

日本語化(ローカライズ)

すべてのプロジェクトで日本語対応を行う。

基本設定

.env:

env
APP_LOCALE=ja
APP_FALLBACK_LOCALE=ja
APP_FAKER_LOCALE=ja_JP

ディレクトリ構成

lang/
└── ja/
    ├── validation.php    # バリデーションメッセージ
    ├── auth.php          # 認証メッセージ
    └── pagination.php    # ページネーション

バリデーションメッセージ

lang/ja/validation.php を作成し、日本語メッセージを設定する。

php
<?php

return [
    'required' => ':attributeは必須です。',
    'email' => ':attributeは有効なメールアドレスにしてください。',
    'unique' => 'この:attributeは既に使用されています。',
    'confirmed' => ':attributeが確認用と一致しません。',
    'min' => [
        'string' => ':attributeは:min文字以上にしてください。',
    ],
    'max' => [
        'string' => ':attributeは:max文字以下にしてください。',
    ],
    // ... 他のメッセージ

    'attributes' => [
        'name' => '名前',
        'email' => 'メールアドレス',
        'password' => 'パスワード',
        'password_confirmation' => 'パスワード(確認)',
    ],
];

認証画面の日本語化

Breezeの認証画面(Login.vue, Register.vue, VerifyEmail.vue 等)は日本語に書き換える。

vue
<!-- resources/js/Pages/Auth/Login.vue -->
<Head title="ログイン" />

<InputLabel for="email" value="メールアドレス" />
<InputLabel for="password" value="パスワード" />
<PrimaryButton>ログイン</PrimaryButton>
<Link :href="route('password.request')">パスワードをお忘れですか?</Link>

メール通知の日本語化

認証メール等のNotificationは日本語でカスタマイズする。

php
// app/Notifications/VerifyEmailNotification.php
<?php

namespace App\Notifications;

use Illuminate\Auth\Notifications\VerifyEmail;
use Illuminate\Notifications\Messages\MailMessage;

class VerifyEmailNotification extends VerifyEmail
{
    public function toMail($notifiable): MailMessage
    {
        $verificationUrl = $this->verificationUrl($notifiable);

        return (new MailMessage)
            ->subject('【サービス名】メールアドレスの確認')
            ->greeting($notifiable->name . ' 様')
            ->line('ご登録ありがとうございます。')
            ->line('以下のボタンをクリックして、メールアドレスの確認を完了してください。')
            ->action('メールアドレスを確認する', $verificationUrl)
            ->line('このリンクは60分間有効です。')
            ->salutation('運営事務局');
    }
}

Userモデルでカスタム通知を使用:

php
// app/Models/User.php
use App\Notifications\VerifyEmailNotification;

public function sendEmailVerificationNotification(): void
{
    $this->notify(new VerifyEmailNotification);
}

日本語化チェックリスト

新規プロジェクト作成時に確認:

  • [ ] .envAPP_LOCALE=ja 設定
  • [ ] lang/ja/validation.php 作成
  • [ ] attributes で項目名を日本語化
  • [ ] Login.vue, Register.vue を日本語化
  • [ ] VerifyEmail.vue を日本語化
  • [ ] ForgotPassword.vue, ResetPassword.vue を日本語化
  • [ ] メール通知(VerifyEmailNotification)を日本語化

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のメソッド分割

インフラ・外部サービス

必須サービス

カテゴリサービス備考
メール送信ResendLaravel標準のMailに統合可能
ドメイン管理CloudflareDNS・SSL・CDN一元管理

Resend(メール送信)

すべてのプロジェクトでメール送信には Resend を使用する。

理由

  • シンプルなAPI
  • 高い到達率
  • Laravel との統合が容易
  • 開発者フレンドリーなダッシュボード

Laravel での設定

bash
composer require resend/resend-laravel

.env:

env
MAIL_MAILER=resend
RESEND_API_KEY=re_xxxxxxxxxx
MAIL_FROM_ADDRESS=noreply@example.com
MAIL_FROM_NAME="${APP_NAME}"

config/services.php:

php
'resend' => [
    'api_key' => env('RESEND_API_KEY'),
],

使用例

php
use Illuminate\Support\Facades\Mail;
use App\Mail\WelcomeMail;

Mail::to($user->email)->send(new WelcomeMail($user));

Cloudflare(ドメイン管理)

すべてのプロジェクトでドメイン管理には Cloudflare を使用する。

理由

  • DNS・SSL・CDNを一元管理
  • 無料SSLで証明書管理が不要
  • DDoS対策が標準装備
  • 高速なDNS解決

設定方針

項目設定
SSL/TLS モードFlexible(サーバー側SSL不要)
Always Use HTTPSON
プロキシON(オレンジ雲)
キャッシュ静的ファイルのみ

Apache設定(Flexibleモード)

Cloudflare Flexible モードを使用する場合、サーバー側のSSL設定は不要。ポート80のみで設定する。

apache
<VirtualHost *:80>
    ServerName example.com
    DocumentRoot /var/www/html/project/public

    <Directory /var/www/html/project/public>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/project_error.log
    CustomLog ${APACHE_LOG_DIR}/project_access.log combined
</VirtualHost>

注意

  • HTTPSリダイレクトはApacheに設定しない(Cloudflare側で処理)
  • Let's Encrypt は使用しない(Cloudflareが証明書を管理)

テスト

テストの考え方

テストピラミッド

        /\
       /  \      E2E(少)
      /────\     重要なユーザーフローのみ
     /      \
    /────────\   Feature/Integration(中)
   /          \  APIエンドポイント、主要機能
  /────────────\ Unit(多)
 /              \ ビジネスロジック、ユーティリティ
種類対象速度
UnitService、ユーティリティ関数速い多い
FeatureController、API中程度中程度
E2Eユーザーフロー全体遅い少ない

テストを書く優先度

  1. 必ず書く: 決済、認証、重要なビジネスロジック
  2. 書くべき: CRUD操作、バリデーション、API
  3. 余裕があれば: UIコンポーネント、ヘルパー関数

使用ツール

種類ツール
PHP Unit/FeaturePest(推奨)または PHPUnit
E2EPlaywright
Vue コンポーネントVitest + Vue Test Utils(必要に応じて)

Laravel テスト(Pest)

セットアップ

bash
composer require pestphp/pest --dev
composer require pestphp/pest-plugin-laravel --dev
php artisan pest:install

ディレクトリ構成

tests/
├── Feature/           # 機能テスト(HTTP、DB使用)
│   ├── Auth/
│   ├── Api/
│   └── Admin/
├── Unit/              # ユニットテスト(単体、高速)
│   └── Services/
└── Pest.php           # Pest設定

Feature テスト例

php
// tests/Feature/Auth/RegistrationTest.php
<?php

use App\Models\User;

describe('ユーザー登録', function () {
    it('新規ユーザーを登録できる', function () {
        $response = $this->post('/register', [
            'name' => 'Test User',
            'email' => 'test@example.com',
            'password' => 'password123',
            'password_confirmation' => 'password123',
        ]);

        $response->assertRedirect('/dashboard');
        $this->assertDatabaseHas('users', [
            'email' => 'test@example.com',
        ]);
    });

    it('メールアドレスが重複していると登録できない', function () {
        User::factory()->create(['email' => 'test@example.com']);

        $response = $this->post('/register', [
            'name' => 'Test User',
            'email' => 'test@example.com',
            'password' => 'password123',
            'password_confirmation' => 'password123',
        ]);

        $response->assertSessionHasErrors('email');
    });
});

API テスト例

php
// tests/Feature/Api/OrderTest.php
<?php

use App\Models\User;
use App\Models\Order;

describe('注文API', function () {
    beforeEach(function () {
        $this->user = User::factory()->create();
        $this->actingAs($this->user);
    });

    it('注文一覧を取得できる', function () {
        Order::factory()->count(3)->create(['user_id' => $this->user->id]);

        $response = $this->getJson('/api/orders');

        $response->assertOk()
            ->assertJsonCount(3, 'data');
    });

    it('注文を作成できる', function () {
        $response = $this->postJson('/api/orders', [
            'product_id' => 1,
            'quantity' => 2,
        ]);

        $response->assertCreated();
        $this->assertDatabaseHas('orders', [
            'user_id' => $this->user->id,
            'quantity' => 2,
        ]);
    });

    it('他人の注文は取得できない', function () {
        $otherUser = User::factory()->create();
        $order = Order::factory()->create(['user_id' => $otherUser->id]);

        $response = $this->getJson("/api/orders/{$order->id}");

        $response->assertForbidden();
    });
});

Unit テスト例

php
// tests/Unit/Services/PriceCalculatorTest.php
<?php

use App\Services\PriceCalculator;

describe('PriceCalculator', function () {
    it('税込価格を計算できる', function () {
        $calculator = new PriceCalculator();

        expect($calculator->withTax(1000))->toBe(1100);
        expect($calculator->withTax(1500))->toBe(1650);
    });

    it('割引を適用できる', function () {
        $calculator = new PriceCalculator();

        expect($calculator->applyDiscount(1000, 10))->toBe(900);
        expect($calculator->applyDiscount(1000, 25))->toBe(750);
    });

    it('割引率が100%を超えるとエラー', function () {
        $calculator = new PriceCalculator();

        expect(fn() => $calculator->applyDiscount(1000, 150))
            ->toThrow(InvalidArgumentException::class);
    });
});

E2E テスト(Playwright)

セットアップ

bash
npm init playwright@latest

ディレクトリ構成

e2e/
├── tests/
│   ├── auth.spec.ts
│   ├── order.spec.ts
│   └── admin.spec.ts
├── fixtures/
│   └── test-data.ts
└── playwright.config.ts

E2E テスト例

typescript
// e2e/tests/auth.spec.ts
import { test, expect } from '@playwright/test';

test.describe('認証フロー', () => {
  test('ログインできる', async ({ page }) => {
    await page.goto('/login');

    await page.fill('input[name="email"]', 'user@example.com');
    await page.fill('input[name="password"]', 'password');
    await page.click('button[type="submit"]');

    await expect(page).toHaveURL('/dashboard');
    await expect(page.locator('h1')).toContainText('ダッシュボード');
  });

  test('間違ったパスワードではログインできない', async ({ page }) => {
    await page.goto('/login');

    await page.fill('input[name="email"]', 'user@example.com');
    await page.fill('input[name="password"]', 'wrongpassword');
    await page.click('button[type="submit"]');

    await expect(page.locator('.error-message')).toBeVisible();
  });
});
typescript
// e2e/tests/order.spec.ts
import { test, expect } from '@playwright/test';

test.describe('注文フロー', () => {
  test.beforeEach(async ({ page }) => {
    // ログイン
    await page.goto('/login');
    await page.fill('input[name="email"]', 'user@example.com');
    await page.fill('input[name="password"]', 'password');
    await page.click('button[type="submit"]');
  });

  test('商品を注文できる', async ({ page }) => {
    await page.goto('/products');

    // 商品を選択
    await page.click('text=商品を見る >> nth=0');
    await page.click('button:has-text("カートに追加")');

    // カートへ
    await page.click('a:has-text("カート")');
    await page.click('button:has-text("注文する")');

    // 確認
    await expect(page.locator('.order-complete')).toContainText('注文が完了しました');
  });
});

テストの原則

1. AAA パターン

php
it('注文を作成できる', function () {
    // Arrange(準備)
    $user = User::factory()->create();
    $product = Product::factory()->create(['price' => 1000]);

    // Act(実行)
    $order = $this->actingAs($user)
        ->postJson('/api/orders', ['product_id' => $product->id]);

    // Assert(検証)
    $order->assertCreated();
    expect(Order::count())->toBe(1);
});

2. テスト名は日本語で具体的に

php
// Good
it('在庫がない商品は注文できない', function () { });
it('管理者のみユーザーを削除できる', function () { });

// Bad
it('test order', function () { });
it('works correctly', function () { });

3. 1テスト1検証

php
// Good - 分離されている
it('注文が作成される', function () {
    // ...
    expect(Order::count())->toBe(1);
});

it('在庫が減少する', function () {
    // ...
    expect($product->fresh()->stock)->toBe(9);
});

// Bad - 複数の検証が混在
it('注文処理', function () {
    // ...注文、在庫、メール、ポイント全部チェック
});

4. テストデータは Factory で

php
// Good
$user = User::factory()->create();
$user = User::factory()->admin()->create();  // 状態を使用
$users = User::factory()->count(10)->create();

// Bad - ハードコード
$user = User::create([
    'name' => 'Test',
    'email' => 'test@example.com',
    // ...
]);

テスト実行

bash
# 全テスト実行
php artisan test

# 特定のファイル
php artisan test tests/Feature/Auth/LoginTest.php

# 特定のテスト
php artisan test --filter="ログインできる"

# カバレッジ付き
php artisan test --coverage

# E2E テスト
npx playwright test

# E2E テスト(UIモード)
npx playwright test --ui

テスト実行環境

環境テスト実行目的
開発環境(ローカル)◎ 必須コード変更のたびに実行
CI/CD(GitHub Actions)◎ 必須PR・pushのたびに自動実行
ステージング△ E2Eのみ本番に近い環境での最終確認
本番✕ 実行しないデータ破損リスク

テストの流れ

開発者のローカル
    │  ① php artisan test

GitHub に Push / PR
    │  ② CI/CDで自動テスト(GitHub のサーバー上)

テスト通過 → ステージング
    │  ③ E2E・手動確認

本番デプロイ

CI/CD でのテスト

CI/CD のテストはどこで動くか

GitHub Actions の場合、GitHub のクラウドサーバー上で一時的に環境が作られます。

┌─────────────────────────────────────────────┐
│  GitHub Actions Runner(一時的な仮想マシン)    │
│                                             │
│  ┌─────────────┐    ┌─────────────────┐    │
│  │    PHP      │    │  MySQL (Docker) │    │
│  │   8.3       │───▶│  テスト専用DB    │    │
│  └─────────────┘    │  (空のDB)      │    │
│         │           └─────────────────┘    │
│         ▼                                   │
│  php artisan test                           │
│                                             │
│  テスト完了後 → VM ごと破棄(何も残らない)     │
└─────────────────────────────────────────────┘

本番サーバー・開発サーバーには一切影響なし
  • 実行場所: GitHub のクラウドサーバー(VM)
  • データベース: Docker で一時的に作成
  • テスト後: VM ごと完全に削除
  • 本番への影響: なし
  • 費用: プライベートリポジトリは月2,000分無料(小規模なら十分)

GitHub Actions 設定例

yaml
# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_DATABASE: testing
          MYSQL_ROOT_PASSWORD: password
        ports:
          - 3306:3306

    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          coverage: xdebug

      - name: Install Dependencies
        run: composer install --no-progress --prefer-dist

      - name: Run Tests
        run: php artisan test --coverage
        env:
          DB_CONNECTION: mysql
          DB_HOST: 127.0.0.1
          DB_DATABASE: testing
          DB_USERNAME: root
          DB_PASSWORD: password

その他

ログ

ログチャンネル設定

ログは Daily(日次ローテーション)を使用する。

.env:

env
LOG_CHANNEL=daily
LOG_LEVEL=debug

config/logging.php のデフォルト設定で14日間保持される。

ログレベルの使い分け

レベル用途
emergencyシステム全体が使用不能データベース接続不可
alert即座に対応が必要ディスク容量枯渇
critical重大なエラー決済処理の失敗
errorエラー(処理は継続可能)API呼び出し失敗、例外発生
warning警告(問題になりうる)非推奨機能の使用、リトライ発生
info重要な処理の記録ユーザー登録、注文完了、バッチ実行
debug開発時のデバッグ情報変数の値、処理の流れ確認

実装例

php
use Illuminate\Support\Facades\Log;

// 重要な処理の記録
Log::info('User registered', ['user_id' => $user->id, 'email' => $user->email]);

// エラー発生時
try {
    $this->paymentService->charge($order);
    Log::info('Payment succeeded', ['order_id' => $order->id]);
} catch (\Exception $e) {
    Log::error('Payment failed', [
        'order_id' => $order->id,
        'error' => $e->getMessage(),
    ]);
    throw $e;
}

// 警告
Log::warning('API rate limit approaching', ['remaining' => $remaining]);

// デバッグ(本番では出力されない)
Log::debug('Processing order', ['data' => $orderData]);

ログ出力の原則

  1. 本番環境では LOG_LEVEL=info: debug は出力しない
  2. コンテキスト情報を含める: ['user_id' => $id] のように配列で渡す
  3. 機密情報は出力しない: パスワード、クレジットカード番号など
  4. 英語で記述: ログは英語で統一(検索・解析しやすい)
php
// Good
Log::info('Order created', ['order_id' => $order->id, 'amount' => $order->total]);

// Bad - 日本語
Log::info('注文が作成されました');

// Bad - コンテキストなし
Log::info('Order created');

// Bad - 機密情報
Log::info('User login', ['password' => $password]);

ログを出力すべきタイミング

タイミングレベル
ユーザー登録・ログインinfo
重要なデータ作成・更新・削除info
決済処理の成功・失敗info / error
外部API呼び出しの失敗error
バッチ処理の開始・終了info
例外のキャッチerror
リトライ処理warning
開発時の変数確認debug

エラーハンドリング

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

セキュリティ

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

ロゴ・Favicon

プロジェクト立ち上げ時に、Laravelのデフォルトロゴを差し替える。

ロゴ形式

SVG(ベクター形式)必須

  • 拡大縮小で劣化しない
  • CSSで色変更可能(fill-current
  • ファイルサイズが軽量

用意するファイル

ファイル形状サイズ基準用途
logo.svg横長(3:1〜4:1)高さ40px(h-10)で最適化ヘッダー、ログイン画面
logo-icon.svg正方形(1:1)32px以上で視認可能favicon、サイドメニュー

Favicon セット

512x512 の正方形画像から以下を生成する。

public/
├── favicon.ico           # 32x32(レガシーブラウザ)
├── favicon-16x16.png     # ブラウザタブ
├── favicon-32x32.png     # ブラウザタブ(高解像度)
├── favicon-96x96.png     # 高解像度favicon
├── apple-touch-icon.png  # 180x180(iOS ホーム画面)
├── web-app-manifest-192x192.png  # PWA アイコン
├── web-app-manifest-512x512.png  # PWA スプラッシュ
└── site.webmanifest      # PWA設定

Favicon ジェネレーター

Favicon ジェネレーター を使用すると、512x512 の画像から上記すべてを一括生成できます。

使用サイズ一覧

場所使用ロゴ高さTailwind
ヘッダーlogo.svg32-40pxh-8 ~ h-10
ログイン画面logo.svg48pxh-12
サイドメニュー上部logo.svg または logo-icon.svg32-40pxh-8 ~ h-10
フッターlogo.svg24-32pxh-6 ~ h-8
faviconlogo-icon.svg32px-

ApplicationLogo コンポーネントの差し替え

Breezeのデフォルトロゴを差し替える:

vue
<!-- resources/js/Components/ApplicationLogo.vue -->
<template>
    <img src="/images/logo.svg" alt="サービス名" class="h-10 w-auto">
</template>

または SVG を直接埋め込む:

vue
<template>
    <svg viewBox="0 0 120 40" class="h-10 w-auto fill-current">
        <!-- SVGパス -->
    </svg>
</template>

チェックリスト

新規プロジェクト作成時:

  • [ ] logo.svg(横長ロゴ)を作成
  • [ ] logo-icon.svg(正方形アイコン)を作成
  • [ ] Favicon ジェネレーター で favicon セットを生成
  • [ ] public/ に favicon ファイルを配置
  • [ ] site.webmanifest のサービス名を編集
  • [ ] ApplicationLogo.vue を差し替え
  • [ ] HTML の <head> に favicon リンクを追加

AI コーディングルール

AI(Claude等)にコーディングを依頼する際のルール。

テスト

  • 新しい機能には必ず Feature テストを作成する
  • Service クラスには Unit テストを作成する
  • テストがパスすることを確認してから完了とする
  • php artisan test で全テストが通ること
bash
# 完了前に必ず実行
php artisan test

コード品質

  • 既存のコードスタイルに従う
  • 型宣言(引数・戻り値)を必ず付ける
  • 1メソッド30行以内を目安にする
  • マジックナンバーは定数化する
php
// Good
public function calculateTotal(Order $order): int
{
    return $order->items->sum(fn($item) => $item->price * $item->quantity);
}

// Bad - 型なし、長すぎ
public function calculateTotal($order)
{
    // 50行以上の処理...
}

ファイル配置

種類配置場所
ビジネスロジックapp/Services/
バリデーションapp/Http/Requests/
共通コンポーネントresources/js/Components/Common/
ページコンポーネントresources/js/Pages/

完了前チェックリスト

コーディング完了前に必ず確認する:

  • [ ] php artisan test が通る
  • [ ] npm run build がエラーなく完了する
  • [ ] 既存機能が壊れていない(関連テストが通る)
  • [ ] 不要な console.log / dd() / dump() を削除した
  • [ ] 機密情報がハードコードされていない

禁止事項

以下は禁止とする:

禁止理由代替手段
Controller に直接ビジネスロジック肥大化・テスト困難Service クラスに分離
生の SQL クエリSQLインジェクションリスクEloquent / クエリビルダ
env() を config 以外で使用キャッシュ時に動作しないconfig() を使用
any 型(TypeScript)型安全性が失われる適切な型を定義
@ts-ignore / @ts-nocheck型エラーの握りつぶし型を正しく修正
!important(CSS)スタイル競合の原因詳細度を調整
php
// 禁止: Controller にロジック
public function store(Request $request)
{
    // 100行のビジネスロジック...
}

// 推奨: Service に分離
public function store(StoreOrderRequest $request)
{
    $order = $this->orderService->create($request->validated());
    return redirect()->route('orders.show', $order);
}

コミット前の確認

bash
# 1. テスト実行
php artisan test

# 2. フロントエンドビルド
npm run build

# 3. 静的解析(導入している場合)
./vendor/bin/pint --test

AIへの依頼時のヒント

効率的に依頼するためのヒント:

  1. 既存コードを参照させる: 「既存の UserController を参考にして」
  2. テストを明示的に依頼: 「テストも作成して、パスすることを確認して」
  3. 完了条件を明確に: 「php artisan test が通ったら完了」
  4. スコープを限定: 「このファイルだけ修正して」

Gridworks Developer Documentation