
はじめに
Laravelをインストールして初期画面を表示できたら、次はデータベースを使ったアプリを作ってみましょう。
この記事では、Laravel 13・SQLite・Bladeを使い、タスクの登録・一覧表示・編集・削除ができる小さなアプリを作成します。
モデル、マイグレーション、コントローラー、ルーティング、画面の役割を、実装しながら確認できる構成です。
LaravelのCRUDとは?
CRUDとは、データを扱う基本的な4つの操作です。
| 操作 | 意味 | 今回作る機能 |
|---|---|---|
| Create | 作成 | タスクを登録する |
| Read | 読み取り | 保存したタスクを一覧表示する |
| Update | 更新 | タスク名と完了状態を変更する |
| Delete | 削除 | 不要なタスクを削除する |
問い合わせ管理、顧客管理、在庫管理なども、基本はこうしたデータ操作の組み合わせです。
今回は、登録したタスクがページを再読み込みしても残るよう、SQLiteに保存します。
今回の前提と完成するアプリ
前回の導入記事で作成したLaravelプロジェクトを使います。

| 項目 | 今回の構成 |
|---|---|
| 開発環境 | Windows・WSL2・Ubuntu |
| Laravel | 13系 |
| データベース | SQLite |
| 画面 | Blade |
| 入力項目 | タスク名・完了状態 |
| 一覧画面 | /tasks |
| 編集画面 | /tasks/{id}/edit |
コマンドは、WSLのUbuntuターミナルから、Laravelプロジェクトのルートで実行します。artisanやcomposer.jsonがあるフォルダーです。
php artisan --version
Laravel 13系であることを確認してください。
本記事はローカル学習用のサンプルです。ログインやユーザー別のアクセス制御は含めていないため、公開サービスにする場合は認証・認可を追加してください。
1.SQLiteの設定を確認する
プロジェクトの.envを開き、SQLiteを使用する設定を確認します。
DB_CONNECTION=sqlite
DB_DATABASEを独自に設定している場合は、その接続先も確認してください。以下では、標準のdatabase/database.sqliteを使う前提で進めます。
ファイルが存在しない場合に備えて、Ubuntuターミナルで次を実行します。
touch database/database.sqlite
php artisan config:clear
touchは、ファイルが存在しなければ作成するコマンドです。すでにあるデータベースの内容を空にする操作ではありません。
2.Taskモデルとマイグレーションを作成する
次のコマンドを実行します。
php artisan make:model Task -m
主に次の2つが作成されます。
| ファイル | 役割 |
|---|---|
app/Models/Task.php | タスクのデータを扱うモデル |
database/migrations/日時_create_tasks_table.php | tasksテーブルを作るための定義 |
マイグレーションは、テーブル構造をコードとして管理する仕組みです。Laravel公式:マイグレーション
tasksテーブルの項目を定義する
生成されたcreate_tasks_tableのマイグレーションファイルを、次の内容にします。ファイル名の日時部分は、そのままで構いません。
<?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('tasks', function (Blueprint $table) {
$table->id();
$table->string('title', 255);
$table->boolean('is_completed')->default(false);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('tasks');
}
};
各カラムの役割は次のとおりです。
| カラム | 用途 |
|---|---|
id | タスクを識別する番号 |
title | タスク名 |
is_completed | 完了・未完了 |
created_at | 作成日時 |
updated_at | 更新日時 |
マイグレーションを実行する
php artisan migrate
実行状況は次で確認できます。
php artisan migrate:status
対象のマイグレーションがRanになっていることを確認します。
すでに実行したマイグレーションを書き換えても、通常のmigrateでは再実行されません。後から項目を変更する場合は、変更用のマイグレーションを追加します。
3.Taskモデルを設定する
app/Models/Task.phpを次の内容にします。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Task extends Model
{
protected function casts(): array
{
return [
'is_completed' => 'boolean',
];
}
}
is_completedを真偽値として扱う設定です。
LaravelのEloquentでは、通常、Taskモデルがtasksテーブルに対応します。今回はコントローラーで保存する項目を一つずつ代入するため、$fillableの設定は使用しません。Laravel公式:Eloquent
4.CRUDを処理するコントローラーを作成する
次を実行します。
php artisan make:controller TaskController --resource --model=Task
生成されたapp/Http/Controllers/TaskController.phpを、次の内容に置き換えます。
<?php
namespace App\Http\Controllers;
use App\Models\Task;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class TaskController extends Controller
{
public function index(): View
{
return view('tasks.index', [
'tasks' => Task::orderByDesc('id')->get(),
]);
}
public function store(Request $request): RedirectResponse
{
$data = $this->validatedData($request);
$task = new Task();
$task->title = $data['title'];
$task->is_completed = $request->boolean('is_completed');
$task->save();
return redirect()
->route('tasks.index')
->with('status', 'タスクを登録しました。');
}
public function edit(Task $task): View
{
return view('tasks.edit', [
'task' => $task,
]);
}
public function update(
Request $request,
Task $task
): RedirectResponse {
$data = $this->validatedData($request);
$task->title = $data['title'];
$task->is_completed = $request->boolean('is_completed');
$task->save();
return redirect()
->route('tasks.index')
->with('status', 'タスクを更新しました。');
}
public function destroy(Task $task): RedirectResponse
{
$task->delete();
return redirect()
->route('tasks.index')
->with('status', 'タスクを削除しました。');
}
private function validatedData(Request $request): array
{
return $request->validate(
[
'title' => ['required', 'string', 'max:255'],
'is_completed' => ['required', 'boolean'],
],
[
'title.required' => 'タスク名を入力してください。',
'title.string' => 'タスク名は文字列で入力してください。',
'title.max' => 'タスク名は255文字以内で入力してください。',
'is_completed.required' => '完了状態を選択してください。',
'is_completed.boolean' => '完了状態の値が不正です。',
]
);
}
}
各メソッドの役割
| メソッド | 処理 |
|---|---|
index | 一覧画面を表示する |
store | 新しいタスクを保存する |
edit | 編集画面を表示する |
update | タスクを更新する |
destroy | タスクを削除する |
今回は登録フォームを一覧画面に置くため、登録専用画面のcreateは使いません。詳細画面のshowも省いています。
CRUDに対応した処理をまとめる方法として、Laravelにはリソースコントローラーが用意されています。Laravel公式:リソースコントローラー
バリデーションで不正な入力を防ぐ
validatedData()では、タスク名の必須入力と文字数、完了状態の値を確認しています。
検証に失敗すると、通常のWebフォームでは前の画面へ戻り、エラーと入力内容がセッションに保存されます。画面側では、後述する@errorとold()で利用します。Laravel公式:バリデーション
5.CRUDのルーティングを設定する
routes/web.phpに、次の読み込みとルートを追加します。
use App\Http\Controllers\TaskController;
use Illuminate\Support\Facades\Route;
Route::resource('tasks', TaskController::class)
->only(['index', 'store', 'edit', 'update', 'destroy']);
Routeの読み込みがすでにある場合は、重複して追加しないでください。既存のトップページ用ルートなどは、そのままで構いません。
導入記事で仮の配列を表示するRoute::get('/tasks', ...)を作っている場合は、その定義を今回のリソースルートへ置き換えます。
登録されたルートを確認します。
php artisan route:list --path=tasks
主な対応は次のとおりです。
| HTTPメソッド | URL | 処理 | ルート名 |
|---|---|---|---|
| GET | /tasks | 一覧表示 | tasks.index |
| POST | /tasks | 登録 | tasks.store |
| GET | /tasks/{task}/edit | 編集画面 | tasks.edit |
| PUT/PATCH | /tasks/{task} | 更新 | tasks.update |
| DELETE | /tasks/{task} | 削除 | tasks.destroy |
Task $taskは、URLに含まれる番号に対応するタスクを受け取ります。これをルートモデルバインディングと呼び、存在しないタスクへのアクセスは通常404になります。Laravel公式:ルートモデルバインディング
6.Bladeで登録・一覧・編集画面を作る
今回は次の4ファイルを作成します。
resources/views/
├── layouts/
│ └── app.blade.php
└── tasks/
├── _form.blade.php
├── index.blade.php
└── edit.blade.php
フォルダーは次のコマンドで作成できます。
mkdir -p resources/views/layouts resources/views/tasks
導入記事のresources/views/tasks.blade.phpと、今回のtasks/index.blade.phpは別のビューです。今回のコントローラーは、新しく作るtasksフォルダー内のビューを使用します。
共通レイアウト
resources/views/layouts/app.blade.phpを作成します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@yield('title', 'タスク管理')</title>
<style>
body {
max-width: 800px;
margin: 40px auto;
padding: 0 16px;
font-family: sans-serif;
line-height: 1.8;
}
input[type="text"] {
box-sizing: border-box;
width: 100%;
padding: 8px;
}
button, select {
padding: 6px 12px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 24px;
}
th, td {
padding: 12px;
border-bottom: 1px solid #ddd;
text-align: left;
overflow-wrap: anywhere;
}
.error {
color: #b91c1c;
}
.notice {
padding: 12px;
background: #ecfdf5;
}
.inline {
display: inline;
}
</style>
</head>
<body>
<main>
@if (session('status'))
<p class="notice" role="status">
{{ session('status') }}
</p>
@endif
@yield('content')
</main>
</body>
</html>
登録・編集で共通の入力欄
resources/views/tasks/_form.blade.phpを作成します。
<p>
<label for="title">タスク名</label>
<input
type="text"
id="title"
name="title"
value="{{ old('title', $task->title ?? '') }}"
maxlength="255"
required
>
</p>
@error('title')
<p class="error" role="alert">{{ $message }}</p>
@enderror
<p>
<label for="is_completed">状態</label>
<select id="is_completed" name="is_completed">
<option
value="0"
@selected(
(string) old(
'is_completed',
(int) ($task->is_completed ?? false)
) === '0'
)
>
未完了
</option>
<option
value="1"
@selected(
(string) old(
'is_completed',
(int) ($task->is_completed ?? false)
) === '1'
)
>
完了
</option>
</select>
</p>
@error('is_completed')
<p class="error" role="alert">{{ $message }}</p>
@enderror
old()は、入力エラーで戻ったときに以前の入力を表示するために使います。編集画面では、以前の入力がなければ保存済みの値を表示します。
登録フォームと一覧画面
resources/views/tasks/index.blade.phpを作成します。
@extends('layouts.app')
@section('title', 'タスク一覧')
@section('content')
<h1>タスク一覧</h1>
<h2>タスクを登録する</h2>
<form action="{{ route('tasks.store') }}" method="POST">
@csrf
@include('tasks._form', ['task' => null])
<button type="submit">登録する</button>
</form>
<h2>登録済みのタスク</h2>
<table>
<thead>
<tr>
<th scope="col">タスク名</th>
<th scope="col">状態</th>
<th scope="col">操作</th>
</tr>
</thead>
<tbody>
@forelse ($tasks as $task)
<tr>
<td>{{ $task->title }}</td>
<td>
{{ $task->is_completed ? '完了' : '未完了' }}
</td>
<td>
<a href="{{ route('tasks.edit', $task) }}">
編集
</a>
<form
class="inline"
action="{{ route('tasks.destroy', $task) }}"
method="POST"
onsubmit="return confirm('このタスクを削除しますか?');"
>
@csrf
@method('DELETE')
<button type="submit">削除</button>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="3">
まだタスクがありません。
</td>
</tr>
@endforelse
</tbody>
</table>
@endsection
編集画面
resources/views/tasks/edit.blade.phpを作成します。
@extends('layouts.app')
@section('title', 'タスクを編集する')
@section('content')
<h1>タスクを編集する</h1>
<form
action="{{ route('tasks.update', $task) }}"
method="POST"
>
@csrf
@method('PUT')
@include('tasks._form', ['task' => $task])
<button type="submit">更新する</button>
<a href="{{ route('tasks.index') }}">一覧へ戻る</a>
</form>
@endsection
@csrfと@methodの役割
@csrfは、フォームにCSRF対策用のトークンを入れます。
HTMLフォームからPUTやDELETEを扱うため、Laravelでは@method('PUT')などで送信する操作を指定します。フォーム自体のmethodはPOSTにします。
また、{{ $task->title }}はHTMLをエスケープして表示します。ユーザーが入力した文字列の表示には、この形式を使用します。Laravel公式:Bladeテンプレート
7.LaravelのCRUDを動作確認する
今回の画面は外部のCSS・JavaScriptに依存しないため、PHPの開発サーバーで確認できます。
php artisan serve
すでにcomposer run devで起動している場合は、そのまま使えます。同じポートで重複起動する必要はありません。
Windows側のブラウザでタスク一覧画面を開きます。
登録・一覧表示を確認する
- タスク名に「LaravelでCRUDを作る」と入力する
- 状態を「未完了」にする
- 「登録する」を押す
- 一覧に表示されることを確認する
- ページを再読み込みしても残っていることを確認する
登録後は一覧へリダイレクトするため、表示された一覧を再読み込みしても同じ登録処理を繰り返しません。
編集・更新を確認する
- 登録したタスクの「編集」を押す
- タスク名を変更する
- 状態を「完了」にする
- 「更新する」を押す
- 一覧へ戻り、名前と状態が変わっていることを確認する
入力エラーを確認する
タスク名を空にして登録しようとすると、通常はブラウザの必須入力チェックが働きます。
サーバー側の検証も確認する場合は、タスク名に半角スペースだけを入力して送信します。標準構成のLaravelでは前後の空白が除去され、必須入力エラーになることを確認します。
削除を確認する
- 「削除」を押す
- 確認ダイアログでキャンセルし、タスクが残ることを確認する
- 再び「削除」を押し、確定する
- 一覧から消え、再読み込みしても戻らないことを確認する
今回の削除は、データベースからレコードを削除する処理です。ごみ箱や復元機能は実装していません。
LaravelのCRUDでよくあるエラー
| エラー・症状 | 確認すること |
|---|---|
no such table: tasks | php artisan migrateを実行したか、接続先のSQLiteが正しいか |
could not find driver | 実行しているPHPにSQLite用のPDOドライバーがあるか |
View [tasks.index] not found | resources/views/tasks/index.blade.phpの場所と名前 |
Route [tasks.store] not defined | リソースルートの設定とroute:listの結果 |
419 Page Expired | @csrf、Cookie、セッション、ページを開き直して改善するか |
更新・削除が405になる | フォームのPOSTと@methodが正しいか |
編集画面が404になる | 指定したタスクが存在するか、すでに削除していないか |
| 導入記事の仮データが表示される | 古いRoute::get('/tasks', ...)が残っていないか |
ルーティングを修正しても反映されない場合は、キャッシュを解除して確認します。
php artisan route:clear
php artisan route:list --path=tasks
.envの変更後は、設定キャッシュも確認します。
php artisan config:clear
データを残したまま調べたい場合は、テーブルを作り直すmigrate:freshを実行せず、まず接続先とマイグレーション状況を確認してください。
次に追加したい機能
CRUDができたら、次の順に機能を追加すると、実際の業務アプリに近づけられます。
- ページネーション:タスクを一定件数ずつ表示する
- 検索・絞り込み:タスク名や完了状態で探す
- 入力処理の整理:Form Requestにバリデーションを分ける
- ログインと認可:自分のタスクだけ操作できるようにする
- 機能テスト:登録・更新・削除・入力エラーを自動確認する
今回の画面はBladeですが、画面の操作性を発展させたい場合は、LaravelでInertia.jsとReactを使うメリットも参考にしてください。
データベースとCRUDの流れを理解しておくと、Reactを使う構成へ進んだときも、Laravel側で必要になる処理を整理しやすくなります。
