Laravel 13とPostgreSQLでToDoアプリ作成📝

1ページ目に戻る

フォームリクエストファイルの作成

画面から送られてくる入力データのバリデーション(入力値チェック)のルールブックとしてRequestファイルの作成します。

ここでは、これから作成する ListControllerToDoController にデータを安全に渡すためのルールブックとして、StoreListRequestStoreToDoRequest を作成します。

以下のコマンドを実行してください。

php artisan make:request StoreListRequest
php artisan make:request StoreToDoRequest

StoreListRequest.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', 
        ];
    }
}
Information

returnが空欄だと安全かどうかわからないため、無視されます。

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/liststore新規リスト(親)の保存
PUT/list/{list}updateリストのタイトル変更
DELETE/list/{list}destroyリストの削除(紐づくタスクも自動消滅)

TodoContoroller

HTTPメソッドエンドポイントアクション何をするか
GET/list/{id}showタスクの一覧表示
POST/todostore新規タスク(子)の保存
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();
    }
}
Information

他の記述として難解になるが、トランザクション処理においてDB::beginTransaction();から始める方法もある

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/liststore新規リスト(親)の保存
PUT/list/{list}updateリストのタイトル変更
DELETE/list/{list}destroyリストの削除(紐づくタスクも自動消滅)

TodoContoroller

HTTPメソッドエンドポイントアクション何をするか
GET/list/{id}showタスクの一覧表示
POST/todostore新規タスク(子)の保存
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)