【実務完全ガイド】Laravelの管理画面設計――CRUD、検索・絞り込み、権限、監査ログ、一括操作、アクセシブルな運用UIの作り方
この記事で学べること(要点)
- Laravelで管理画面を作るときに、最初に決めておきたい設計方針
- CRUDの基本構成と、一覧・詳細・編集・削除を保守しやすく組み立てる方法
- 検索、絞り込み、並び替え、ページングを安全かつ使いやすく実装する考え方
- ロール/権限、承認フロー、監査ログ、一括操作など、実務で必要になりやすい管理機能
- BladeコンポーネントとFormRequestを使って、画面の統一感と実装の再利用性を高める方法
- 事故を防ぐ確認UI、削除導線、CSVエクスポート、通知、エラー表示の設計
- アクセシブルな管理画面を実現するための表、フォーム、モーダル、ステータス表示の基本
- テストで管理画面の品質を守る観点
想定読者(だれが得をする?)
- Laravel 初〜中級エンジニア:社内向け・運用向けの管理画面を、場当たり的でなく整った形で作りたい方
- テックリード:増え続ける管理機能を、権限・監査・再利用性を含めて標準化したい方
- PM/CS/運用担当:日々使う管理画面での操作ミスや問い合わせを減らしたい方
- QA/アクセシビリティ担当:運用UIでも、キーボード操作や読み上げに配慮された体験を保証したい方
アクセシビリティレベル:★★★★★
管理画面は社内向けだからと後回しにされがちですが、日常的に長時間使うUIだからこそ、見出し構造、表の読みやすさ、色に依存しない状態表示、エラーサマリ、キーボード操作、確認ダイアログの明確さが大切です。本記事では、その前提で設計を進めます。
1. はじめに:管理画面は「作れればよい」ではなく「安全に運用できる」が大切です
Laravelでアプリを作っていると、一般ユーザー向け画面とは別に、必ずと言ってよいほど管理画面が必要になります。投稿の公開・非公開、ユーザーの停止、注文の確認、問い合わせ対応、ファイル確認、CSV出力、設定変更など、運用に必要な機能は少しずつ増えていきます。最初は「一覧と編集画面があれば十分」と思っていても、しばらくすると検索や絞り込み、権限、監査ログ、一括操作、承認フローが必要になり、画面が複雑になりやすいです。
ここで怖いのは、管理画面の事故です。誤って削除した、権限がない人が触れてしまった、何を変更したのか履歴が残っていない、検索条件が分かりづらく意図しない一括操作をしてしまった、といった問題は、一般公開画面の不具合とは別の意味で大きな影響があります。しかも、管理画面は社内ツールだからとアクセシビリティが軽視されることが多く、使いにくさが業務効率の低下やヒューマンエラーに直結します。
そこで本記事では、Laravelの管理画面を「ただのCRUD画面」ではなく、日々の運用を支える業務UIとして設計するための実務的な考え方を整理します。最初から全部を実装する必要はありませんが、どの順番で整えると壊れにくいかを把握しておくと、後からの手戻りが大きく減ります。
2. まず決める:管理画面の基本方針と責務
管理画面を作る前に、次の4点を決めておくと設計がぶれにくくなります。
- だれが使うのか
- 何を見られて、何を変更できるのか
- 変更の履歴を残すか
- 事故が起きたとき、どう戻せるか
たとえば「管理者だけが使う」と言っても、実際には次のような違いがあります。
- サポート担当:閲覧中心、少数の更新だけ
- コンテンツ担当:記事や画像の編集、公開予約
- 経理担当:請求情報の閲覧、CSV出力
- システム管理者:ユーザー停止、権限変更、設定変更
これらを全部同じ“管理者”として扱うと、すぐに権限が粗くなります。最初から細かすぎるロール設計は不要ですが、「誰がどの画面に入れて、どの操作ができるか」は、コードで表現できる形にしておくべきです。Laravelなら Policy や Gate がその中心になります。
また、管理画面での変更は後から説明責任が求められやすいです。「だれが、いつ、何を変えたか」を残しておくと、障害対応も問い合わせ対応も格段に楽になります。つまり、管理画面は表示と更新だけでなく、権限と監査を最初から考えた方が結果的に楽です。
3. ディレクトリ構成:増えても迷いにくい形にする
管理画面のコードは、一般ユーザー向け画面と混ざるほど分かりにくくなります。最初から完全分離する必要はありませんが、少なくとも名前空間とビューの置き場所は分けておくのがおすすめです。
例として、次のような構成が扱いやすいです。
app/
├─ Http/
│ ├─ Controllers/
│ │ ├─ Admin/
│ │ │ ├─ DashboardController.php
│ │ │ ├─ UserController.php
│ │ │ ├─ OrderController.php
│ │ │ └─ PostController.php
│ ├─ Requests/
│ │ ├─ Admin/
│ │ │ ├─ UserUpdateRequest.php
│ │ │ ├─ OrderSearchRequest.php
│ │ │ └─ PostStoreRequest.php
resources/
└─ views/
├─ admin/
│ ├─ dashboard.blade.php
│ ├─ users/
│ │ ├─ index.blade.php
│ │ ├─ edit.blade.php
│ │ └─ show.blade.php
│ ├─ orders/
│ └─ posts/
└─ components/
├─ admin/
│ ├─ table.blade.php
│ ├─ filter-panel.blade.php
│ ├─ status-badge.blade.php
│ ├─ danger-zone.blade.php
│ └─ pagination-summary.blade.php
└─ form/
ルーティングも分けます。
// routes/web.php
Route::prefix('admin')
->name('admin.')
->middleware(['auth', 'can:access-admin'])
->group(function () {
Route::get('/', \App\Http\Controllers\Admin\DashboardController::class)->name('dashboard');
Route::resource('users', \App\Http\Controllers\Admin\UserController::class)->except(['create', 'store']);
Route::resource('orders', \App\Http\Controllers\Admin\OrderController::class)->only(['index', 'show', 'update']);
Route::resource('posts', \App\Http\Controllers\Admin\PostController::class);
});
このようにしておくと、一般ユーザー向けの画面と責務が自然に分かれますし、レビューでも「これは管理画面の文脈」と分かりやすくなります。
4. 権限設計:管理画面に入れることと、操作できることを分ける
管理画面では、次の2段階で権限を考えると整理しやすいです。
- 管理画面に入れるか
- その画面・その操作ができるか
4.1 Gateで入口を守る
たとえば、管理画面全体にアクセスできるかどうかは Gate でまとめておくと便利です。
// App\Providers\AuthServiceProvider.php
Gate::define('access-admin', function (User $user) {
return in_array($user->role, ['admin', 'operator', 'support'], true);
});
4.2 Policyで操作を守る
個別の編集や削除は Policy に寄せます。
// app/Policies/UserPolicy.php
class UserPolicy
{
public function viewAny(User $user): bool
{
return in_array($user->role, ['admin', 'support'], true);
}
public function update(User $user, User $target): bool
{
return $user->role === 'admin';
}
public function suspend(User $user, User $target): bool
{
return $user->role === 'admin' && $user->id !== $target->id;
}
}
コントローラ側では authorize を徹底します。
public function update(UserUpdateRequest $request, User $user)
{
$this->authorize('update', $user);
$user->update($request->validated());
return redirect()->route('admin.users.show', $user)
->with('status', 'ユーザー情報を更新しました。');
}
UI側では @can でボタンの表示を制御しますが、最終防衛は必ずサーバ側の authorize です。表示を隠すだけでは安全ではありません。
5. 一覧画面の設計:管理画面の品質は一覧で決まります
管理画面で最も使用頻度が高いのは一覧画面です。だからこそ、ここを丁寧に設計すると運用が安定します。一覧画面の基本要素は、次のように整理できます。
- 件数表示
- 検索・絞り込み
- 並び替え
- ページング
- 行ごとの主要情報
- 状態表示
- 行ごとの操作
- 一括操作(必要な場合)
5.1 件数を明示する
件数が分かるだけで、検索条件や絞り込みが意図どおりか判断しやすくなります。
<h1 class="text-2xl font-semibold" id="page-title" tabindex="-1">ユーザー管理</h1>
<p class="mt-2 text-sm text-gray-700">{{ number_format($users->total()) }}件のユーザーが見つかりました。</p>
5.2 絞り込み条件をフォームにまとめる
検索条件が分散すると、意図が見えなくなります。FormRequest と一緒にまとめて扱うと安全です。
// app/Http/Requests/Admin/UserSearchRequest.php
class UserSearchRequest extends FormRequest
{
public function rules(): array
{
return [
'q' => ['nullable', 'string', 'max:100'],
'status' => ['nullable', 'in:active,suspended'],
'role' => ['nullable', 'in:admin,support,member'],
'sort' => ['nullable', 'in:name,-name,created_at,-created_at'],
];
}
}
public function index(UserSearchRequest $request)
{
$query = User::query()
->select(['id', 'name', 'email', 'role', 'status', 'created_at'])
->when($request->filled('q'), function ($q) use ($request) {
$keyword = $request->string('q')->toString();
$q->where(function ($w) use ($keyword) {
$w->where('name', 'like', "%{$keyword}%")
->orWhere('email', 'like', "%{$keyword}%");
});
})
->when($request->filled('status'), fn ($q) => $q->where('status', $request->status))
->when($request->filled('role'), fn ($q) => $q->where('role', $request->role));
$users = $query->latest()->paginate(20)->withQueryString();
return view('admin.users.index', compact('users'));
}
この形にすると、条件追加や修正がしやすくなります。
6. 表(テーブル)は「見やすい」だけでなく「読める」ことが大切です
管理画面の一覧はテーブルになることが多いですが、テーブルは構造が正しくないと読み上げで理解しづらくなります。見た目だけで <div> を並べるより、意味のある表として作る方が長期的に安定します。
<table class="w-full border-collapse">
<caption class="sr-only">ユーザー一覧</caption>
<thead>
<tr>
<th scope="col" class="text-left border-b py-2">名前</th>
<th scope="col" class="text-left border-b py-2">メールアドレス</th>
<th scope="col" class="text-left border-b py-2">権限</th>
<th scope="col" class="text-left border-b py-2">状態</th>
<th scope="col" class="text-left border-b py-2">登録日</th>
<th scope="col" class="text-left border-b py-2">操作</th>
</tr>
</thead>
<tbody>
@foreach($users as $user)
<tr class="border-b">
<td class="py-2">{{ $user->name }}</td>
<td class="py-2">{{ $user->email }}</td>
<td class="py-2">{{ $user->role }}</td>
<td class="py-2">
<x-admin.status-badge :status="$user->status" />
</td>
<td class="py-2">{{ $user->created_at->format('Y-m-d') }}</td>
<td class="py-2">
<a href="{{ route('admin.users.show', $user) }}" class="underline">詳細</a>
</td>
</tr>
@endforeach
</tbody>
</table>
ここで大切なのは、状態表示を色だけに頼らないことです。たとえば停止中なら赤いバッジだけでなく、「停止中」という文字を必ず表示します。
7. 詳細画面:確認と操作を分けると事故が減ります
詳細画面では、情報確認と操作を同じ面に置きがちですが、重要な操作ほど視覚的にも構造的にも分けた方が安全です。おすすめは、次の3区分です。
- 基本情報
- 履歴や関連情報
- 危険な操作(停止、削除など)
7.1 危険な操作は「Danger Zone」に分離する
<section aria-labelledby="danger-zone-title" class="mt-8 border border-red-300 rounded p-4">
<h2 id="danger-zone-title" class="text-lg font-semibold text-red-800">重要な操作</h2>
<p class="mt-2 text-sm">この操作は影響が大きく、取り消せない場合があります。</p>
@can('suspend', $user)
<form action="{{ route('admin.users.suspend', $user) }}" method="POST" class="mt-4">
@csrf
<x-button variant="danger" type="submit">このユーザーを停止する</x-button>
</form>
@endcan
</section>
このように“危険な操作”を別のまとまりとして見せると、通常の閲覧操作と混ざりにくくなります。
8. 編集フォーム:一般向けフォームよりも、確認性を重視します
管理画面のフォームでは、入力のしやすさに加えて「何を変更しようとしているか」が明確であることが大切です。特に運用担当が触る画面では、元の値と変更後を把握しやすくする工夫が効きます。
8.1 FormRequestで入力を整理
class UserUpdateRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:50'],
'role' => ['required', 'in:admin,support,member'],
'status' => ['required', 'in:active,suspended'],
];
}
public function attributes(): array
{
return [
'name' => '名前',
'role' => '権限',
'status' => '状態',
];
}
}
8.2 エラーサマリを共通化
<x-form.error-summary :errors="$errors" />
8.3 変更の意味が伝わるラベルにする
「status」ではなく「状態」、「role」ではなく「権限」と書くだけで理解度が上がります。社内用画面ほど、英語の内部名ではなく自然言語ラベルが効きます。
9. 一括操作:便利ですが、事故を起こしやすい機能です
一括公開、一括停止、一括削除、一括エクスポートなどは便利ですが、誤操作の影響が大きいです。実装するなら、次のルールをおすすめします。
- 対象件数を明示する
- 対象の条件を見えるようにする
- 取り消せない操作は確認画面または確認文入力を挟む
- 実行できる権限を厳格にする
- 監査ログを残す
9.1 対象件数の表示例
<p role="status" aria-live="polite" class="text-sm">
現在、{{ $selectedCount }}件を選択しています。
</p>
9.2 一括削除は“本当に必要か”を見直す
一括削除は便利ですが、可能なら「一括非公開」や「一括停止」で代替した方が安全です。物理削除より、状態変更の方が事故復旧しやすいからです。
10. 監査ログ:管理画面の信頼性を支える要です
管理画面で特に重要なのは、「何が起きたか」が後から分かることです。最低限、次の情報を残すとよいです。
- 操作したユーザー
- 対象ID
- 操作内容
- 変更前 / 変更後(必要な範囲で)
- 実行日時
trace_idや IP などの補助情報
例として、権限変更時の監査ログを残します。
AuditLog::create([
'actor_user_id' => auth()->id(),
'action' => 'user.role.updated',
'target_type' => User::class,
'target_id' => $user->id,
'before' => ['role' => $beforeRole],
'after' => ['role' => $user->role],
'trace_id' => request()->header('X-Trace-Id'),
]);
監査ログは、問題が起きてから「残しておけばよかった」と思いやすい機能なので、重要画面から優先的に導入するとよいです。
11. CSVエクスポート:運用では重要ですが、同期でやると危険です
管理画面ではCSVエクスポートがよく求められます。ただし、件数が多い場合に同期で処理すると、タイムアウトやメモリ不足の原因になります。基本はジョブで非同期にして、完了後にダウンロードできる形が安全です。
11.1 非同期の流れ
- 条件を受け取る
- ジョブを投入
- 「エクスポートを開始しました」と通知
- 完了後にダウンロードリンクを通知または一覧表示
<div role="status" aria-live="polite" class="border p-3 mb-4">
エクスポートを開始しました。完了するとダウンロードできます。
</div>
アクセシビリティの観点でも、「押したけれど何が起きたか分からない」を避けるために、開始と完了の案内は必須です。
12. 管理画面の通知:成功・失敗・警告を文言で明確にする
管理画面では通知が多くなりがちですが、種類を整理すると伝わりやすくなります。
- 成功:
role="status" - 警告や重大な失敗:
role="alert" - 長時間の進捗:
aria-live="polite"を必要な箇所だけ
例:
@if(session('status'))
<div role="status" class="border border-green-300 bg-green-50 p-3 mb-4">
{{ session('status') }}
</div>
@endif
@if(session('error'))
<div role="alert" class="border border-red-300 bg-red-50 p-3 mb-4">
{{ session('error') }}
</div>
@endif
重要なのは、色だけで意味を伝えないことです。「保存しました」「更新できませんでした」など、テキストで状態が分かるようにします。
13. モーダル確認:便利ですが、乱用しない方が安全です
削除確認や停止確認でモーダルを使いたくなりますが、モーダルはアクセシビリティと実装の難易度が高い部品です。特に管理画面では、確認すべき情報が多いときはモーダルより専用確認画面の方が安全なこともあります。
モーダルを使う場合は、最低限次を守ります。
- 開いたらモーダル内へフォーカス
- Escで閉じられる
- 背景へフォーカスが飛ばない
- 何を確認しているかが見出しで明確
- 確定とキャンセルが分かりやすい
「本当に必要な場面だけ」モーダルにすると、操作ミスが減ります。
14. Bladeコンポーネント:管理画面こそ部品化の効果が大きいです
管理画面は画面数が増えやすいため、部品化の効果が大きいです。おすすめの共通部品は次のようなものです。
- 検索フォーム
- フィルタパネル
- ステータスバッジ
- テーブル
- ページネーションサマリ
- エラーサマリ
- 危険操作ブロック
- 通知メッセージ
たとえばステータスバッジは、色とテキストをセットで標準化できます。
{{-- resources/views/components/admin/status-badge.blade.php --}}
@props(['status'])
@php
$map = [
'active' => ['label' => '有効', 'class' => 'bg-green-100 text-green-800'],
'suspended' => ['label' => '停止中', 'class' => 'bg-red-100 text-red-800'],
'draft' => ['label' => '下書き', 'class' => 'bg-gray-100 text-gray-800'],
'published' => ['label' => '公開中', 'class' => 'bg-blue-100 text-blue-800'],
];
$item = $map[$status] ?? ['label' => $status, 'class' => 'bg-gray-100 text-gray-800'];
@endphp
<span class="inline-flex items-center rounded px-2 py-1 text-sm {{ $item['class'] }}">
{{ $item['label'] }}
</span>
こうしておくと、状態表現が画面ごとにぶれなくなります。
15. テスト:管理画面は事故コストが高いので、守る価値があります
管理画面のテストでは、特に次を重視すると効果が大きいです。
- 未ログインでは入れない
- 権限がないと操作できない
- 検索条件が意図どおりに効く
- 更新後に期待どおりの結果になる
- 監査ログが残る
- 危険操作の確認が機能する
15.1 Featureテスト例
public function test_admin_can_update_user_status()
{
$admin = User::factory()->create(['role' => 'admin']);
$user = User::factory()->create(['status' => 'active']);
$this->actingAs($admin);
$res = $this->patch(route('admin.users.update', $user), [
'name' => $user->name,
'role' => $user->role,
'status' => 'suspended',
]);
$res->assertRedirect(route('admin.users.show', $user));
$this->assertDatabaseHas('users', [
'id' => $user->id,
'status' => 'suspended',
]);
}
15.2 認可テスト例
public function test_support_cannot_update_user_role()
{
$support = User::factory()->create(['role' => 'support']);
$user = User::factory()->create(['role' => 'member']);
$this->actingAs($support);
$this->patch(route('admin.users.update', $user), [
'name' => $user->name,
'role' => 'admin',
'status' => 'active',
])->assertForbidden();
}
重要なフォームについては、Duskなどでエラーサマリへのフォーカスや aria-invalid の回帰を守るのも有効です。
16. よくある落とし穴と回避策
- 管理画面だからと権限が粗い
- 回避:入口と操作を分けて設計し、Policyで守る
- 検索条件が増えて一覧が読みにくい
- 回避:FormRequestで整理し、フィルタUIを一箇所に集約する
- 状態表示が色だけ
- 回避:必ず文字ラベルを付ける
- 一括削除を安易に実装する
- 回避:まずは一括状態変更で代替できないか考える
- 監査ログが無い
- 回避:権限変更、削除、停止など重要操作から優先して残す
- モーダル乱用で操作が分かりにくい
- 回避:専用確認画面の方が安全なケースを見極める
- 社内ツールだからアクセシビリティは不要と思ってしまう
- 回避:日常的に使うUIほど、読みやすさと操作性が業務効率に直結する
17. チェックリスト(配布用)
設計
- [ ] 管理画面の利用者と役割を整理した
- [ ] 入口の権限(Gate)と操作権限(Policy)を分けた
- [ ] 監査ログが必要な操作を決めた
一覧画面
- [ ] 件数表示がある
- [ ] 検索・絞り込み条件がまとまっている
- [ ] 並び替えとページングが安全に実装されている
- [ ] 状態表示が色だけに依存していない
詳細・編集
- [ ] 危険な操作が通常操作と分離されている
- [ ] FormRequest で入力が整理されている
- [ ] エラーサマリと入力紐付けがある
- [ ] 変更後の通知が分かりやすい
運用
- [ ] 一括操作の確認導線がある
- [ ] CSVエクスポートは件数に応じて非同期化を検討している
- [ ] 重要な更新で監査ログが残る
- [ ] 失敗時に trace_id や問い合わせ導線がある
アクセシビリティ
- [ ] 見出し構造がある
- [ ] 表に scope/caption などの意味づけがある
- [ ] キーボードだけで主要操作が完了できる
- [ ]
role="status"/role="alert"が適切に使われている - [ ] モーダルが必要最低限で、Escやフォーカス制御がある
18. まとめ
Laravelの管理画面は、最初は単純なCRUDでも、運用が始まると確実に複雑になります。だからこそ、一覧、権限、監査、一括操作、エラー表示といった“事故が起きやすい場所”から順に標準化していくのが効果的です。管理画面は社内向けだからと雑に作るのではなく、日々の業務を支えるプロダクトとして扱うと、運用コストも問い合わせも減っていきます。特にアクセシビリティは、社外向けだけでなく管理画面でも大きな価値があります。だれでも迷わず、安心して、正しく操作できる管理画面を、Laravelで少しずつ育てていきましょう。

