フォームリクエストファイルの作成
画面から送られてくる入力データのバリデーション(入力値チェック)のルールブックとしてRequestファイルの作成します。
ここでは、これから作成する ListController と ToDoController にデータを安全に渡すためのルールブックとして、StoreListRequest と StoreToDoRequest を作成します。
以下のコマンドを実行してください。
php artisan make:request StoreListRequestphp artisan make:request StoreToDoRequestStoreListRequest.php
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreListRequest extends FormRequest
{
/**
* このリクエストを実行する権限がユーザーにあるか判定
*
* @return bool 権限がある場合はtrue,ない場合はfalse
*/
public function authorize(): bool
{
// 今回は全員に許可するため true
return true;
}
/**
* リクエストに適用するバリデーションのルールを定義
*/
public function rules(): array
{
return [
// データの安全性を担保しつつ、$request->validated() で取得可能にする設定
// 空でもOKで、最大30文字
'title' => 'nullable|string|max:30',
];
}
}StoreTodoRequest.php
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreToDoRequest extends FormRequest
{
/**
* このrequestを実行する権限がユーザーにあるか判定
*
* @return bool 権限がある場合はtrue,ない場合はfalse
*/
public function authorize(): bool
{
// 今回は全員に許可するため true
return true;
}
/**
* リクエストに適用するバリデーションのルールを定義
*/
public function rules(): array
{
return [
// どの親カテゴリ(リスト)に紐づくタスクかを判別するため必須(整数型)
'list_id' => 'required|integer',
// 空でもOKで、最大255文字
'title' => 'nullable|string|max:255',
// 完了・未完了のチェック状態(真偽値)、チェックが外されてデータが届かなくても、エラーにせず『未完了(false)』とする
'is_checked' => 'nullable|boolean',
];
}
}コントローラーの作成
続いて、アプリケーションの具体的な処理(データの取得・保存等)を担当するコントローラー(Controller)の作成をします。
これにより、URLと連動したWEBアプリケーションの主要なデータ操作(CRUD処理)を実現できます。
今回は、大元のカテゴリ一覧を管理する ListController.php と、詳細なタスクを管理する ToDoController.php の2つに分けて作成・編集を行います。
HTTPメソッド/エンドポイントの対応表になります。
ListController
| HTTPメソッド | エンドポイント | アクション | 何をするか |
|---|---|---|---|
| GET | / | index | リスト一覧表示 |
| POST | /list | store | 新規リスト(親)の保存 |
| PUT | /list/{list} | update | リストのタイトル変更 |
| DELETE | /list/{list} | destroy | リストの削除(紐づくタスクも自動消滅) |
TodoContoroller
| HTTPメソッド | エンドポイント | アクション | 何をするか |
|---|---|---|---|
| GET | /list/{id} | show | タスクの一覧表示 |
| POST | /todo | store | 新規タスク(子)の保存 |
| PUT | /todo/{todo} | update | タスクのタイトル変更、チェックの更新 |
| DELETE | /todo/{todo} | destroy | タスクの削除 |
ListController.php
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreListRequest;
use App\Models\ListTable;
class ListController extends Controller
{
public function index()
{
// query()によってDB命令文を書く宣言になる。query()は省略可能
$list = ListTable::query()->select('lists.*')
->selectSub(function ($query) {
$query->from('todos')
->whereColumn('todos.list_id', 'lists.id')
->selectRaw('count(*)');
}, 'todo_count')
->latest('id')// IDの降順(最新順)でソート
->get();
// 取得したデータを一覧画面(index.blade.php)に渡して表示
return view('index', compact('list'));
}
public function store(StoreListRequest $request)
{
// バリデーション(入力値チェック)済みのデータを取得
$validated = $request->validated();
// インスタンスを作成し、データをセットしてデータベースへ保存
$newList = new ListTable;
// データベースのカラムに合わせて値をセット
$newList->title = $validated['title'];
// list_id(主キー)は自動的に生成される(オートインクリメント)ので、ここでは指定しない
$newList->save();
// 作成したページへ移動(画面遷移)
return redirect('/list/'.$newList->id);
}
public function destroy(int $id)
{
// マイグレーションで設定しているため、親を消せば、紐づくToDoTableのデータもデータベースが自動削除する
ListTable::query()->where('id', $id)->delete();
return redirect()->back();
}
public function update(StoreListRequest $request, int $id)
{
$validated = $request->validated();
// 指定されたIDのタイトルと更新日時を変更
ListTable::query()->where('id', $id)->update([
'title' => $validated['title'],
'updated_at' => now(),
]);
// 一覧画面の状態を維持するため、元の画面に戻る
return redirect()->back();
}
}TodoController.php
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreTodoRequest;
use App\Models\ListTable;
use App\Models\TodoTable;
/**
* 詳細(Todo一覧)ページのデータ操作を制御する TodoController の定義
*/
class TodoController extends Controller
{
public function store(StoreTodoRequest $request)
{
// バリデーション(入力値チェック)済みのデータを取得
$validated = $request->validated();
// インスタンスを作成し、データをセットしてデータベースへ保存
$newTodo = new TodoTable;
// データベースのカラムに合わせて値をセット
$newTodo->title = $request->title;
$newTodo->list_id = $request->list_id;
// list_id(主キー)は自動的に生成される(オートインクリメント)ので、ここでは指定しない
$newTodo->save();
// タスクを追加したリストの詳細ページへリダイレクト
return redirect('/list/'.$request->list_id);
}
public function destroy(int $id)
{
// 指定されたIDに対応する子テーブル(todos)のデータを削除
TodoTable::query()->where('id', $id)->delete();
return redirect()->back();
}
public function update(StoreTodoRequest $request, int $id)
{
$validated = $request->validated();
TodoTable::query()->where('id', $id)->update([
'list_id' => $validated['list_id'],
'title' => $validated['title'] ?? '',
// リクエストに is_checked があればその値を、なければ false (0)
'is_checked' => $request->input('is_checked', false),
]);
return redirect()->back();
}
public function show(int $id)
{
// URLのIDを元に、親となるリストを1件取得
$list = ListTable::query()->where('id', $id)->firstOrFail();
// そのリストに紐づくTodo一覧を取得
$todo = TodoTable::query()->where('list_id', $id)->get();
// 親リスト($list)とタスク一覧($todo)の両方をビュー(show.blade.php)に渡して表示
return view('show', compact('list', 'todo'));
}
}ルーティングの設定
HTTPメソッド/エンドポイントの対応表になります。(再掲)
ListController
| HTTPメソッド | エンドポイント | アクション | 何をするか |
|---|---|---|---|
| GET | / | index | リスト一覧表示 |
| POST | /list | store | 新規リスト(親)の保存 |
| PUT | /list/{list} | update | リストのタイトル変更 |
| DELETE | /list/{list} | destroy | リストの削除(紐づくタスクも自動消滅) |
TodoContoroller
| HTTPメソッド | エンドポイント | アクション | 何をするか |
|---|---|---|---|
| GET | /list/{id} | show | タスクの一覧表示 |
| POST | /todo | store | 新規タスク(子)の保存 |
| PUT | /todo/{todo} | update | タスクのタイトル変更、チェックの更新 |
| DELETE | /todo/{todo} | destroy | タスクの削除 |
web.php
ユーザーが特定のURLにアクセスした際に、どのコントローラーの処理を呼び出すか決める「ルーティング」の設定をします。
<?php
use App\Http\Controllers\ListController;
use App\Http\Controllers\ToDoController;
use Illuminate\Support\Facades\Route;
// トップページ(/)にアクセスした際、単にビューを返すのではなく、ListControllerのindexアクションを通す
Route::get('/', [ListController::class, 'index']);
// listのリソースルート
// except(['show']) を指定することで、詳細表示(show)アクションのみを自動生成から除外しています
Route::resource('/list', ListController::class)->except(['show']);
// タスク(ToDo)に関するルート定義(CRUD処理の基本ルートをまとめて自動生成)
Route::resource('/todo', ToDoController::class);
// リスト詳細画面(/list/{id})を開いたときに、そのカテゴリに紐づく「タスク(ToDo)一覧」を同時にまとめて表示したい。
// そのため、詳細表示の処理はすべて「ToDoControllerのshowアクション」に任せる(一本化する)設定にしています。
Route::get('/list/{id}', [ToDoController::class, 'show'])->name('todo.show');
ビューの設定
ユーザーが実際に目にするフロントエンド(画面表示)部分を作成していきます。
Laravelでは、ブラウザに表示される画面(ビュー)にブレード(Blade)テンプレートを使用します。
index.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>ToDo List</title>
</head>
<body>
@php
use App\Models\Lists;
/** @var Lists $item */
@endphp
<ul>
@foreach ($list->sortByDesc('id') as $item)
{{-- ここからリストの表示部分 --}}
<li style="display: flex; align-items: center; gap: 15px; margin-bottom: 10px;">
{{-- 更新日時の表示 --}}
<span>{{$item->updated_at->format('Y-m-d')}}</span>
<span style="font-weight: bold;">({{ $item->todo_count??0 }}件) </span>
{{-- 更新ボタン POST(update) --}}
<form method="POST" action="{{ route('list.update', $item->id) }}"
style="display: flex; gap: 10px; margin: 0;">
@csrf
@method('PUT')
<input type="text" name="title" value="{{ $item->title }}">
<button type="submit">更新</button>
</form>
{{-- リスト一覧へのPOST(show) --}}
<form method="GET" action="{{ route('todo.show', $item->id) }}"
style="display: flex; gap: 10px; margin: 0;">
<button type="submit">リスト</button>
</form>
{{-- 削除ボタン --}}
<form method="POST" action="{{ route('list.destroy', $item->id) }}">
@csrf
@method('DELETE')
<button type="submit" style="color: red;" onclick="return confirm('削除しますか?');">削除</button>
</form>
</li>
@endforeach
</ul>
<hr>
<h2>タスクの作成</h2>
{{-- Laravel開発ではURLを直接書くのではなく、名前付きrouteを使ってURLを自動生成するのが標準
そのため、route('list.store') が一般的 になる
<form method="POST" action="/list">だとURlが変更になった際に、手作業で書き換える必要がでてきて、エラーの原因に --}}
<form method="POST" action="{{ route('list.store') }}">
@csrf
<div style="display: flex; align-items: center; gap: 10px;">
<textarea cols="40" rows="2" name="title" placeholder="タスクの作成">{{ old('title') }}</textarea>
<button type="submit">作成</button>
</div>
</form>
</body>
</html>
show.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>{{ $list->id }} - {{ $list->title }}</title>
</head>
<body>
<h1>{{ $list->title }}</h1>
@php
use App\Models\Lists;
use App\Models\Todo;
/** @var Lists $item */
/** @var Todo $item */
@endphp
<ul>
@foreach ($todo->where('is_checked', false)->sortByDesc('id') as $item)
<li style="display: flex; align-items: center; gap: 15px; margin-bottom: 10px;">
{{-- チェックボックス用 (ONにする) --}}
<form action="{{ route('todo.update', $item->id) }}" method="POST" style="margin:0;">
@csrf @method('PUT')
<input type="hidden" name="list_id" value="{{ $item->list_id }}">
<input type="hidden" name="title" value="{{ $item->title }}">
<input type="hidden" name="is_checked" value="1"> {{-- 1を送る --}}
<input type="checkbox" onchange="this.form.submit()">
</form>
{{-- 更新用 --}}
<form method="POST" action="{{ route('todo.update', $item->id) }}"
style="display: flex; gap: 10px; margin: 0;">
@csrf @method('PUT')
<input type="hidden" name="list_id" value="{{ $item->list_id }}">
<input type="text" name="title" value="{{ $item->title }}">
<button type="submit">更新</button>
</form>
{{-- 削除用 --}}
<form method="POST" action="{{ route('todo.destroy', $item->id) }}" style="margin:0;">
@csrf @method('DELETE')
<button type="submit" style="color: red;">削除</button>
</form>
</li>
@endforeach
</ul>
<hr>
{{-- 完了済みリスト (is_checked が true) --}}
<ul>
@foreach ($todo->where('is_checked', true)->sortByDesc('id') as $item)
<li style="display: flex; align-items: center; gap: 15px; margin-bottom: 10px; opacity: 0.6;">
{{-- チェックボックス用 (OFFに戻す) --}}
<form action="{{ route('todo.update', $item->id) }}" method="POST" style="margin:0;">
@csrf @method('PUT')
<input type="hidden" name="list_id" value="{{ $item->list_id }}">
<input type="hidden" name="title" value="{{ $item->title }}">
<input type="hidden" name="is_checked" value="0"> {{-- 0を送る --}}
<input type="checkbox" checked onchange="this.form.submit()">
</form>
{{-- 更新用 --}}
<form method="POST" action="{{ route('todo.update', $item->id) }}"
style="display: flex; gap: 10px; margin: 0;">
@csrf @method('PUT')
<input type="hidden" name="list_id" value="{{ $item->list_id }}">
<input type="text" name="title" value="{{ $item->title }}"
style="text-decoration: line-through;">
<button type="submit">更新</button>
</form>
{{-- 削除用 --}}
<form method="POST" action="{{ route('todo.destroy', $item->id) }}" style="margin:0;">
@csrf @method('DELETE')
<button type="submit" style="color: red;">削除</button>
</form>
</li>
@endforeach
<hr>
<h2>ToDoの追加</h2>
{{-- route('todo.store') が一般的です --}}
<form method="POST" action="{{ route('todo.store') }}">
@csrf
<div style="display: flex; align-items: center; gap: 10px;">
<input type="hidden" name="list_id" value="{{ $list->id }}">
<textarea cols="40" rows="2" name="title"
placeholder="新しいToDoを入れてください">{{ old('title') }}</textarea>
{{-- name属性でPHP側が判断する --}}
<button type="submit" name="add">追加</button>
</div>
</form>
<hr>
<form method="GET" action="{{ route('list.index') }}">
@csrf
<div style="display: flex; align-items: center; gap: 10px;">
{{-- name属性でPHP側が判断する --}}
<button type="submit" name="return">戻る</button>
</div>
</form>
</ul>
</body>
</html>
すべて保存したら、最後にもう一度サーバーを起動し、http://127.0.0.1:8000にアクセスして動けば完了です。
php artisan serve
まとめ
以上、LoaによるLaravel 13を使用したToDoアプリの作成になります。
Laravelを使用しようとしている方の、手助けになれば幸いですm(_ _”m)
