サイトアイコン IT & ライフハックブログ|学びと実践のためのアイデア集

【実務完全ガイド】Laravelの管理画面設計――CRUD、検索・絞り込み、権限、監査ログ、一括操作、アクセシブルな運用UIの作り方

php elephant sticker

Photo by RealToughCandy.com on Pexels.com

【実務完全ガイド】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で少しずつ育てていきましょう。


参考リンク

モバイルバージョンを終了