Appearance
コーディング規約
全プロジェクト共通のコーディング規約
技術スタック
| カテゴリ | 技術 |
|---|---|
| Backend | Laravel 12 |
| Frontend | Vue 3 (Composition API) + Inertia.js |
| CSS | Tailwind CSS |
| UIコンポーネント | Headless UI |
| アイコン | Heroicons |
| Database | MySQL 8 |
| 認証 | Laravel Jetstream |
推奨ライブラリ
| ライブラリ | 用途 | URL |
|---|---|---|
| Headless UI | アクセシブルなUIプリミティブ(モーダル、ドロップダウン等) | https://headlessui.com |
| Heroicons | SVGアイコン(Tailwind公式) | https://heroicons.com |
PHP / Laravel
命名規則
| 対象 | 規則 | 例 |
|---|---|---|
| クラス | PascalCase | UserController, OrderService |
| メソッド | camelCase | getById(), createNew() |
| 変数 | camelCase | $totalAmount, $userList |
| 定数 | UPPER_SNAKE_CASE | MAX_RETRY_COUNT, DEFAULT_LIMIT |
| テーブル名 | snake_case(複数形) | users, orders, order_items |
| カラム名 | snake_case | created_at, total_price |
ディレクトリ構成
app/
├── Http/
│ ├── Controllers/ # コントローラー(薄く保つ)
│ ├── Middleware/ # ミドルウェア
│ └── Requests/ # FormRequest(バリデーション)
├── Models/ # Eloquentモデル
├── Services/ # ビジネスロジック(Eloquent直接操作)
└── Enums/ # 列挙型アーキテクチャ方針
Service + Eloquent 直接操作 を採用(Repositoryパターンは使わない)
Controller → Service → Model(Eloquent)- LaravelのEloquentは十分に抽象化されている
- Repositoryは過剰な抽象化になりがち
- シンプルさと開発速度を優先
コーディングルール
- Controllerは薄く: ビジネスロジックは Service クラスに分離
- FormRequest必須: バリデーションは必ず FormRequest で行う
- Eloquent推奨: 生SQLは極力避ける
- 型宣言必須: 引数・戻り値には型を明記
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;
});
}
}トランザクション処理
以下の場合は必ずトランザクションを使用する:
- 複数テーブルへの書き込み: 親子関係のあるデータ作成
- 更新と作成の組み合わせ: 在庫減少 + 注文作成など
- 削除を伴う処理: 関連データの整合性を保つ必要がある場合
php
use Illuminate\Support\Facades\DB;
// 基本形
DB::transaction(function () {
// 処理
});
// 例外をキャッチして処理する場合
DB::beginTransaction();
try {
// 処理
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}Serviceクラスの原則
- 1クラス1責務: OrderService, UserService のように分割
- Modelへの直接操作を避ける: Controller から Model を直接操作しない
- 例外は上位に投げる: Service 内で握りつぶさない
- 戻り値を明確に: 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 を使用:
- リアルタイム検索: 入力中の検索候補取得
- 非同期バリデーション: メールアドレスの重複チェック等
- バックグラウンド処理: 通知の既読化、いいね等
- 無限スクロール: 追加データの取得
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_case | created_at, total_price, user_id |
| 主キー | id | id(BIGINT UNSIGNED AUTO_INCREMENT) |
| 外部キー | {テーブル単数形}_id | user_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(必要に応じて)設計原則
- 正規化を基本とする: 第3正規形を目指す。パフォーマンス上必要な場合のみ非正規化
- 外部キー制約を設定: データ整合性を担保
php
$table->foreignId('user_id')->constrained()->cascadeOnDelete();- インデックスを適切に設定: 検索・ソートに使うカラムにはインデックス
php
$table->index('email');
$table->index(['status', 'created_at']); // 複合インデックス- NULL許容は最小限に: デフォルト値を設定できる場合は設定
php
// Good
$table->string('status')->default('pending');
$table->integer('count')->default(0);
// 本当にNULLが必要な場合のみ
$table->date('deleted_at')->nullable();- ENUM は使わない: 将来の変更が困難。代わりに文字列 + バリデーション、またはマスタテーブル
php
// Bad
$table->enum('status', ['draft', 'published', 'archived']);
// Good
$table->string('status', 20); // バリデーションで制御
// または
$table->foreignId('status_id')->constrained('statuses');マイグレーション
- 1ファイル1テーブル: テーブル作成は個別のマイグレーションで
- ロールバック可能に:
down()メソッドを必ず実装 - 本番環境では変更のみ: 既存カラムの削除・変更は新しいマイグレーションで
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 |
| JSON | json |
Vue.js / JavaScript
命名規則
| 対象 | 規則 | 例 |
|---|---|---|
| コンポーネント | PascalCase | UserCard.vue, LoginForm.vue |
| 変数・関数 | camelCase | isLoading, handleSubmit |
| 定数 | UPPER_SNAKE_CASE | API_BASE_URL |
| Props | camelCase | userId, isEditable |
| Emit | kebab-case | @update-value, @form-submit |
ディレクトリ構成
resources/js/
├── Components/
│ ├── Common/ # 共通コンポーネント
│ ├── Forms/ # フォーム部品
│ └── Tables/ # テーブル部品
├── Pages/ # 画面コンポーネント
├── Composables/ # 再利用可能なロジック
└── Stores/ # 状態管理(必要に応じて)コーディングルール
- Composition API使用: Options APIは使わない
- script setup推奨:
<script setup>を使用 - 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
クラスの順序
- レイアウト(display, position, flex, grid)
- サイズ(width, height, padding, margin)
- 装飾(background, border, shadow)
- タイポグラフィ(font, text)
- その他(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で権限チェック