【第四弾】Laravel入門資料

📚 Laravel公式ドキュメント

この総合実践では、以下の公式ドキュメントで学んだ知識を全て統合します:

💡 これまでの学習を振り返りながら、実践的なアプリケーションを構築しましょう。

総合実践: TODOアプリを作ろう

これまで学んだ全ての知識を使って、実践的なTODOアプリを作成します。
認証、CRUD、ロール管理、ページネーション、検索など、全てを統合したアプリケーションです。

🎯 このアプリで使う技術

  • 認証: ユーザー登録・ログイン・ログアウト
  • CRUD: タスクの作成・表示・更新・削除
  • ロール管理: 管理者と一般ユーザーの権限分け
  • リレーション: ユーザーとタスクの1対多
  • ページネーション: タスク一覧のページ分け
  • 検索・フィルタリング: ステータス別、キーワード検索
  • バリデーション: 入力値チェック
  • ミドルウェア: 認証・権限チェック

📋 アプリの機能

  • 一般ユーザー: 自分のタスクの作成・編集・削除・完了
  • 管理者: 全ユーザーのタスクを閲覧・削除可能
  • ステータス管理: 未着手・進行中・完了の3状態
  • 優先度: 低・中・高の3段階
  • 期限設定: タスクの期限を設定

ステップ1: データベース設計

必要なテーブル

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)
├─ completed_at (nullable)
├─ created_at
└─ updated_at

マイグレーション作成

# Laravelプロジェクト作成(まだの場合)
composer create-project laravel/laravel todo-app
cd todo-app

# 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->string('role')->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->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;

class Task extends Model
{
    protected $fillable = [
        'user_id',
        'title',
        'description',
        'status',
        'priority',
        'due_date',
        '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(),
        ]);
    }

    /**
     * 優先度の色を取得(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',
        };
    }
}

✅ モデルの作成が完了しました!

ステップ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;

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'));
    }

    /**
     * タスク作成フォーム表示
     */
    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',
        ]);

        auth()->user()->tasks()->create($validated);

        return redirect()->route('tasks.index')
            ->with('success', 'タスクを作成しました!');
    }

    /**
     * タスク詳細表示
     */
    public function show(Task $task)
    {
        // 権限チェック
        $this->authorize('view', $task);

        return view('tasks.show', compact('task'));
    }

    /**
     * タスク編集フォーム表示
     */
    public function edit(Task $task)
    {
        // 権限チェック
        $this->authorize('update', $task);

        return view('tasks.edit', compact('task'));
    }

    /**
     * タスク更新
     */
    public function update(Request $request, Task $task)
    {
        // 権限チェック
        $this->authorize('update', $task);

        $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',
        ]);

        // ステータスが完了に変わった場合、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)
    {
        // 権限チェック
        $this->authorize('delete', $task);

        $task->delete();

        return redirect()->route('tasks.index')
            ->with('success', 'タスクを削除しました。');
    }

    /**
     * タスクを完了にする
     */
    public function complete(Task $task)
    {
        // 権限チェック
        $this->authorize('update', $task);

        $task->markAsCompleted();

        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: ポリシーで権限管理

TaskPolicyの作成

# ポリシー作成
php artisan make:policy TaskPolicy --model=Task
// app/Policies/TaskPolicy.php

namespace App\Policies;

use App\Models\Task;
use App\Models\User;

class TaskPolicy
{
    /**
     * タスクを表示できるか
     */
    public function view(User $user, Task $task): bool
    {
        // 管理者または自分のタスク
        return $user->isAdmin() || $task->user_id === $user->id;
    }

    /**
     * タスクを作成できるか
     */
    public function create(User $user): bool
    {
        // 認証済みユーザーなら誰でも作成可能
        return true;
    }

    /**
     * タスクを更新できるか
     */
    public function update(User $user, Task $task): bool
    {
        // 自分のタスクのみ更新可能(管理者は更新不可)
        return $task->user_id === $user->id;
    }

    /**
     * タスクを削除できるか
     */
    public function delete(User $user, Task $task): bool
    {
        // 管理者または自分のタスク
        return $user->isAdmin() || $task->user_id === $user->id;
    }
}

ポリシーの登録

// app/Providers/AppServiceProvider.php

namespace App\Providers;

use App\Models\Task;
use App\Policies\TaskPolicy;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // ポリシーの登録(Laravel 11では自動検出されるが明示的に登録も可能)
        Gate::policy(Task::class, TaskPolicy::class);
    }
}

Policyの使い方:

// コントローラーで
$this->authorize('update', $task);

// Bladeで
@can('update', $task)
    <a href="{{ route('tasks.edit', $task) }}">編集</a>
@endcan

// コードで
if (auth()->user()->can('delete', $task)) {
    // 削除処理
}

✅ ポリシーでの権限管理が完了しました!

ステップ6: 管理者用ミドルウェア

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');
});

✅ 管理者用ミドルウェアが完成しました!

ステップ7: シーダーで初期データ作成

シーダーの作成

# シーダー作成
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

✅ 初期データの作成が完了しました!

ステップ8: Tinkerで動作確認

Tinkerで実践してみよう

# Tinker起動
php artisan tinker
1. ユーザーとタスクの確認
// 全ユーザーを取得
User::all();

// 田中太郎のタスクを取得
$tanaka = User::where('email', 'tanaka@example.com')->first();
$tanaka->tasks;

// タスク数をカウント
$tanaka->tasks()->count();
// => 3

// 未完了のタスクのみ
$tanaka->tasks()->where('status', '!=', 'completed')->get();
2. タスクの作成
// 新しいタスクを作成
$tanaka->tasks()->create([
    'title' => 'Tinkerから作成したタスク',
    'description' => 'Tinkerでタスク作成をテスト',
    'priority' => 'high',
    'due_date' => now()->addDays(7),
]);

// 確認
$tanaka->tasks()->latest()->first();
3. タスクの更新
// タスクを取得
$task = Task::find(1);

// ステータスを更新
$task->update(['status' => 'in_progress']);

// タスクを完了にする
$task->markAsCompleted();

// 確認
$task->fresh();
$task->status;
// => "completed"
$task->completed_at;
// => Carbon\Carbon instance
4. リレーションの確認
// タスクから所有者を取得
$task = Task::find(1);
$task->user;
// => User {name: "田中太郎", ...}

$task->user->name;
// => "田中太郎"

// Eager Loading(N+1問題を回避)
$tasks = Task::with('user')->get();

foreach ($tasks as $task) {
    echo $task->title . ' - ' . $task->user->name . "\n";
}
5. 権限チェックのテスト
// 管理者ユーザー
$admin = User::where('email', 'admin@example.com')->first();
$admin->isAdmin();
// => true

// 一般ユーザー
$tanaka = User::where('email', 'tanaka@example.com')->first();
$tanaka->isAdmin();
// => false

// ログインシミュレーション
Auth::login($tanaka);

auth()->user()->name;
// => "田中太郎"

auth()->user()->isAdmin();
// => false
6. 検索・フィルタリング
// ステータスでフィルタ
Task::where('status', 'completed')->get();

// 優先度が高いタスク
Task::where('priority', 'high')->get();

// 期限が近いタスク(7日以内)
Task::where('due_date', '<=', now()->addDays(7))
    ->where('status', '!=', 'completed')
    ->orderBy('due_date', 'asc')
    ->get();

// キーワード検索
Task::where('title', 'like', '%Laravel%')->get();

// 複合検索
Task::where('user_id', $tanaka->id)
    ->where('status', 'pending')
    ->where('priority', 'high')
    ->get();
7. ページネーション
// ページネーション(1ページ10件)
$tasks = Task::paginate(10);

$tasks->total();      // 総件数
$tasks->perPage();    // 1ページあたりの件数
$tasks->currentPage(); // 現在のページ番号
$tasks->lastPage();   // 最終ページ番号

// 2ページ目を取得
$tasks = Task::paginate(10, ['*'], 'page', 2);
8. 統計情報の取得
// 田中太郎のタスク統計
$tanaka = User::where('email', 'tanaka@example.com')->first();

// ステータス別カウント
$pendingCount = $tanaka->tasks()->where('status', 'pending')->count();
$inProgressCount = $tanaka->tasks()->where('status', 'in_progress')->count();
$completedCount = $tanaka->tasks()->where('status', 'completed')->count();

echo "未着手: {$pendingCount}\n";
echo "進行中: {$inProgressCount}\n";
echo "完了: {$completedCount}\n";

// 期限切れタスク数
$overdueCount = $tanaka->tasks()
    ->where('due_date', '<', now())
    ->where('status', '!=', 'completed')
    ->count();

echo "期限切れ: {$overdueCount}\n";

✅ Tinkerでの動作確認が完了しました!

まとめ

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)でアクセス制御
  • Policyで細かい権限チェック
  • 自分のタスクのみ編集可能
  • 管理者は全タスク閲覧・削除可能

6. 検索・フィルタリング

  • ステータス・優先度でフィルタ
  • キーワード検索(タイトル・説明文)
  • 期限順・作成日順でソート
  • 完了タスクは下に表示

7. ページネーション

  • paginate(10)で1ページ10件
  • withQueryString()で検索条件を保持
  • ページリンクの自動生成

8. シーダーとTinker

  • シーダーで初期データ作成
  • Tinkerでデータ操作・確認
  • リレーション・権限チェックのテスト

学んだLaravelの機能一覧

基礎

  • ルーティング
  • コントローラー
  • ビュー(Blade)
  • マイグレーション
  • モデル

データベース

  • Eloquent ORM
  • リレーション
  • クエリビルダー
  • シーダー
  • Tinker

認証・権限

  • ユーザー認証
  • ミドルウェア
  • ロール管理
  • Policy
  • Gate

応用

  • ページネーション
  • 検索・フィルタリング
  • バリデーション
  • フラッシュメッセージ
  • リダイレクト

次のステップ

TODOアプリの基本が完成しました!さらに機能を追加してみましょう:

  • カテゴリ機能: タスクをカテゴリ分け(仕事・プライベートなど)
  • タグ機能: 多対多リレーションでタグ管理
  • コメント機能: タスクにコメントを追加
  • ファイル添付: 画像アップロード機能の応用
  • 通知機能: 期限が近いタスクをメール通知
  • API化: RESTful APIでSPA対応
  • テスト: PHPUnitでテストコード作成

🎉 総合実践TODOアプリが完成しました!

これまで学んだ全ての知識を使って、実践的なアプリケーションを作成できました。
認証、CRUD、権限管理、リレーション、ページネーション、検索など、実務で必要な機能を全て実装しました。
この知識を活かして、さらに複雑なアプリケーションにチャレンジしてみましょう!