リクルート
  • TECH

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

こんにちは!株式会社VALGO、新入社員のLoaです。
今回は研修の過程で、PHPのフレームワーク「Laravel 13」と「PostgreSQL」を使用し、簡単なToDoアプリの作成を行いました。イメージとしてはこんな感じになります!

タスク一覧のホーム画面

タスク詳細画面

LaravelはMVCモデルのフレームワークになります。
MVCモデルとは、アプリの機能を「モデル(Model)」、「ビュー(View)」、「コントローラー(Controller」の3つの役割に分けて開発する設計手法のことです。
What is MVC? | Laravel - The clean stack for Artisans and agents

今回使用したものは、
Laravel 13.11.1PostgreSQL 18.3Docker Desktop(Docker) になります。

開発はローカル環境で行いました。
CSS、HTMLの見た目の部分はいじってないのでかなり淡白な感じになってます。

私自身、調べるのにとても苦労したので、これからLaravelを勉強しようとしている方の手助けになれば幸いですm(_ _“m)

目次

Laravel 13の準備

バージョンの確認

プロジェクト用に作成した空のディレクトリにて、コマンドを実行します。
今回はディレクトリ名をToDoAppで作成してます。

Laravel 13をインストールする前にPHPとComposerのバージョンを確認します。
PHPは8.3以上、Composerは2.2.0以上である必要があります。

php -v
composer -V

Warning

事前にPHPComposerのインストールが必須です。

Laravelの最新版をインストール

Laravelの最新版(2026年5月)である、Laravel 13をインストールします。

composer create-project laravel/laravel .

ファイル構成

主に作成・編集するファイルは以下の通りです。

ToDoApp/
├─app/
│  ├─ Http/            
│  │  ├─ Controllers/
│  │  │  ├─Controller.php                       # (コントローラーのベースクラス)
│  │  │  ├─ListController.php                   # (リスト管理用)
│  │  │  └─ToDoController.php                   # (ToDoタスク管理用)
│  │  │  
│  │  └─Requests/
│  │    ├─StoreListRequest.php                  # (リスト作成時のバリデーション)
│  │    └─StoreTodoRequest.php                  # (タスク作成時のバリデーション)
│  │  
│  └─Models/
│    ├─ListTable.php                            # (リスト用モデル)
│    └─TodoTable.php                            # (タスク用モデル)

├─database/
│  └─migrations/
│    ├─0001_01_01_000000_create_users_table.php    # (ユーザー用テーブル)
│    ├─0001_01_01_000001_create_cache_table.php    # (キャッシュ管理用テーブル)
│    ├─0001_01_01_000002_create_jobs_table.php     # (ジョブ管理用テーブル)
│    ├─0001_01_01_000003_create_lists_table.php    # (リスト保存用テーブル)
│    └─0001_01_01_000004_create_todos_table.php    # (タスク保存用テーブル)  

├─resources/
│  ├─css/                                       # (CSSファイル配置用ディレクトリ)
│  ├─js/                                        # (JavaScriptファイル配置用ディレクトリ)
│  └─views/
│    ├─index.blade.php                          # (ToDo一覧画面のビュー)
│    └─show.blade.php                           # (タスク詳細画面のビュー)

├─routes/
│ └─web.php                                     # (Web APIルーティング設定)

├─.env                                          # (環境変数・データベース接続設定)
├─docker-compose.yml                            # (Dockerコンテナ全体の構成定義)
└─Dockerfile                                    # (PHP環境のDockerイメージ定義)

Dockerコンテナの作成

開発用データベースを構築するため、Dockerコンテナの作成を行います。
プロジェクトのルートディレクトリに、Dockerの構成ファイルである
Dockerfiledocker-compose.yml を作成します。

Dockerfile(PostgreSQLをベースにした日本語環境を構築)

FROM postgres:18

# 日本語ロケールの生成
RUN apt-get update && \
    apt-get install -y locales && \
    sed -i -e 's/# ja_JP.UTF-8 UTF-8/ja_JP.UTF-8 UTF-8/' /etc/locale.gen && \
    locale-gen && \
    update-locale LANG=ja_JP.UTF-8 && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# 環境変数
ENV TZ=Asia/Tokyo
ENV LANG=ja_JP.UTF-8

docker-compose.yml(データベースを起動・運用するためのルールの設定)

# コンテナ(開発環境)の定義
services:
    db:
        # image: の代わりに build: を使い、「Dockerfileを参照してビルド」と指示
        build: .

        # コンテナ名の指定
        container_name: todoapp_db

        # コンテナ停止時に自動で再起動
        restart: always

        ports:
            - 5432:5432
        volumes:
            - db-store:/var/lib/postgresql/data
        environment:
            POSTGRES_USER: 'postgres'
            POSTGRES_PASSWORD: '2026'
            POSTGRES_DB: 'ToDoApp_DB'
volumes:
    db-store:

コンテナの作成と起動

ファイルを保存し、Docker Desktopを起動した状態で、コマンドを実行します。
設定ファイル(docker-compose.yml)に基づいて、todoapp_db というデータベースのためのコンテナを作成・起動します。

Docker Desktopでは管理しやすいように、表示はディレクトリ名(todoapp)で表示されます。

このコンテナにデータベースを構築していきます。

docker compose up -d

Information

データベース名(DB_DATABAS)、ユーザー名(EDB_USERNAM)、パスワード(DB_PASSWORD)は docker-compose.yml にて設定します。

こんな感じ

DB作成とそのための環境設定

.env

.env は、環境変数(アプリ全体設定)を管理します。
今回はDockerで作成したPostgreSQLを使用するため、.env内のDB_ から始まる項目を編集します。

DB_DATABAS:、EDB_USERNAMDB_PASSWORDは先ほどdocker-compose.yml に設定したものになります。

DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=ToDoApp_DB # 作成したDBの名前を設定
DB_USERNAME=postgres   # 作成したDBにアクセスするためのユーザー名(通常はpostgres)
DB_PASSWORD=2026       # 作成したDBにアクセスするためのパスワード

マイグレーションの実行

設定が完了し保存したら、マイグレーションコマンドを実行します。
マイグレーションは、設計図ファイルを読み込んで、テーブルを自動で作成・更新する機能です。

php artisan migrate

初回実行時は、まだデータベースそのものが作成されていないため、ターミナルに確認メッセージが表示されます。

WARN; The database 'ToDoApp_DB' does not exist on the 'pgsql' connection.
Would you like to create it? (yes/no) [yes]

Enterキーを押してください。(YESになります)
Laravelが自動的にPostgreSQL内にデータベースを作成し、そのまま初期テーブル(Laravelに最初から入ってる)のマイグレーションまで完了させてくれます。

Warning

この自動作成を成功させるには、.envDB_USERNAME に指定したユーザーが、「CREATEDB権限(データベースを新規作成する権限)」を持っている必要があります。
(通常、インストール時に最初からある postgres というユーザーであれば問題なく作成できます)

これで、開発に必要なデータベース環境がすべて生成されました。

開発サーバーの起動

php artisan serve

ターミナルにhttp://127.0.0.1:8000と表示されたら起動成功です。
ブラウザでこのURLを開くと、Laravelの初期画面が表示されます。

開発サーバーを停止したい場合は、Ctrl+C

テーブルの作成

マイグレーションファイルの生成

次に、作成したデータベース内に使用するテーブルを構築していきます。
今回は”掃除”などの大きなカテゴリの内容を管理するlistsテーブルと、
”窓を拭く”などの具体的なタスクを管理するtodosテーブルを作成します。

Warning

初期テーブルであるusersテーブルcacheテーブルjobsテーブルは削除するとエラーになるため、削除せずそのままの状態で大丈夫です。

以下のコマンドを実行すると database/migrations/ディレクトリ内に新しいマイグレーションファイル(テーブルの設計図ファイル)が生成されます。

php artisan make:migration create_lists_table
php artisan make:migration create_todos_table

続いて、この生成されたファイルを編集してテーブルのカラムや制約を定義します。

0001_01_01_000003_create_lists_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('lists', function (Blueprint $table) {
            $table->id();

            // カテゴリ名は最大30文字、空欄(未入力)も許容する
            $table->string('title',30)->nullable();
     
            // timestamps()を単数形にして、updated_atのみの生成
            $table->timestamp('updated_at');
        });
    }

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

Information

timestamps()では通常 created_at(保存日時)とupdated_at(更新日時)のカラムが自動で作成されます。

0001_01_01_000004_create_todos_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('todos', function (Blueprint $table) {
            $table->id();
            $table->string('title')->nullable();

            // 外部キーを設定して連携
            $table->foreignId('list_id')->constrained('lists', column: 'id')

                  // 親データが削除されたら、子データも自動削除設定
                  ->cascadeOnDelete();

            // 実行済みかどうかの判定項目
            $table->boolean('is_checked')->default(false);
        });
    }

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

これで、マイグレーションファイルが揃いました。
ファイルを保存しコマンドを実行したら、データベースにテーブルが作成されます。

php artisan migrate

モデルの設定

続いて、データベース内に作成したテーブルをLaravel側から簡単に操作するために、モデル(Model) の設定を行います。

Laravelには、「Eloquent(エロクアント)」と呼ばれる非常に強力で便利な
ORM(Object Relational Mapping)」の仕組みが最初から備わっています。

この仕組みがあるため、モデルを定義しておくことで、複雑なSQLを書かなくても、シンプルなコードでデータベースの操作が可能になります。

今回は属性やクラス定数の型宣言を厳格に定義しています。

ListTable.php

<?php

namespace App\Models;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

/**
 * @property int $id
 * @property string $title
 * @property Carbon|null $updated_at
 */
#[Table(name: 'lists', key: 'id')]
class ListTable extends Model
{
    use HasFactory;

    // 作成日時(created_at)は使用しないため、定数にnullを指定して型を定義
    const ?string CREATED_AT = null;

    // 更新日時(updated_at)は使用するため、定数でカラム名を明示的に指定
    const string UPDATED_AT = 'updated_at';

    // 一括代入許可をするカラムの指定
    protected $fillable = ['title'];
}

Information

This #[Table(name: 'lists', key: 'id')] によって、モデル名とテーブル名が一致していなくても、Laravelに正しくテーブルと主キーを認識させることができます。

TodoTable.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

/**
 * @property string $list_id
 * @property string $title
 */
#[Table(name: 'todos', key: 'id')]
class TodoTable extends Model
{
    use HasFactory;

    // 一括代入許可をするカラムの指定
    protected $fillable = ['title', 'list_id'];

    // 今回のテーブルには(created_at/updated_at)がないため、タイムスタンプ機能を無効化
    public $timestamps = false;
}

テーブル定義の確認

ここでモデル作成まで完了したら、Eloquentを活用して、テーブル定義の確認が行えます。

php artisan model:show ListTable
php artisan model:show TodoTable

今設定して頂いたモデル名を使って確認できます。

出力結果

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

画面から送られてくる入力データのバリデーション(入力値チェック)のルールブックとして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処理)を実現できます。

今回は、大元のカテゴリ一覧を管理する ListCont roller.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)