【第四弾】Laravel入門資料
- コース紹介
- Windows環境構築
- Mac環境構築
- Laravelとは
- ルーティング
- ビュー
- コントローラ
- マイグレーション
- モデル
- ここまでの知識の実践
- 検索・フィルタ
- ソフトデリート
- ページネーション
- 画像アップロード
- 認証ライブラリ
- 認証実装
- ミドルウェア・ロール
- 総合実践
📚 Laravel公式ドキュメント
この総合実践では、以下の公式ドキュメントで学んだ知識を全て統合します:
💡 これまでの学習を振り返りながら、実践的なアプリケーションを構築しましょう。
総合実践: TODOアプリを作ろう
これまで学んだ全ての知識を使って、実践的なTODOアプリを作成します。
認証、CRUD、ロール管理、ページネーション、検索など、全てを統合したアプリケーションです。
🎯 このアプリで使う技術
- ✅ 認証: ユーザー登録・ログイン・ログアウト
- ✅ CRUD: タスクの作成・表示・更新・削除
- ✅ ロール管理: 管理者と一般ユーザーの権限分け
- ✅ リレーション: ユーザーとタスクの1対多
- ✅ ページネーション: タスク一覧のページ分け
- ✅ 検索・フィルタリング: ステータス別、キーワード検索
- ✅ バリデーション: 入力値チェック
- ✅ ミドルウェア: 認証・権限チェック
📋 アプリの機能
- • 一般ユーザー: 自分のタスクの作成・編集・削除・完了
- • 管理者: 全ユーザーのタスクを閲覧・削除可能
- • ステータス管理: 未着手・進行中・完了の3状態
- • 優先度: 低・中・高の3段階
- • 期限設定: タスクの期限を設定
ステップ1: データベース設計
⚠️ Laravelプロジェクトがまだの場合
Windows環境構築セクションまたはMac環境構築セクションを参照して、プロジェクトを作成してください。
必要なテーブル
users テーブル
├─ id
├─ name
├─ email (unique)
├─ password
├─ role (default: 'user') ← 'user' or 'admin'
├─ created_at
└─ updated_at
tasks テーブル
├─ id
├─ user_id (外部キー → users.id)
├─ title
├─ description (nullable)
├─ status (default: 'pending') ← 'pending', 'in_progress', 'completed'
├─ priority (default: 'medium') ← 'low', 'medium', 'high'
├─ due_date (nullable)
├─ image (nullable) ← 画像ファイルパス
├─ completed_at (nullable)
├─ created_at
└─ updated_at
マイグレーション作成
# usersテーブルにroleカラム追加
php artisan make:migration add_role_to_users_table
# tasksテーブル作成
php artisan make:migration create_tasks_table
// database/migrations/xxxx_add_role_to_users_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::table('users', function (Blueprint $table) {
$table->enum('role', ['admin', 'user'])->default('user')->after('password');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('role');
});
}
};
// database/migrations/xxxx_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->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('title');
$table->text('description')->nullable();
$table->enum('status', ['pending', 'in_progress', 'completed'])->default('pending');
$table->enum('priority', ['low', 'medium', 'high'])->default('medium');
$table->date('due_date')->nullable();
$table->string('image')->nullable(); // 画像パス
$table->timestamp('completed_at')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('tasks');
}
};
# マイグレーション実行
php artisan migrate
✅ データベース設計が完了しました!
ステップ2: モデル作成
Userモデルの修正
// app/Models/User.php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
protected $fillable = [
'name',
'email',
'password',
'role',
];
protected $hidden = [
'password',
'remember_token',
];
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
/**
* ユーザーが持つタスク
*/
public function tasks()
{
return $this->hasMany(Task::class);
}
/**
* 管理者かどうか
*/
public function isAdmin(): bool
{
return $this->role === 'admin';
}
/**
* 特定のロールを持っているか
*/
public function hasRole(string $role): bool
{
return $this->role === $role;
}
}
Taskモデルの作成
# Taskモデル作成
php artisan make:model Task
// app/Models/Task.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
class Task extends Model
{
protected $fillable = [
'user_id',
'title',
'description',
'status',
'priority',
'due_date',
'image',
'completed_at',
];
protected $casts = [
'due_date' => 'date',
'completed_at' => 'datetime',
];
/**
* タスクの所有者
*/
public function user()
{
return $this->belongsTo(User::class);
}
/**
* 完了済みかどうか
*/
public function isCompleted(): bool
{
return $this->status === 'completed';
}
/**
* 期限切れかどうか
*/
public function isOverdue(): bool
{
if (!$this->due_date || $this->isCompleted()) {
return false;
}
return $this->due_date->isPast();
}
/**
* タスクを完了にする
*/
public function markAsCompleted(): void
{
$this->update([
'status' => 'completed',
'completed_at' => now(),
]);
}
/**
* 画像のURLを取得
*/
public function getImageUrl(): ?string
{
if (!$this->image) {
return null;
}
return Storage::url($this->image);
}
/**
* 優先度の色を取得(Tailwind CSS用)
*/
public function getPriorityColor(): string
{
return match($this->priority) {
'low' => 'green',
'medium' => 'yellow',
'high' => 'red',
default => 'gray',
};
}
/**
* ステータスの色を取得
*/
public function getStatusColor(): string
{
return match($this->status) {
'pending' => 'gray',
'in_progress' => 'blue',
'completed' => 'green',
default => 'gray',
};
}
/**
* 優先度の日本語名を取得
*/
public function getPriorityLabel(): string
{
return match($this->priority) {
'low' => '低',
'medium' => '中',
'high' => '高',
default => '不明',
};
}
/**
* ステータスの日本語名を取得
*/
public function getStatusLabel(): string
{
return match($this->status) {
'pending' => '未着手',
'in_progress' => '進行中',
'completed' => '完了',
default => '不明',
};
}
}
✅ モデルの作成が完了しました!
ステップ3: 認証機能の実装
認証コントローラーの作成
# コントローラー作成
php artisan make:controller Auth/RegisterController
php artisan make:controller Auth/LoginController
php artisan make:controller Auth/LogoutController
// app/Http/Controllers/Auth/RegisterController.php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class RegisterController extends Controller
{
public function showRegistrationForm()
{
return view('auth.register');
}
public function register(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8|confirmed',
]);
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => $validated['password'],
]);
Auth::login($user);
return redirect()->route('tasks.index')
->with('success', 'アカウントを作成しました!');
}
}
// app/Http/Controllers/Auth/LoginController.php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
public function showLoginForm()
{
return view('auth.login');
}
public function login(Request $request)
{
$credentials = $request->validate([
'email' => 'required|email',
'password' => 'required',
]);
if (Auth::attempt($credentials)) {
$request->session()->regenerate();
return redirect()->intended(route('tasks.index'))
->with('success', 'ログインしました!');
}
return back()->withErrors([
'email' => 'メールアドレスまたはパスワードが正しくありません。',
])->onlyInput('email');
}
}
// app/Http/Controllers/Auth/LogoutController.php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LogoutController extends Controller
{
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('login')
->with('success', 'ログアウトしました。');
}
}
認証ルートの設定
// routes/web.php
use App\Http\Controllers\Auth\RegisterController;
use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\Auth\LogoutController;
// ゲスト専用ルート
Route::middleware('guest')->group(function () {
Route::get('/register', [RegisterController::class, 'showRegistrationForm'])->name('register');
Route::post('/register', [RegisterController::class, 'register']);
Route::get('/login', [LoginController::class, 'showLoginForm'])->name('login');
Route::post('/login', [LoginController::class, 'login']);
});
// 認証済みユーザー専用
Route::middleware('auth')->group(function () {
Route::post('/logout', [LogoutController::class, 'logout'])->name('logout');
});
✅ 認証機能の実装が完了しました!
ステップ4: タスクのCRUD実装
TaskControllerの作成
# リソースコントローラー作成
php artisan make:controller TaskController --resource
// app/Http/Controllers/TaskController.php
namespace App\Http\Controllers;
use App\Models\Task;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class TaskController extends Controller
{
/**
* タスク一覧表示(ページネーション・検索付き)
*/
public function index(Request $request)
{
$query = Task::query();
// ログインユーザーのタスクのみ(管理者は全タスク表示)
if (!auth()->user()->isAdmin()) {
$query->where('user_id', auth()->id());
} else {
// 管理者の場合はユーザー情報も一緒に取得
$query->with('user');
}
// ステータスフィルタ
if ($request->filled('status')) {
$query->where('status', $request->status);
}
// 優先度フィルタ
if ($request->filled('priority')) {
$query->where('priority', $request->priority);
}
// キーワード検索
if ($request->filled('search')) {
$query->where(function($q) use ($request) {
$q->where('title', 'like', '%' . $request->search . '%')
->orWhere('description', 'like', '%' . $request->search . '%');
});
}
// 並び順(期限が近い順)
$query->orderByRaw("CASE WHEN status = 'completed' THEN 1 ELSE 0 END")
->orderBy('due_date', 'asc')
->orderBy('created_at', 'desc');
$tasks = $query->paginate(10)->withQueryString();
return view('tasks.index', compact('tasks'));
}
/**
* 📝 補足説明
*
* Task::query()とは?
* - クエリビルダーのインスタンスを取得するメソッド
* - 条件分岐で動的にクエリを組み立てる時に使う
*
* 例:
* $query = Task::query(); // クエリビルダー取得
* if ($status) {
* $query->where('status', $status); // 条件があれば追加
* }
* $tasks = $query->get();
*
* ----------------------------------------
*
* サブクエリ(where内の無名関数)とは?
* - OR条件をグループ化して、他の検索条件と組み合わせるために使う
*
* 例:
* $query->where(function($q) use ($request) {
* $q->where('title', 'like', '%' . $request->search . '%')
* ->orWhere('description', 'like', '%' . $request->search . '%');
* });
*
* これにより生成されるSQL:
* WHERE (title LIKE '%keyword%' OR description LIKE '%keyword%') AND 他の条件
*
* もしwhere()を使わずにorWhere()だけ書くと:
* WHERE title LIKE '%keyword%' OR description LIKE '%keyword%' OR 他の条件
* → 意図しない結果になる!
*
* ----------------------------------------
*
* orderByRaw()とCASE文
* - orderByRaw(): 生のSQL文で並び替え
* - CASE文: SQLの条件分岐(if文のようなもの)
*
* 例:
* $query->orderByRaw("CASE WHEN status = 'completed' THEN 1 ELSE 0 END")
*
* 意味:
* - status = 'completed'(完了)なら「1」を返す
* - それ以外(未完了)なら「0」を返す
* - 0 < 1 なので、未完了が先、完了が後に並ぶ
*
* 複数のorderBy()の優先順位:
* ->orderByRaw("CASE WHEN status = 'completed' THEN 1 ELSE 0 END") // ①完了を後ろに
* ->orderBy('due_date', 'asc') // ②期限が近い順
* ->orderBy('created_at', 'desc'); // ③期限が同じ場合は新しい順
*
* 結果:
* 未完了タスク(期限が近い順)
* ├ 2025-01-05 締切
* └ 2025-01-10 締切
* 完了済みタスク(期限が近い順)
* └ 2025-01-01 締切
*
* ----------------------------------------
*
* withQueryString()とは?
* - ページネーションリンクに現在のクエリパラメータを保持するメソッド
* - 検索条件やフィルタがある場合、ページ移動しても条件が消えない
*
* 例:
* URL: /tasks?status=completed&search=Laravel&page=2
*
* withQueryString()なし:
* → 次ページのリンク: /tasks?page=3(検索条件が消える!)
*
* withQueryString()あり:
* → 次ページのリンク: /tasks?status=completed&search=Laravel&page=3(条件保持!)
*/
/**
* タスク作成フォーム表示
*/
public function create()
{
return view('tasks.create');
}
/**
* タスク保存
*/
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'priority' => 'required|in:low,medium,high',
'due_date' => 'nullable|date|after_or_equal:today',
'image' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048',
]);
// 画像アップロード処理
if ($request->hasFile('image')) {
$validated['image'] = $request->file('image')->store('tasks', 'public');
}
auth()->user()->tasks()->create($validated);
return redirect()->route('tasks.index')
->with('success', 'タスクを作成しました!');
}
/**
* タスク詳細表示
*/
public function show(Task $task)
{
// 自分のタスクまたは管理者のみ閲覧可能
if ($task->user_id !== auth()->id() && !auth()->user()->isAdmin()) {
abort(403, 'このタスクを閲覧する権限がありません。');
}
return view('tasks.show', compact('task'));
}
/**
* タスク編集フォーム表示
*/
public function edit(Task $task)
{
// 自分のタスクのみ編集可能
if ($task->user_id !== auth()->id()) {
abort(403, 'このタスクを編集する権限がありません。');
}
return view('tasks.edit', compact('task'));
}
/**
* タスク更新
*/
public function update(Request $request, Task $task)
{
// 自分のタスクのみ更新可能
if ($task->user_id !== auth()->id()) {
abort(403, 'このタスクを更新する権限がありません。');
}
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'status' => 'required|in:pending,in_progress,completed',
'priority' => 'required|in:low,medium,high',
'due_date' => 'nullable|date',
'image' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048',
]);
// 画像アップロード処理
if ($request->hasFile('image')) {
// 古い画像を削除
if ($task->image) {
Storage::disk('public')->delete($task->image);
}
$validated['image'] = $request->file('image')->store('tasks', 'public');
}
// ステータスが完了に変わった場合、completed_atを設定
if ($validated['status'] === 'completed' && $task->status !== 'completed') {
$validated['completed_at'] = now();
} elseif ($validated['status'] !== 'completed') {
$validated['completed_at'] = null;
}
$task->update($validated);
return redirect()->route('tasks.index')
->with('success', 'タスクを更新しました!');
}
/**
* タスク削除
*/
public function destroy(Task $task)
{
// 自分のタスクまたは管理者のみ削除可能
if ($task->user_id !== auth()->id() && !auth()->user()->isAdmin()) {
abort(403, 'このタスクを削除する権限がありません。');
}
// 画像も削除
if ($task->image) {
Storage::disk('public')->delete($task->image);
}
$task->delete();
return redirect()->route('tasks.index')
->with('success', 'タスクを削除しました。');
}
/**
* タスクを完了にする
*/
public function complete(Task $task)
{
// 自分のタスクのみ完了可能
if ($task->user_id !== auth()->id()) {
abort(403, 'このタスクを完了する権限がありません。');
}
$task->markAsCompleted();
return back()->with('success', 'タスクを完了しました!');
}
/**
* 画像を削除
*/
public function deleteImage(Task $task)
{
// 自分のタスクのみ画像削除可能
if ($task->user_id !== auth()->id()) {
abort(403, 'このタスクの画像を削除する権限がありません。');
}
if ($task->image) {
Storage::disk('public')->delete($task->image);
$task->update(['image' => null]);
}
return back()->with('success', '画像を削除しました。');
}
}
ルート設定
// routes/web.php
use App\Http\Controllers\TaskController;
Route::middleware('auth')->group(function () {
// タスク一覧をトップページに
Route::get('/', [TaskController::class, 'index'])->name('tasks.index');
// タスクのリソースルート
Route::resource('tasks', TaskController::class);
// タスク完了
Route::post('/tasks/{task}/complete', [TaskController::class, 'complete'])
->name('tasks.complete');
});
✅ タスクのCRUD実装が完了しました!
ステップ5: ストレージ設定
シンボリックリンクの作成
アップロードした画像をブラウザからアクセスできるようにします:
# シンボリックリンク作成
php artisan storage:link
これにより storage/app/public が public/storage にリンクされ、
アップロードした画像が /storage/tasks/xxx.jpg のURLでアクセス可能になります。
config/filesystems.phpの確認
// config/filesystems.php
'disks' => [
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
'throw' => false,
],
// ...
],
✅ ストレージ設定が完了しました!
ステップ6: ビューファイルの作成
実際に使えるTODOアプリにするため、全てのビューファイルを作成します。
レイアウトファイル(共通部分)
// 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.0">
<title>@yield('title', 'TODOアプリ')</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100">
{{-- ナビゲーション --}}
<nav class="bg-white shadow-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex items-center">
<a href="{{ route('tasks.index') }}" class="text-xl font-bold text-blue-600">
📝 TODOアプリ
</a>
</div>
<div class="flex items-center gap-4">
@auth
<span class="text-gray-700">{{ auth()->user()->name }}</span>
@if(auth()->user()->isAdmin())
<span class="px-2 py-1 bg-red-100 text-red-800 text-xs rounded">管理者</span>
@endif
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="text-gray-600 hover:text-gray-900">
ログアウト
</button>
</form>
@endauth
</div>
</div>
</div>
</nav>
{{-- フラッシュメッセージ --}}
@if(session('success'))
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4">
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
{{ session('success') }}
</div>
</div>
@endif
{{-- メインコンテンツ --}}
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
@yield('content')
</main>
</body>
</html>
ログインページ
// resources/views/auth/login.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ログイン - TODOアプリ</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100">
<div class="min-h-screen flex items-center justify-center">
<div class="max-w-md w-full bg-white rounded-lg shadow-md p-8">
<h2 class="text-2xl font-bold text-center mb-6">📝 TODOアプリ ログイン</h2>
@if($errors->any())
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
@foreach($errors->all() as $error)
<p>{{ $error }}</p>
@endforeach
</div>
@endif
<form method="POST" action="{{ route('login') }}">
@csrf
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2">
メールアドレス
</label>
<input type="email" name="email" value="{{ old('email') }}"
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
required autofocus>
</div>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2">
パスワード
</label>
<input type="password" name="password"
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
</div>
<button type="submit"
class="w-full bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700 transition">
ログイン
</button>
<p class="text-center mt-4 text-sm text-gray-600">
アカウントをお持ちでない方は
<a href="{{ route('register') }}" class="text-blue-600 hover:underline">新規登録</a>
</p>
</form>
</div>
</div>
</body>
</html>
ユーザー登録ページ
// resources/views/auth/register.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>新規登録 - TODOアプリ</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100">
<div class="min-h-screen flex items-center justify-center">
<div class="max-w-md w-full bg-white rounded-lg shadow-md p-8">
<h2 class="text-2xl font-bold text-center mb-6">📝 TODOアプリ 新規登録</h2>
@if($errors->any())
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
@foreach($errors->all() as $error)
<p>{{ $error }}</p>
@endforeach
</div>
@endif
<form method="POST" action="{{ route('register') }}">
@csrf
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2">名前</label>
<input type="text" name="name" value="{{ old('name') }}"
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
required autofocus>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2">メールアドレス</label>
<input type="email" name="email" value="{{ old('email') }}"
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2">パスワード</label>
<input type="password" name="password"
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
</div>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2">パスワード(確認)</label>
<input type="password" name="password_confirmation"
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
</div>
<button type="submit"
class="w-full bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700 transition">
登録
</button>
<p class="text-center mt-4 text-sm text-gray-600">
既にアカウントをお持ちの方は
<a href="{{ route('login') }}" class="text-blue-600 hover:underline">ログイン</a>
</p>
</form>
</div>
</div>
</body>
</html>
✅ 認証ビューの作成が完了しました!
ステップ7: タスク関連のビュー
タスク一覧ページ
検索・フィルタリング・ページネーション機能付きの一覧ページです。
{{-- resources/views/tasks/index.blade.php --}}
@extends('layouts.app')
@section('title', 'タスク一覧')
@section('content')
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">タスク一覧</h1>
<a href="{{ route('tasks.create') }}"
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition">
+ 新規タスク作成
</a>
</div>
{{-- 検索・フィルタ --}}
<div class="bg-white rounded-lg shadow p-4 mb-6">
<form method="GET" action="{{ route('tasks.index') }}" class="flex flex-wrap gap-4">
<input type="text" name="search" value="{{ request('search') }}"
placeholder="キーワード検索"
class="flex-1 min-w-[200px] px-3 py-2 border border-gray-300 rounded">
<select name="status" class="px-3 py-2 border border-gray-300 rounded">
<option value="">ステータス: すべて</option>
<option value="pending" {{ request('status') == 'pending' ? 'selected' : '' }}>未着手</option>
<option value="in_progress" {{ request('status') == 'in_progress' ? 'selected' : '' }}>進行中</option>
<option value="completed" {{ request('status') == 'completed' ? 'selected' : '' }}>完了</option>
</select>
<select name="priority" class="px-3 py-2 border border-gray-300 rounded">
<option value="">優先度: すべて</option>
<option value="low" {{ request('priority') == 'low' ? 'selected' : '' }}>低</option>
<option value="medium" {{ request('priority') == 'medium' ? 'selected' : '' }}>中</option>
<option value="high" {{ request('priority') == 'high' ? 'selected' : '' }}>高</option>
</select>
<button type="submit" class="bg-gray-600 text-white px-4 py-2 rounded hover:bg-gray-700">
検索
</button>
<a href="{{ route('tasks.index') }}" class="bg-gray-300 text-gray-700 px-4 py-2 rounded hover:bg-gray-400">
クリア
</a>
</form>
</div>
{{-- タスク一覧 --}}
@if($tasks->count() > 0)
<div class="space-y-4">
@foreach($tasks as $task)
<div class="bg-white rounded-lg shadow p-6 hover:shadow-lg transition">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="px-2 py-1 text-xs rounded
{{ $task->getPriorityColor() == 'high' ? 'bg-red-100 text-red-800' : '' }}
{{ $task->getPriorityColor() == 'medium' ? 'bg-yellow-100 text-yellow-800' : '' }}
{{ $task->getPriorityColor() == 'low' ? 'bg-green-100 text-green-800' : '' }}">
優先度: {{ $task->getPriorityLabel() }}
</span>
<span class="px-2 py-1 text-xs rounded
{{ $task->getStatusColor() == 'completed' ? 'bg-green-100 text-green-800' : '' }}
{{ $task->getStatusColor() == 'in_progress' ? 'bg-blue-100 text-blue-800' : '' }}
{{ $task->getStatusColor() == 'pending' ? 'bg-gray-100 text-gray-800' : '' }}">
{{ $task->getStatusLabel() }}
</span>
@if($task->isOverdue())
<span class="px-2 py-1 text-xs rounded bg-red-600 text-white">期限切れ</span>
@endif
</div>
<h3 class="text-lg font-bold text-gray-800 mb-2">
<a href="{{ route('tasks.show', $task) }}" class="hover:text-blue-600">
{{ $task->title }}
</a>
</h3>
@if($task->description)
<p class="text-gray-600 mb-2">{{ Str::limit($task->description, 100) }}</p>
@endif
<div class="text-sm text-gray-500">
@if($task->due_date)
<span>期限: {{ $task->due_date->format('Y年m月d日') }}</span>
@endif
@if(auth()->user()->isAdmin())
<span class="ml-4">作成者: {{ $task->user->name }}</span>
@endif
</div>
</div>
{{-- 画像サムネイル --}}
@if($task->image)
<img src="{{ $task->getImageUrl() }}" alt="タスク画像"
class="w-24 h-24 object-cover rounded ml-4">
@endif
</div>
{{-- アクション --}}
<div class="flex gap-2 mt-4">
@if($task->user_id == auth()->id() && !$task->isCompleted())
<form method="POST" action="{{ route('tasks.complete', $task) }}">
@csrf
<button type="submit" class="text-green-600 hover:text-green-800 text-sm">
✓ 完了にする
</button>
</form>
@endif
@if($task->user_id == auth()->id())
<a href="{{ route('tasks.edit', $task) }}" class="text-blue-600 hover:text-blue-800 text-sm">
編集
</a>
@endif
@if($task->user_id == auth()->id() || auth()->user()->isAdmin())
<form method="POST" action="{{ route('tasks.destroy', $task) }}" class="inline"
onsubmit="return confirm('本当に削除しますか?')">
@csrf
@method('DELETE')
<button type="submit" class="text-red-600 hover:text-red-800 text-sm">
削除
</button>
</form>
@endif
</div>
</div>
@endforeach
</div>
{{-- ページネーション --}}
<div class="mt-6">
{{ $tasks->links() }}
</div>
@else
<div class="bg-white rounded-lg shadow p-8 text-center">
<p class="text-gray-600">タスクがありません。新しいタスクを作成しましょう!</p>
</div>
@endif
@endsection
✅ タスク一覧ビューの作成が完了しました!次のページに続きます...
ステップ8: 管理者用ミドルウェア
IsAdminミドルウェアの作成
php artisan make:middleware IsAdmin
// app/Http/Middleware/IsAdmin.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class IsAdmin
{
public function handle(Request $request, Closure $next)
{
if (!auth()->check()) {
return redirect()->route('login');
}
if (!auth()->user()->isAdmin()) {
abort(403, 'このページにアクセスする権限がありません。');
}
return $next($request);
}
}
ミドルウェアの登録
// bootstrap/app.php
use App\Http\Middleware\IsAdmin;
return Application::configure(basePath: dirname(__DIR__))
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'admin' => IsAdmin::class,
]);
})
->create();
管理者専用ルート
// routes/web.php
use App\Http\Controllers\Admin\DashboardController;
// 管理者専用ルート
Route::middleware(['auth', 'admin'])->prefix('admin')->name('admin.')->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
Route::get('/users', [DashboardController::class, 'users'])->name('users');
});
管理者用コントローラーの作成
mkdir -p app/Http/Controllers/Admin
touch app/Http/Controllers/Admin/DashboardController.php
// app/Http/Controllers/Admin/DashboardController.php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\Task;
class DashboardController extends Controller
{
public function index()
{
$totalUsers = User::count();
$totalTasks = Task::count();
$completedTasks = Task::where('status', 'completed')->count();
$pendingTasks = Task::where('status', 'pending')->count();
return view('admin.dashboard', compact('totalUsers', 'totalTasks', 'completedTasks', 'pendingTasks'));
}
public function users()
{
$users = User::withCount('tasks')->latest()->get();
return view('admin.users', compact('users'));
}
}
管理者用ビューの作成
mkdir -p resources/views/admin
touch resources/views/admin/dashboard.blade.php
touch resources/views/admin/users.blade.php
管理者ダッシュボード
{{-- resources/views/admin/dashboard.blade.php --}}
@extends('layouts.app')
@section('title', '管理者ダッシュボード')
@section('content')
<div class="container mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-8">管理者ダッシュボード</h1>
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div class="bg-white p-6 rounded-lg shadow">
<h3 class="text-gray-600 text-sm">総ユーザー数</h3>
<p class="text-3xl font-bold text-blue-600">{{ $totalUsers }}</p>
</div>
<div class="bg-white p-6 rounded-lg shadow">
<h3 class="text-gray-600 text-sm">総タスク数</h3>
<p class="text-3xl font-bold text-green-600">{{ $totalTasks }}</p>
</div>
<div class="bg-white p-6 rounded-lg shadow">
<h3 class="text-gray-600 text-sm">完了タスク</h3>
<p class="text-3xl font-bold text-purple-600">{{ $completedTasks }}</p>
</div>
<div class="bg-white p-6 rounded-lg shadow">
<h3 class="text-gray-600 text-sm">未着手タスク</h3>
<p class="text-3xl font-bold text-orange-600">{{ $pendingTasks }}</p>
</div>
</div>
<div class="bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-bold mb-4">管理メニュー</h2>
<a href="{{ route('admin.users') }}" class="text-blue-600 hover:underline">
ユーザー一覧を見る →
</a>
</div>
</div>
@endsection
ユーザー一覧ビュー
{{-- resources/views/admin/users.blade.php --}}
@extends('layouts.app')
@section('title', 'ユーザー一覧')
@section('content')
<div class="container mx-auto px-4 py-8">
<div class="flex justify-between items-center mb-8">
<h1 class="text-3xl font-bold">ユーザー一覧</h1>
<a href="{{ route('admin.dashboard') }}" class="text-blue-600 hover:underline">
← ダッシュボードへ戻る
</a>
</div>
<div class="bg-white rounded-lg shadow overflow-hidden">
<table class="min-w-full">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">ID</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">名前</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">メールアドレス</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">権限</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">タスク数</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">登録日</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@foreach ($users as $user)
<tr>
<td class="px-6 py-4 whitespace-nowrap">{{ $user->id }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ $user->name }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ $user->email }}</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full
{{ $user->role === 'admin' ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800' }}">
{{ $user->role === 'admin' ? '管理者' : '一般' }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">{{ $user->tasks_count }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ $user->created_at->format('Y/m/d') }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@endsection
✅ 管理者用ダッシュボードとユーザー一覧機能が完成しました!
ステップ9: タスク作成・編集ビュー
タスク作成フォーム(画像アップロード付き)
{{-- resources/views/tasks/create.blade.php --}}
@extends('layouts.app')
@section('title', 'タスク作成')
@section('content')
<div class="max-w-2xl mx-auto">
<h1 class="text-2xl font-bold text-gray-800 mb-6">新規タスク作成</h1>
@if($errors->any())
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
<ul>
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form method="POST" action="{{ route('tasks.store') }}" enctype="multipart/form-data"
class="bg-white rounded-lg shadow p-6">
@csrf
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2">
タイトル <span class="text-red-500">*</span>
</label>
<input type="text" name="title" value="{{ old('title') }}"
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2">
説明
</label>
<textarea name="description" rows="4"
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500">{{ old('description') }}</textarea>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2">
優先度 <span class="text-red-500">*</span>
</label>
<select name="priority"
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
<option value="low" {{ old('priority') == 'low' ? 'selected' : '' }}>低</option>
<option value="medium" {{ old('priority', 'medium') == 'medium' ? 'selected' : '' }}>中</option>
<option value="high" {{ old('priority') == 'high' ? 'selected' : '' }}>高</option>
</select>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2">
期限
</label>
<input type="date" name="due_date" value="{{ old('due_date') }}"
min="{{ date('Y-m-d') }}"
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2">
画像(任意)
</label>
<input type="file" name="image" accept="image/*"
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500">
<p class="text-gray-500 text-xs mt-1">
JPEG, PNG, GIF形式(最大2MB)
</p>
</div>
<div class="flex gap-4">
<button type="submit"
class="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 transition">
作成
</button>
<a href="{{ route('tasks.index') }}"
class="bg-gray-300 text-gray-700 px-6 py-2 rounded hover:bg-gray-400 transition">
キャンセル
</a>
</div>
</form>
</div>
@endsection
タスク編集フォーム
{{-- resources/views/tasks/edit.blade.php --}}
@extends('layouts.app')
@section('title', 'タスク編集')
@section('content')
<div class="max-w-2xl mx-auto">
<h1 class="text-2xl font-bold text-gray-800 mb-6">タスク編集</h1>
@if($errors->any())
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
<ul>
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
{{-- 現在の画像表示と削除ボタン --}}
@if($task->image)
<div class="bg-white rounded-lg shadow p-6 mb-6">
<h2 class="text-lg font-bold text-gray-800 mb-4">現在の画像</h2>
<img src="{{ $task->getImageUrl() }}" alt="現在の画像" class="w-48 h-48 object-cover rounded mb-4">
<form method="POST" action="{{ route('tasks.delete-image', $task) }}"
onsubmit="return confirm('画像を削除しますか?')">
@csrf
@method('DELETE')
<button type="submit" class="bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 transition">
画像を削除
</button>
</form>
</div>
@endif
{{-- タスク編集フォーム --}}
<form method="POST" action="{{ route('tasks.update', $task) }}" enctype="multipart/form-data"
class="bg-white rounded-lg shadow p-6">
@csrf
@method('PUT')
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2">
タイトル <span class="text-red-500">*</span>
</label>
<input type="text" name="title" value="{{ old('title', $task->title) }}"
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2">
説明
</label>
<textarea name="description" rows="4"
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500">{{ old('description', $task->description) }}</textarea>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2">
ステータス <span class="text-red-500">*</span>
</label>
<select name="status"
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
<option value="pending" {{ old('status', $task->status) == 'pending' ? 'selected' : '' }}>未着手</option>
<option value="in_progress" {{ old('status', $task->status) == 'in_progress' ? 'selected' : '' }}>進行中</option>
<option value="completed" {{ old('status', $task->status) == 'completed' ? 'selected' : '' }}>完了</option>
</select>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2">
優先度 <span class="text-red-500">*</span>
</label>
<select name="priority"
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
<option value="low" {{ old('priority', $task->priority) == 'low' ? 'selected' : '' }}>低</option>
<option value="medium" {{ old('priority', $task->priority) == 'medium' ? 'selected' : '' }}>中</option>
<option value="high" {{ old('priority', $task->priority) == 'high' ? 'selected' : '' }}>高</option>
</select>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2">
期限
</label>
<input type="date" name="due_date"
value="{{ old('due_date', $task->due_date ? $task->due_date->format('Y-m-d') : '') }}"
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2">
画像 {{ $task->image ? '(新しい画像に変更)' : '' }}
</label>
<input type="file" name="image" accept="image/*"
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500">
<p class="text-gray-500 text-xs mt-1">
{{ $task->image ? '新しい画像をアップロードすると、現在の画像が置き換わります。' : 'JPEG, PNG, GIF形式(最大2MB)' }}
</p>
</div>
<div class="flex gap-4">
<button type="submit"
class="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 transition">
更新
</button>
<a href="{{ route('tasks.index') }}"
class="bg-gray-300 text-gray-700 px-6 py-2 rounded hover:bg-gray-400 transition">
キャンセル
</a>
</div>
</form>
</div>
@endsection
タスク詳細ページ
{{-- resources/views/tasks/show.blade.php --}}
@extends('layouts.app')
@section('title', $task->title)
@section('content')
<div class="max-w-3xl mx-auto">
<div class="bg-white rounded-lg shadow p-8">
{{-- ステータスバッジ --}}
<div class="flex items-center gap-2 mb-4">
<span class="px-3 py-1 text-sm rounded
{{ $task->getPriorityColor() == 'high' ? 'bg-red-100 text-red-800' : '' }}
{{ $task->getPriorityColor() == 'medium' ? 'bg-yellow-100 text-yellow-800' : '' }}
{{ $task->getPriorityColor() == 'low' ? 'bg-green-100 text-green-800' : '' }}">
優先度: {{ $task->getPriorityLabel() }}
</span>
<span class="px-3 py-1 text-sm rounded
{{ $task->getStatusColor() == 'completed' ? 'bg-green-100 text-green-800' : '' }}
{{ $task->getStatusColor() == 'in_progress' ? 'bg-blue-100 text-blue-800' : '' }}
{{ $task->getStatusColor() == 'pending' ? 'bg-gray-100 text-gray-800' : '' }}">
{{ $task->getStatusLabel() }}
</span>
@if($task->isOverdue())
<span class="px-3 py-1 text-sm rounded bg-red-600 text-white">期限切れ</span>
@endif
</div>
{{-- タイトル --}}
<h1 class="text-3xl font-bold text-gray-800 mb-4">{{ $task->title }}</h1>
{{-- メタ情報 --}}
<div class="text-sm text-gray-600 mb-6 space-y-1">
<p>作成者: {{ $task->user->name }}</p>
<p>作成日: {{ $task->created_at->format('Y年m月d日 H:i') }}</p>
@if($task->due_date)
<p>期限: {{ $task->due_date->format('Y年m月d日') }}</p>
@endif
@if($task->completed_at)
<p>完了日時: {{ $task->completed_at->format('Y年m月d日 H:i') }}</p>
@endif
</div>
{{-- 画像 --}}
@if($task->image)
<div class="mb-6">
<img src="{{ $task->getImageUrl() }}" alt="{{ $task->title }}"
class="w-full max-w-lg rounded-lg shadow">
</div>
@endif
{{-- 説明 --}}
@if($task->description)
<div class="mb-6">
<h2 class="text-xl font-bold text-gray-800 mb-2">説明</h2>
<p class="text-gray-700 whitespace-pre-wrap">{{ $task->description }}</p>
</div>
@endif
{{-- アクションボタン --}}
<div class="flex gap-4 mt-8">
@if($task->user_id == auth()->id() && !$task->isCompleted())
<form method="POST" action="{{ route('tasks.complete', $task) }}">
@csrf
<button type="submit"
class="bg-green-600 text-white px-6 py-2 rounded hover:bg-green-700 transition">
✓ 完了にする
</button>
</form>
@endif
@if($task->user_id == auth()->id())
<a href="{{ route('tasks.edit', $task) }}"
class="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 transition">
編集
</a>
@endif
@if($task->user_id == auth()->id() || auth()->user()->isAdmin())
<form method="POST" action="{{ route('tasks.destroy', $task) }}" class="inline"
onsubmit="return confirm('本当に削除しますか?')">
@csrf
@method('DELETE')
<button type="submit"
class="bg-red-600 text-white px-6 py-2 rounded hover:bg-red-700 transition">
削除
</button>
</form>
@endif
<a href="{{ route('tasks.index') }}"
class="bg-gray-300 text-gray-700 px-6 py-2 rounded hover:bg-gray-400 transition">
一覧に戻る
</a>
</div>
</div>
</div>
@endsection
✅ 全てのビューファイルの作成が完了しました!
ステップ10: ルート設定の追加
routes/web.phpに画像削除ルートを追加
// routes/web.php
Route::middleware('auth')->group(function () {
// タスク完了
Route::post('/tasks/{task}/complete', [TaskController::class, 'complete'])
->name('tasks.complete');
// 画像削除
Route::delete('/tasks/{task}/delete-image', [TaskController::class, 'deleteImage'])
->name('tasks.delete-image');
});
✅ ルート設定が完了しました!
ステップ11: シーダーで初期データ作成
シーダーの作成
# シーダー作成
php artisan make:seeder UserSeeder
php artisan make:seeder TaskSeeder
// database/seeders/UserSeeder.php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Seeder;
class UserSeeder extends Seeder
{
public function run(): void
{
// 管理者ユーザー
User::create([
'name' => '管理者',
'email' => 'admin@example.com',
'password' => 'password',
'role' => 'admin',
]);
// 一般ユーザー
User::create([
'name' => '田中太郎',
'email' => 'tanaka@example.com',
'password' => 'password',
'role' => 'user',
]);
User::create([
'name' => '佐藤花子',
'email' => 'sato@example.com',
'password' => 'password',
'role' => 'user',
]);
}
}
// database/seeders/TaskSeeder.php
namespace Database\Seeders;
use App\Models\Task;
use App\Models\User;
use Illuminate\Database\Seeder;
class TaskSeeder extends Seeder
{
public function run(): void
{
$tanaka = User::where('email', 'tanaka@example.com')->first();
$sato = User::where('email', 'sato@example.com')->first();
// 田中太郎のタスク
Task::create([
'user_id' => $tanaka->id,
'title' => 'Laravelの勉強',
'description' => 'ルーティング、コントローラー、モデルを学ぶ',
'status' => 'in_progress',
'priority' => 'high',
'due_date' => now()->addDays(3),
]);
Task::create([
'user_id' => $tanaka->id,
'title' => 'TODOアプリの作成',
'description' => '認証機能とCRUD機能を実装',
'status' => 'pending',
'priority' => 'medium',
'due_date' => now()->addWeek(),
]);
Task::create([
'user_id' => $tanaka->id,
'title' => 'データベース設計書を作成',
'description' => 'ER図とテーブル定義書',
'status' => 'completed',
'priority' => 'low',
'completed_at' => now()->subDays(2),
]);
// 佐藤花子のタスク
Task::create([
'user_id' => $sato->id,
'title' => 'プレゼン資料作成',
'description' => '来週のミーティング用',
'status' => 'pending',
'priority' => 'high',
'due_date' => now()->addDays(5),
]);
Task::create([
'user_id' => $sato->id,
'title' => 'コードレビュー',
'description' => 'プルリクエスト#123をレビュー',
'status' => 'in_progress',
'priority' => 'medium',
'due_date' => now()->addDay(),
]);
}
}
// database/seeders/DatabaseSeeder.php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call([
UserSeeder::class,
TaskSeeder::class,
]);
}
}
# シーダー実行
php artisan db:seed
# または、マイグレーション+シーダーを一度に実行
php artisan migrate:fresh --seed
✅ 初期データの作成が完了しました!
まとめ
TODOアプリで使った技術
1. データベース設計
- マイグレーションでusers、tasksテーブル作成
- 外部キー制約でリレーション定義
- enum型でステータス・優先度管理
2. モデルとリレーション
- User hasMany Task(1対多)
- Task belongsTo User
- カスタムメソッド(isCompleted、isOverdue、markAsCompleted)
- Eager Loadingでn+1問題回避
3. 認証機能
- ユーザー登録・ログイン・ログアウト
- パスワードのハッシュ化
- セッション管理
- guestミドルウェアでログイン済みはリダイレクト
4. CRUD操作
- リソースコントローラーで7つのアクション
- index(一覧)、create(作成フォーム)、store(保存)
- show(詳細)、edit(編集フォーム)、update(更新)、destroy(削除)
- バリデーションで入力チェック
5. 権限管理
- ロール(admin、user)でアクセス制御
- ミドルウェアでルート単位の権限チェック
- コントローラーで個別リソースの権限チェック
- 自分のタスクのみ編集可能
- 管理者は全タスク閲覧・削除可能
6. 画像アップロード
- タスクに画像を添付可能
- Storage::disk('public')で画像保存
- 画像の削除・差し替え機能
- バリデーションでファイル形式・サイズチェック
7. 検索・フィルタリング
- ステータス・優先度でフィルタ
- キーワード検索(タイトル・説明文)
- 期限順・作成日順でソート
- 完了タスクは下に表示
8. ページネーション
- paginate(10)で1ページ10件
- withQueryString()で検索条件を保持
- ページリンクの自動生成
9. ビューファイル
- Bladeテンプレートで全画面実装
- レイアウトファイルで共通部分を再利用
- 認証ページ(ログイン・登録)
- タスクCRUD画面(一覧・作成・編集・詳細)
- 画像アップロードフォーム実装
10. シーダー
- シーダーで初期データ作成
- データベースへのテストデータ投入
学んだLaravelの機能一覧
基礎
- ルーティング
- コントローラー
- ビュー(Blade)
- マイグレーション
- モデル
データベース
- Eloquent ORM
- リレーション
- クエリビルダー
- シーダー
認証・権限
- ユーザー認証
- ミドルウェア
- ロール管理
- コントローラーでの権限チェック
応用
- ページネーション
- 検索・フィルタリング
- バリデーション
- 画像アップロード
- ファイルストレージ
- フラッシュメッセージ
- リダイレクト
🎉 総合実践TODOアプリが完成しました!
これまで学んだ全ての知識を使って、実際に動作する実践的なTODOアプリケーションを作成できました。
認証、CRUD、ロール管理、リレーション、ページネーション、検索、画像アップロードなど、実務で必要な機能を全て実装しました。
全てのビューファイル(Blade)も用意されているので、すぐに動かして試すことができます!