Laravel Inertia とは? SPA フレームワークの新標準
従来の SPA 開発における課題と解決策
従来のSPA(Single Page Application)開発では、フロントエンドとバックエンドを完全に分離する方式が主流でした。この方式には以下のような課題がありました:
- 開発の複雑性
- フロントエンドとバックエンドで別々のリポジトリ管理が必要
- APIの設計・実装・ドキュメント作成に多大な時間が必要
- 認証やルーティングの二重管理
- パフォーマンスへの影響
- 初期ロード時のJavaScriptバンドルサイズが大きくなりがち
- APIリクエストのオーバーヘッド
- キャッシュ戦略の複雑化
- SEO対策の困難さ
- クライアントサイドレンダリングによる検索エンジン対応の課題
- メタ情報の動的更新の複雑さ
Inertia.js が革新的なアプローチを提供する
Inertia.jsは、これらの課題に対して革新的なアプローチを提供します:
// 従来のAPIコントローラー class UserController extends Controller { public function index() { return response()->json([ 'users' => User::all() ]); } } // Inertiaを使用したコントローラー class UserController extends Controller { public function index() { return Inertia::render('Users/Index', [ 'users' => User::all() ]); } }
Inertia.jsの主な特徴:
- モノリシックなアプローチ
- サーバーサイドとクライアントサイドを統合的に扱える
- 従来のMVCパターンを踏襲しつつSPAを実現
- シンプルなデータ受け渡し
- PHPの配列をそのままPropsとして渡せる
- 型の自動変換とバリデーション
- 効率的なページ遷移
- XHRを使用した高速なページ遷移
- 必要なデータのみを更新する差分更新
Laravel×Inertia の組み合わせがもたらすメリット
LaravelとInertiaの組み合わせは、以下のような大きなメリットをもたらします:
- 開発効率の向上
- Laravel Breezeによる簡単な導入
- 既存のLaravelスキルセットを活かせる
- フロントエンドフレームワーク(Vue.js/React)との親和性
- セキュリティの向上
// CSRFトークンの自動処理 Inertia::share('csrf_token', csrf_token()); // 認証状態の共有 Inertia::share([ 'auth' => function () { return [ 'user' => Auth::user() ? [ 'id' => Auth::user()->id, 'name' => Auth::user()->name, ] : null ]; } ]);
- 保守性の向上
- 単一のコードベースでの管理
- コンポーネントの再利用性
- デプロイメントの簡素化
- パフォーマンスの最適化
- サーバーサイドレンダリングのサポート
- 効率的なアセット管理
- レスポンスサイズの最適化
Inertiaは、SPAの利点を活かしつつ、従来のサーバーサイドの開発手法の利点も取り入れた、バランスの取れたアプローチを提供します。特にLaravelとの組み合わせでは、開発者の生産性を大きく向上させることができます。
環境構築から始めるLaravel Inertia
必要な開発環境とバージョンの確認
Laravel Inertiaを使用するための環境要件を確認しましょう:
- 必須システム要件
- PHP 8.1以上
- Composer 2.0以上
- Node.js 16.0以上
- npm 8.0以上またはyarn 1.22以上
- 推奨ブラウザ要件
- Chrome 最新版
- Firefox 最新版
- Safari 最新版
- Edge 最新版
- 開発ツール
# PHP拡張モジュールの確認 php -m | grep -E "openssl|pdo|mbstring|tokenizer|xml|ctype|json" # Composerのバージョン確認 composer --version # Node.jsのバージョン確認 node --version # npmのバージョン確認 npm --version
Laravel BreezeによるInertia導入手順
- 新規Laravelプロジェクトの作成
# プロジェクトの作成 composer create-project laravel/laravel your-app # プロジェクトディレクトリへ移動 cd your-app # 依存パッケージのインストール composer install
- Laravel Breezeのインストールと設定
# Breezeのインストール composer require laravel/breeze --dev # Inertia版Breezeのインストール(Vueを使用する場合) php artisan breeze:install vue # または、Reactを使用する場合 php artisan breeze:install react # 必要なnpmパッケージのインストール npm install # アセットのビルド npm run dev
- データベースの設定
// .envファイルの設定例 DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=your_database DB_USERNAME=your_username DB_PASSWORD=your_password // マイグレーションの実行 php artisan migrate
Vue.js/React対応テンプレートのセットアップ
- Vueテンプレートの基本構造(app.jsの設定)
import { createApp, h } from 'vue' import { createInertiaApp } from '@inertiajs/vue3' createInertiaApp({ resolve: name => { const pages = import.meta.glob('./Pages/**/*.vue', { eager: true }) return pages[`./Pages/${name}.vue`] }, setup({ el, App, props, plugin }) { createApp({ render: () => h(App, props) }) .use(plugin) .mount(el) }, })
- Reactテンプレートの基本構造(app.jsの設定)
import { createInertiaApp } from '@inertiajs/react' import { createRoot } from 'react-dom/client' createInertiaApp({ resolve: name => { const pages = import.meta.glob('./Pages/**/*.jsx', { eager: true }) return pages[`./Pages/${name}.jsx`] }, setup({ el, App, props }) { createRoot(el).render(<App {...props} />) }, })
- 共通の設定とカスタマイズ
// app/Http/Middleware/HandleInertiaRequests.php public function share(Request $request): array { return array_merge(parent::share($request), [ // グローバルで共有したいデータを定義 'auth' => [ 'user' => $request->user() ? [ 'id' => $request->user()->id, 'name' => $request->user()->name, 'email' => $request->user()->email, ] : null, ], 'flash' => [ 'message' => fn () => $request->session()->get('message') ], ]); }
- 開発環境の最適化
# Viteの開発サーバー起動 npm run dev # 本番用ビルド npm run build
この環境構築が完了すると、以下の機能が利用可能になります:
- SPAライクなページ遷移
- Laravel認証システムとの統合
- Vite経由のホットリロード
- TypeScriptサポート(オプション)
- コンポーネントベースの開発
開発を始める前に、以下の動作確認を行うことをお勧めします:
- ログイン/登録機能の確認
- ページ遷移の動作確認
- データベース接続の確認
- アセットの読み込み確認
これらの設定が完了したら、実際の開発をスムーズに進めることができます。
Inertiaの基本的な使い方マスター
コントローラーとビューの連携方法
LaravelのコントローラーからInertiaビューへのデータ受け渡しは、直感的で簡単です。以下に主要な実装パターンを示します:
- 基本的なページレンダリング
// app/Http/Controllers/TaskController.php class TaskController extends Controller { public function index() { return Inertia::render('Tasks/Index', [ 'tasks' => Task::all()->map(fn($task) => [ 'id' => $task->id, 'title' => $task->title, 'completed' => $task->completed ]) ]); } public function show(Task $task) { return Inertia::render('Tasks/Show', [ 'task' => $task->load('user', 'comments') ]); } }
- ページネーションの実装
public function index() { return Inertia::render('Tasks/Index', [ 'tasks' => Task::query() ->when(Request::input('search'), function ($query, $search) { $query->where('title', 'like', "%{$search}%"); }) ->paginate(10) ->withQueryString() ->through(fn($task) => [ 'id' => $task->id, 'title' => $task->title, ]), 'filters' => Request::only(['search']), ]); }
プロップスを活用したデータ受け渡し
- Vueコンポーネントでのプロップス受け取り
<!-- resources/js/Pages/Tasks/Index.vue --> <script setup> import { ref } from 'vue' import Layout from '@/Layouts/AppLayout.vue' // プロップスの定義 defineProps({ tasks: { type: Object, required: true }, filters: { type: Object, required: true } }) // 検索フォームの状態管理 const search = ref(filters.search) </script> <template> <Layout> <div class="container mx-auto py-8"> <!-- 検索フォーム --> <div class="mb-4"> <input v-model="search" type="text" placeholder="タスクを検索..." class="border p-2 rounded" @input="$inertia.get('/tasks', { search }, { preserveState: true, replace: true })" > </div> <!-- タスクリスト --> <div v-for="task in tasks.data" :key="task.id"> <h3>{{ task.title }}</h3> </div> <!-- ページネーション --> <pagination :links="tasks.links" /> </div> </Layout> </template>
- Reactコンポーネントでのプロップス受け取り
// resources/js/Pages/Tasks/Index.jsx import { useState } from 'react' import Layout from '@/Layouts/AppLayout' import { router } from '@inertiajs/react' export default function Index({ tasks, filters }) { const [search, setSearch] = useState(filters.search) const handleSearch = (e) => { setSearch(e.target.value) router.get('/tasks', { search: e.target.value }, { preserveState: true, replace: true }) } return ( <Layout> <div className="container mx-auto py-8"> <input value={search} onChange={handleSearch} type="text" placeholder="タスクを検索..." className="border p-2 rounded" /> {tasks.data.map(task => ( <div key={task.id}> <h3>{task.title}</h3> </div> ))} <Pagination links={tasks.links} /> </div> </Layout> ) }
ページコンポーネントの作成と管理
- 共通レイアウトの作成
<!-- resources/js/Layouts/AppLayout.vue --> <script setup> import { Link } from '@inertiajs/vue3' import { usePage } from '@inertiajs/vue3' const page = usePage() </script> <template> <div> <nav class="bg-gray-800 text-white p-4"> <div class="container mx-auto flex justify-between"> <Link href="/" class="font-bold"> アプリ名 </Link> <div v-if="page.props.auth.user"> <span>{{ page.props.auth.user.name }}</span> <Link href="/logout" method="post" as="button" class="ml-4" > ログアウト </Link> </div> </div> </nav> <main> <slot /> </main> </div> </template>
- 共通コンポーネントの作成
<!-- resources/js/Components/Button.vue --> <script setup> defineProps({ type: { type: String, default: 'submit' }, disabled: { type: Boolean, default: false } }) </script> <template> <button :type="type" :disabled="disabled" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50" > <slot /> </button> </template>
- フォームコンポーネントの実装
<!-- resources/js/Components/TaskForm.vue --> <script setup> import { useForm } from '@inertiajs/vue3' import Button from '@/Components/Button.vue' const form = useForm({ title: '', description: '', due_date: null }) const submit = () => { form.post('/tasks', { onSuccess: () => { form.reset() } }) } </script> <template> <form @submit.prevent="submit" class="space-y-4"> <div> <label class="block">タイトル</label> <input v-model="form.title" type="text" class="w-full border p-2 rounded" :class="{ 'border-red-500': form.errors.title }" > <div v-if="form.errors.title" class="text-red-500 text-sm"> {{ form.errors.title }} </div> </div> <div> <label class="block">説明</label> <textarea v-model="form.description" class="w-full border p-2 rounded" :class="{ 'border-red-500': form.errors.description }" /> <div v-if="form.errors.description" class="text-red-500 text-sm"> {{ form.errors.description }} </div> </div> <Button :disabled="form.processing"> 保存 </Button> </form> </template>
これらのコンポーネントを使用することで、以下のような利点が得られます:
- コードの再利用性の向上
- 一貫性のあるUI/UXの提供
- メンテナンス性の向上
- 開発効率の向上
また、以下のベストプラクティスを意識することをお勧めします:
- コンポーネントは単一責任の原則に従って設計する
- プロップスの型定義を適切に行う
- コンポーネント間の依存関係を最小限に抑える
- 共通のスタイルやユーティリティを活用する
- パフォーマンスを考慮したコンポーネント設計を行う
実践的なInertia活用テクニック
フォーム処理とバリデーションの実装
- バリデーションの実装
// app/Http/Requests/StoreTaskRequest.php class StoreTaskRequest extends FormRequest { public function rules() { return [ 'title' => ['required', 'string', 'max:255'], 'description' => ['nullable', 'string'], 'due_date' => ['required', 'date', 'after:today'], 'attachments.*' => ['file', 'max:10240', 'mimes:pdf,doc,docx'], ]; } public function messages() { return [ 'title.required' => 'タイトルは必須です', 'due_date.after' => '期限は明日以降の日付を指定してください', 'attachments.*.max' => 'ファイルサイズは10MB以下にしてください', ]; } }
- 高度なフォーム処理
<!-- resources/js/Components/AdvancedTaskForm.vue --> <script setup> import { useForm } from '@inertiajs/vue3' import { ref, watch } from 'vue' const props = defineProps({ task: { type: Object, default: () => ({}) } }) const form = useForm({ title: props.task?.title ?? '', description: props.task?.description ?? '', due_date: props.task?.due_date ?? null, attachments: [], _method: props.task?.id ? 'PUT' : 'POST' }) const fileInput = ref(null) const previewUrls = ref([]) // ファイルアップロードの処理 const handleFileUpload = (e) => { const files = Array.from(e.target.files) form.attachments = files // プレビューURLの生成 previewUrls.value = files.map(file => URL.createObjectURL(file)) } // フォーム送信処理 const submit = () => { const url = props.task?.id ? `/tasks/${props.task.id}` : '/tasks' form.post(url, { onSuccess: () => { form.reset() previewUrls.value = [] }, preserveScroll: true }) } // コンポーネントのクリーンアップ onBeforeUnmount(() => { previewUrls.value.forEach(url => URL.revokeObjectURL(url)) }) </script> <template> <form @submit.prevent="submit" class="space-y-6"> <!-- ファイルドロップゾーン --> <div @drop.prevent="handleFileUpload" @dragover.prevent class="border-2 border-dashed p-8 text-center" > <input ref="fileInput" type="file" multiple class="hidden" @change="handleFileUpload" > <button type="button" @click="fileInput.click()" class="px-4 py-2 bg-gray-100 rounded" > ファイルを選択 </button> </div> <!-- ファイルプレビュー --> <div v-if="previewUrls.length" class="grid grid-cols-3 gap-4"> <div v-for="(url, index) in previewUrls" :key="index" class="relative" > <img :src="url" class="w-full h-32 object-cover rounded" > <button type="button" @click="removeFile(index)" class="absolute top-0 right-0 p-1 bg-red-500 text-white rounded-full" > × </button> </div> </div> </form> </template>
SPAらしい画面遷移の実現方法
- プログレスバーの実装
// resources/js/app.js import { InertiaProgress } from '@inertiajs/progress' InertiaProgress.init({ color: '#4B5563', showSpinner: true, includeCSS: true, })
- ページトランジションの実装
<!-- resources/js/Layouts/AppLayout.vue --> <script setup> import { onMounted, ref } from 'vue' import { router } from '@inertiajs/vue3' const isNavigating = ref(false) onMounted(() => { router.on('start', () => isNavigating.value = true) router.on('finish', () => isNavigating.value = false) }) </script> <template> <div> <Transition enter-active-class="transition-opacity duration-300" enter-from-class="opacity-0" enter-to-class="opacity-100" leave-active-class="transition-opacity duration-300" leave-from-class="opacity-100" leave-to-class="opacity-0" > <div v-if="isNavigating" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center" > <div class="animate-spin rounded-full h-32 w-32 border-t-2 border-b-2 border-white"></div> </div> </Transition> <slot /> </div> </template>
認証機能の統合とセキュリティ対策
- 認証ミドルウェアの設定
// routes/web.php Route::middleware(['auth', 'verified'])->group(function () { Route::get('/dashboard', function () { return Inertia::render('Dashboard'); })->name('dashboard'); Route::resource('tasks', TaskController::class); });
- CSRFトークンの自動処理
// app/Http/Middleware/HandleInertiaRequests.php public function share(Request $request): array { return array_merge(parent::share($request), [ 'csrf_token' => csrf_token(), 'auth' => [ 'user' => $request->user() ? [ 'id' => $request->user()->id, 'name' => $request->user()->name, 'email' => $request->user()->email, 'permissions' => $request->user()->permissions, ] : null, ], 'flash' => [ 'message' => fn () => $request->session()->get('message'), 'type' => fn () => $request->session()->get('type'), ], ]); }
- 権限管理の実装
// app/Policies/TaskPolicy.php class TaskPolicy { public function viewAny(User $user): bool { return true; } public function view(User $user, Task $task): bool { return $user->id === $task->user_id || $user->hasRole('admin'); } public function create(User $user): bool { return $user->hasPermissionTo('create tasks'); } public function update(User $user, Task $task): bool { return $user->id === $task->user_id || $user->hasRole('admin'); } public function delete(User $user, Task $task): bool { return $user->id === $task->user_id || $user->hasRole('admin'); } }
- セキュアなファイルアップロード処理
// app/Http/Controllers/TaskController.php public function store(StoreTaskRequest $request) { $task = DB::transaction(function () use ($request) { $task = Task::create([ 'user_id' => $request->user()->id, 'title' => $request->title, 'description' => $request->description, 'due_date' => $request->due_date, ]); if ($request->hasFile('attachments')) { foreach ($request->file('attachments') as $file) { $path = $file->store('task-attachments', 'private'); $task->attachments()->create([ 'path' => $path, 'original_name' => $file->getClientOriginalName(), 'mime_type' => $file->getMimeType(), 'size' => $file->getSize(), ]); } } return $task; }); return redirect() ->route('tasks.show', $task) ->with('message', 'タスクが作成されました'); }
これらの実装により、以下のような利点が得られます:
- セキュアなフォーム処理
- スムーズな画面遷移
- 堅牢な認証・認可システム
- 安全なファイル管理
また、以下のセキュリティベストプラクティスを必ず考慮してください:
- 入力値の徹底的なバリデーション
- XSS対策の実施
- CSRF対策の確実な実装
- 適切な認可制御の実装
- ファイルアップロードのセキュリティ対策
パフォーマンス最適化とデバッグ
アセットのバンドル設定とキャッシュ戦略
- Viteの最適化設定
// vite.config.js import { defineConfig } from 'vite'; import laravel from 'laravel-vite-plugin'; import vue from '@vitejs/plugin-vue'; export default defineConfig({ plugins: [ laravel({ input: ['resources/js/app.js'], refresh: true, }), vue({ template: { transformAssetUrls: { base: null, includeAbsolute: false, }, }, }), ], build: { // チャンクサイズの最適化 chunkSizeWarningLimit: 1000, rollupOptions: { output: { manualChunks: { vendor: ['vue', '@inertiajs/vue3'], // 共通コンポーネントの分割 common: ['@/Components/Common'], }, }, }, }, optimizeDeps: { include: ['@inertiajs/vue3'], }, });
- キャッシュ戦略の実装
// app/Http/Middleware/HandleInertiaRequests.php public function version(Request $request): ?string { // アセットバージョンの管理 return parent::version($request); } public function share(Request $request): array { return array_merge(parent::share($request), [ // キャッシュバスティングのためのバージョン情報 'asset_version' => config('app.asset_version'), // データのキャッシュ 'cached_data' => Cache::remember('global_data', 3600, function () { return [ 'settings' => Setting::all(), 'categories' => Category::all(), ]; }), ]); }
- 静的アセットの最適化
// config/filesystems.php 'disks' => [ 'public' => [ 'driver' => 'local', 'root' => public_path('storage'), 'url' => env('APP_URL').'/storage', 'visibility' => 'public', // キャッシュヘッダーの設定 'headers' => [ 'Cache-Control' => 'public, max-age=31536000', ], ], ];
Lazy Loadingのような効果実現方法
- コンポーネントの動的インポート
// resources/js/Pages/Tasks/Index.vue import { defineAsyncComponent } from 'vue' // 大きなコンポーネントを遅延ロード const TaskEditor = defineAsyncComponent(() => import('@/Components/TaskEditor.vue') ) // 複雑なチャートコンポーネントを遅延ロード const TaskAnalytics = defineAsyncComponent(() => import('@/Components/TaskAnalytics.vue') )
- ルートベースの分割
// resources/js/app.js createInertiaApp({ resolve: async (name) => { // 動的インポートによるコード分割 const pages = import.meta.glob('./Pages/**/*.vue') return (await pages[`./Pages/${name}.vue`]()).default }, // ... })
- データの遅延ロード実装
// app/Http/Controllers/TaskController.php public function index() { return Inertia::render('Tasks/Index', [ 'tasks' => Task::query() ->select(['id', 'title', 'status']) // 必要な項目のみ取得 ->paginate(20) ->through(fn($task) => [ 'id' => $task->id, 'title' => $task->title, 'status' => $task->status, // 詳細データへのURL提供 'details_url' => route('tasks.details', $task), ]), ]); } public function details(Task $task) { // 詳細データを非同期で取得 return response()->json([ 'description' => $task->description, 'attachments' => $task->attachments, 'comments' => $task->comments()->with('user')->get(), ]); }
開発時のデバッグテクニックとツール活用
- Inertiaデバッグツールの設定
// resources/js/app.js import { createInertiaApp, plugin as InertiaPlugin } from '@inertiajs/vue3' if (process.env.NODE_ENV === 'development') { const DevTools = await import('@inertiajs/devtools').then(m => m.default) createApp({ render: () => h(app, props) }) .use(InertiaPlugin) .use(DevTools) .mount(el) }
- カスタムデバッグヘルパー
// app/Helpers/Debug.php class Debug { public static function logInertiaRequest($request) { Log::channel('inertia')->info('Inertia Request', [ 'url' => $request->url(), 'method' => $request->method(), 'data' => $request->all(), 'headers' => $request->headers->all(), ]); } public static function logPerformance($operation, callable $callback) { $start = microtime(true); $result = $callback(); $duration = microtime(true) - $start; Log::channel('performance')->info("Performance Log: {$operation}", [ 'duration' => $duration, 'memory' => memory_get_peak_usage(true), ]); return $result; } } // 使用例 public function index() { return Debug::logPerformance('Tasks Index', function () { return Inertia::render('Tasks/Index', [ 'tasks' => Task::paginate(20), ]); }); }
- パフォーマンスモニタリング
// app/Providers/AppServiceProvider.php public function boot() { if (config('app.debug')) { DB::listen(function ($query) { Log::channel('queries')->info('SQL Query', [ 'sql' => $query->sql, 'bindings' => $query->bindings, 'time' => $query->time, ]); }); Event::listen('inertia:start', function ($event) { debug('Inertia request started'); }); Event::listen('inertia:finish', function ($event) { debug('Inertia request finished'); }); } }
パフォーマンス最適化のベストプラクティス:
- フロントエンド最適化
- コンポーネントの適切な分割
- 不要なリレンダリングの防止
- 画像の最適化とLazy Loading
- キャッシュの効果的な活用
- バックエンド最適化
- N+1クエリの防止
- データベースインデックスの適切な設定
- キャッシュ戦略の実装
- 必要なデータのみの取得
- 監視とデバッグ
- パフォーマンスメトリクスの収集
- エラーログの適切な管理
- ユーザー体験の監視
- デバッグツールの効果的な活用
本番環境への展開とメンテナンス
デプロイメントのベストプラクティス
- 本番環境の準備
# 本番環境用の.env設定 APP_ENV=production APP_DEBUG=false # キャッシュの最適化 php artisan config:cache php artisan route:cache php artisan view:cache # アセットのビルド npm run build
- デプロイメントスクリプト
#!/bin/bash # deploy.sh echo "Starting deployment..." # プルリクエストの実行 git pull origin main # Composerの依存関係更新 composer install --no-dev --optimize-autoloader # 環境設定 cp .env.production .env php artisan key:generate # データベースマイグレーション php artisan migrate --force # キャッシュのクリアと再生成 php artisan config:clear php artisan cache:clear php artisan view:clear php artisan config:cache php artisan route:cache php artisan view:cache # npmパッケージのインストールとビルド npm ci npm run build # パーミッションの設定 chown -R www-data:www-data storage bootstrap/cache chmod -R 775 storage bootstrap/cache # サービスの再起動 sudo supervisorctl restart all sudo systemctl reload php8.1-fpm sudo systemctl reload nginx echo "Deployment completed!"
- Nginxの設定
# /etc/nginx/sites-available/your-app.conf server { listen 80; server_name your-domain.com; root /var/www/your-app/public; add_header X-Frame-Options "SAMEORIGIN"; add_header X-Content-Type-Options "nosniff"; index index.php; charset utf-8; location / { try_files $uri $uri/ /index.php?$query_string; } location = /favicon.ico { access_log off; log_not_found off; } location = /robots.txt { access_log off; log_not_found off; } error_page 404 /index.php; location ~ \.php$ { fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; include fastcgi_params; } # 静的アセットのキャッシュ設定 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { expires max; log_not_found off; } location ~ /\.(?!well-known).* { deny all; } }
継続的な統合/デプロイメントの構築
- GitHub Actionsの設定
# .github/workflows/deploy.yml name: Deploy on: push: branches: [ main ] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: '8.1' - name: Install Dependencies run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist - name: Setup Node.js uses: actions/setup-node@v2 with: node-version: '16' - name: Install NPM Dependencies run: npm ci - name: Build Assets run: npm run build - name: Execute Tests run: vendor/bin/phpunit - name: Deploy to Production if: success() uses: appleboy/ssh-action@master with: host: ${{ secrets.SSH_HOST }} username: ${{ secrets.SSH_USERNAME }} key: ${{ secrets.SSH_PRIVATE_KEY }} script: | cd /var/www/your-app ./deploy.sh
- 自動テストの設定
// tests/Feature/TaskTest.php class TaskTest extends TestCase { use RefreshDatabase; public function test_can_view_tasks() { $user = User::factory()->create(); $task = Task::factory()->create(['user_id' => $user->id]); $response = $this->actingAs($user) ->get('/tasks'); $response->assertInertia(fn (Assert $page) => $page ->component('Tasks/Index') ->has('tasks.data', 1) ->where('tasks.data.0.id', $task->id) ); } public function test_can_create_task() { $user = User::factory()->create(); $response = $this->actingAs($user) ->post('/tasks', [ 'title' => 'New Task', 'description' => 'Task Description' ]); $response->assertRedirect(); $this->assertDatabaseHas('tasks', [ 'title' => 'New Task', 'user_id' => $user->id ]); } }
バージョンアップ時の注意点と互換性維持
- アップデート手順の確立
// config/version.php return [ 'current' => '1.0.0', 'minimum_php_version' => '8.1.0', 'minimum_laravel_version' => '10.0.0', 'update_scripts' => [ '1.0.0' => [ 'migrate' => true, 'seed' => false, 'clear_cache' => true, 'custom_script' => 'App\Updates\Version100Update' ] ] ];
- バージョンアップスクリプト
// app/Console/Commands/UpdateApplication.php class UpdateApplication extends Command { protected $signature = 'app:update'; public function handle() { // メンテナンスモードの開始 $this->call('down'); try { // バックアップの作成 $this->call('backup:run'); // 依存関係の更新 $this->info('Updating dependencies...'); exec('composer update'); exec('npm update'); // データベースマイグレーション $this->call('migrate', ['--force' => true]); // キャッシュのクリア $this->call('cache:clear'); $this->call('config:clear'); $this->call('view:clear'); $this->call('route:clear'); // アセットの再ビルド exec('npm run build'); // キャッシュの再生成 $this->call('config:cache'); $this->call('route:cache'); $this->call('view:cache'); $this->info('Update completed successfully!'); } catch (\Exception $e) { $this->error('Update failed: ' . $e->getMessage()); // ロールバックの実行 $this->call('backup:restore', ['backup' => 'latest']); } finally { // メンテナンスモードの解除 $this->call('up'); } } }
- 互換性チェックリスト
- PHP/Laravel/Node.jsのバージョン確認
- データベーススキーマの変更確認
- 依存パッケージの互換性確認
- APIの後方互換性の確認
- フロントエンドの互換性確認
メンテナンスのベストプラクティス:
- 定期的なメンテナンス
- セキュリティアップデートの適用
- パフォーマンスモニタリング
- バックアップの実行と検証
- ログの定期的な確認
- 障害対策
- 監視システムの導入
- バックアップ戦略の確立
- 障害復旧手順の文書化
- インシデント対応計画の策定
- セキュリティ対策
- 脆弱性スキャンの定期実行
- セキュリティパッチの適用
- アクセスログの監視
- セキュリティ監査の実施