前提
- 今回作るのは、私が以前作った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> |
見た目最悪ですがとりあえず完成です!
管理者私だけなので正直これで十分のですが気が向いたらUIの改善もしていこうと思います。

