1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Laravel Bladeで管理画面を作ってみた【MVC】

1
Posted at

前提

  • 今回作るのは、私が以前作ったREFLEC BEAT plus レベル11 難易度表&クリアランク管理サイト というサイトの管理システムです
    • 本アプリの詳細はこちら(README)
    • 簡単にいうと、ゲーセンの店舗情報や譜面の難易度情報を掲載しています
  • 元々API専用でLaravelを動かしていましたが、管理システムが欲しくなったのでBladeでLaravel直結の管理画面を作ることにしました
  • つまり、「API(フロント画面)用のルーティング」と「Blade(管理画面)用のルーティング」を並立させる形となります
  • Laravelはバイブコーディングで仕様もわからないまま無理やり作って動かしてたので、今回はガチの初学者です。頑張ってわかりやすく説明します!

執筆目的

  • 自分の勉強のアウトプットと知識の整理
  • Laravel Bladeを使って管理画面を作ってみたい!という人に読んでほしい
  • 行った参考書↓(めっちゃわかりやすいのでおすすめです!)

本題

今回は既存のshopsテーブルを用いた「店舗一覧画面」「店舗編集画面」の作成に絞って説明していきます。
一旦ミドルウェアとか抜きにして、とにかく画面を作ることを目標にします。

0.モデル(Shop.php)とマイグレーションファイルの用意

私はすでにAPIで使っていたものがありましたが、なければ作りましょう。

Shop.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Shop extends Model
{
    use SoftDeletes;

    protected $fillable = [
        'name',
        'address',
        'lat',
        'lng',
        'price',
        'number_of_machine',
        'description',
        'is_deleted',
    ];
}

SoftDeleteを使うことで論理削除が可能になります。
また、$fillableはセキュリティのために記述します。fillableプロパティを使うことで、fillableで設定した値以外は一括保存・更新処理から除外することができます。

xxxx_xx_xx_xxxxxx_create_shops_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up(): void
    {
        Schema::create('shops', function (Blueprint $table) {
            $table->id(); // id (BIGINT, AUTO_INCREMENT)
            $table->string('name'); // 店舗名
            $table->string('address'); // 住所
            $table->decimal('lat', 10, 7); // 緯度(例: 35.6894871)
            $table->decimal('lng', 10, 7); // 経度(例: 139.6917064)
            $table->integer('price')->nullable(); // 1プレイ料金(円)
            $table->unsignedTinyInteger('number_of_machine')->nullable(); // 設置台数
            $table->text('description')->nullable(); // 備考・メモ
            $table->boolean('is_deleted')->default(false); // 表示用の論理削除フラグ
            $table->softDeletes(); // deleted_at TIMESTAMP NULL

            $table->timestamps(); // created_at / updated_at
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('shops');
    }
};

1.ルータ(routes/web.php)の作成

APIの場合ルータはapi.phpに書いていましたが、bladeの場合はweb.phpに書いていきます。

routes/web.php

Route::get('shops', [\App\Http\Controllers\AdminShopController::class, 'index'])
    ->name('shops.index');

Route::get('shops/{id}/edit', [\App\Http\Controllers\AdminShopController::class, 'edit'])
    ->name('shops.edit');

Route::put('shops/{id}', [\App\Http\Controllers\AdminShopController::class, 'update'])
    ->name('shops.update');

コントローラを使ったときのルータの書き方は以下の通りです。

Route::HTTPメソッド(('URL'),[コントローラ::class,'メソッド'])
->name('ルート名')

後述するコントローラにはよく使われる7つの標準メソッドがあります。
今回は一覧表示、編集、更新を行いたいのでindex, edit, updateを使います。
もうオワコン音ゲーなので店舗数が増えることはないのでcreateが必要ないのが悲しい。。。

メソッド 主な役割 典型的なBladeとの関わり方
index() 一覧表示ページ resources/views/xxx/index.blade.php を返す(例:記事一覧)
create() 新規作成フォーム表示 create.blade.php を返す(例:投稿フォーム)
store() 新規データ保存 フォーム送信データをDBに保存し、redirect() で一覧へ
show(\$id) 詳細表示ページ 特定の1件を取得して show.blade.php に渡す
edit(\$id) 編集フォーム表示 既存データを取得して edit.blade.php に渡す
update(Request \$request,\$id) データ更新 フォーム送信データでDBを更新し、redirect() で詳細ページなどへ
destroy(\$id) データ削除 削除処理後、redirect() で一覧へ

2.コントローラ(app/Http/Controllers/AdminShopController.php)の作成

前述の通り、index, edit, updateのメソッドを書いていきます。

app/Http/Controllers/AdminShopController.php

<?php
namespace App\Http\Controllers;

use App\Models\Shop;
use Illuminate\Http\Request;

class AdminShopController extends Controller
{
    public function index()
    {
        // ショップ一覧を取得
        $shops = Shop::all();

        // ビューにデータを渡して表示
        return view('shops.index', compact('shops'));
    }

    public function edit($id)
    {
        $shop = Shop::findOrFail($id);
        return view('shops.edit', compact('shop'));
    }

    public function update(Request $request, $id)
    {
        $data = $request->validate([
            'name' => 'required|string|max:255',
            'address' => 'required|string|max:255',
            'lat' => 'required|numeric',
            'lng' => 'required|numeric',
            'price' => 'nullable|numeric',
            'number_of_machine' => 'nullable|integer',
            'description' => 'nullable|string|max:1000',
        ]);

        // 論理削除の値を設定(チェックされていない場合はfalse)
        $data['is_deleted'] = $request->has('is_deleted') ? true : false;

        $shop = Shop::findOrFail($id);
        $shop->update($data);

        return redirect()->route('shops.index')->with('success', 'Shop updated successfully.');
    }

}
?>

小分けにして説明します。まずはindex()メソッドから。

    public function index()
    {
        // ショップ一覧を取得
        $shops = Shop::all();

        // ビューにデータを渡して表示
        return view('shops.index', compact('shops'));
    }

$shops = Shop::all();は、shopsテーブルに入っているデータをすべて取得し、$shops変数に入れるという意味です。
この変数を、compact('shops')で後述するビュー側に受け渡します。

次はedit()メソッドです。

    public function edit($id)
    {
        $shop = Shop::findOrFail($id);
        return view('shops.edit', compact('shop'));
    }

一覧ではなく1件ずつの編集なので変数名は単数形です。
findOrFail()で該当IDがなければ404が出るようにしています。

次はupdate()メソッドです。

    public function update(Request $request, $id)
    {
        $data = $request->validate([
            'name' => 'required|string|max:255',
            'address' => 'required|string|max:255',
            'lat' => 'required|numeric',
            'lng' => 'required|numeric',
            'price' => 'nullable|numeric',
            'number_of_machine' => 'nullable|integer',
            'description' => 'nullable|string|max:1000',
        ]);

        // 論理削除の値を設定(チェックされていない場合はfalse)
        $data['is_deleted'] = $request->has('is_deleted') ? true : false;

        $shop = Shop::findOrFail($id);
        $shop->update($data);

        return redirect()->route('shops.index')->with('success', 'Shop updated successfully.');
    }

public function update(Request $request, $id)の部分で、/shops/{id} へのPUTリクエストを受け取っています。
$requestはフォームから送られた入力値を持つオブジェクト(Laravelが自動注入)で、$idはルートパラメータで更新対象の Shop の主キーです。

$request->validate()で入力バリデーションを行い、問題なければ$shop->update($data);で更新し、shops.index(一覧画面)にリダイレクトするという流れです。

3.ビュー(resource/views/shops/index.blade.php,edit.blade.php)の作成

index.blade.phpが店舗一覧画面で、店舗名を押下するとedit.blade.php(店舗編集画面)に遷移するイメージです。

index.blade.php

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ショップ一覧</title>
</head>
<body>
    <h1>ショップ一覧</h1>

    @if(session('success'))
        <p style="color: green;">{{ session('success') }}</p>
    @endif

    <table border="1">
        <thead>
            <tr>
                <th>ID</th>
                <th>店舗名</th>
                <th>住所</th>
                <th>緯度</th>
                <th>経度</th>
                <th>価格</th>
                <th>台数</th>
                <th>説明</th>
                <th>論理削除</th>
            </tr>
        </thead>
        <tbody>
            @foreach($shops as $shop)
                <tr>
                    <td>{{ $shop->id }}</td>
                    <td>
                        <a href="{{ route('shops.edit', $shop->id) }}">
                            {{ $shop->name }}
                        </a>
                    </td>
                    <td>{{ $shop->address }}</td>
                    <td>{{ $shop->lat }}</td>
                    <td>{{ $shop->lng }}</td>
                    <td>{{ $shop->price }}</td>
                    <td>{{ $shop->number_of_machine }}</td>
                    <td>{{ $shop->description }}</td>
                    <td>
                        @if($shop->is_deleted)
                            <span style="color: red;">削除済み</span>
                        @else
                        @endif
                </tr>
            @endforeach
        </tbody>
    </table>
</body>
</html>

edit.blade.php

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ショップ編集</title>
</head>
<body>
    <h1>ショップ編集</h1>

    @if($errors->any())
        <ul>
            @foreach($errors->all() as $error)
                <li style="color: red;">{{ $error }}</li>
            @endforeach
        </ul>
    @endif

    <form action="{{ route('shops.update', $shop->id) }}" method="POST">
        @csrf
        @method('PUT')

        <label for="name">名前:</label>
        <input type="text" id="name" name="name" value="{{ old('name', $shop->name) }}" required><br>

        <label for="address">住所:</label>
        <input type="text" id="address" name="address" value="{{ old('address', $shop->address) }}" required><br>

        <label for="lat">緯度:</label>
        <input type="text" id="lat" name="lat" value="{{ old('lat', $shop->lat) }}" required><br>

        <label for="lng">経度:</label>
        <input type="text" id="lng" name="lng" value="{{ old('lng', $shop->lng) }}" required><br>

        <label for="price">価格:</label>
        <input type="text" id="price" name="price" value="{{ old('price', $shop->price) }}" ><br>

        <label for="number_of_machine">台数:</label>
        <input type="text" id="number_of_machine" name="number_of_machine" value="{{ old('number_of_machine', $shop->number_of_machine) }}" ><br>

        <label for="description">説明:</label>
        <textarea id="description" name="description" >{{ old('description', $shop->description) }}</textarea><br>

        <label for="is_deleted">論理削除:</label>
        <input type="checkbox" id="is_deleted" name="is_deleted" value="1" {{ $shop->is_deleted ? 'checked' : '' }}>

        <button type="submit">更新</button>
    </form>
</body>
</html>

単なるHTMLかと思いきや、なんだかよくわからない@ifとか@foreachとか入ってますね。
これはディレクティブというBlade専用の構文です。Controllerで用意したデータをテンプレート内で使うときにめちゃくちゃ便利です。
主なディレクティブには以下のようなものがあります。

ディレクティブ 役割 使用例
{{ $var }} HTMLエスケープして表示 {{ $title }}
{!! $var !!} HTMLタグをそのまま出力 {!! $html !!}
@if / @elseif / @else / @endif 条件分岐 @if($count > 10) ... @endif
@auth / @endauth ログイン中のみ表示 @auth {{ Auth::user()->name }} @endauth
@guest / @endguest 未ログイン時のみ表示 @guest ログインしてください @endguest
@for / @endfor 通常のfor文 @for($i = 0; $i < 10; $i++) ... @endfor
@foreach / @endforeach 配列やコレクションのループ @foreach($users as $user) {{ $user->name }} @endforeach
@extends レイアウト継承 @extends('layouts.app')
@section / @endsection レイアウトに差し込むコンテンツ定義 @section('title', 'ページタイトル')
@csrf CSRFトークン埋め込み <form>@csrf</form>
@method('PUT') フォームでPUTやDELETEを送る <form>@method('DELETE')</form>

スクリーンショット 2025-08-12 10.43.47.png

スクリーンショット 2025-08-12 10.43.17.png

見た目最悪ですがとりあえず完成です!
管理者私だけなので正直これで十分のですが気が向いたらUIの改善もしていこうと思います。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?