Aperçu de l'interface :
Public : Très débutant • Objectif : CRUD ToDoList avec 2 tables (categories 1—N tasks) + Blade sans framework.
1. Prérequis et liens officiels
▼Requirements
| Logiciel | Version | Lien |
|---|---|---|
| PHP | 8.2+ | php.net |
| Composer | 2.x | getcomposer.org |
| MySQL | 5.7+ | Via XAMPP |
| VS Code | Stable | code.visualstudio.com |
| XAMPP | 8.x | apachefriends.org |
Vérifier :
php -v
composer -v
2. Créer le dossier et le projet Laravel 12
▼1. Ouvrir CMD dans un dossier (ex. C:\...\laravel)
2. XAMPP + base todolist_db dans phpMyAdmin
3. Exécuter :
composer create-project laravel/laravel todolist
cd todolist
php artisan serve
→ http://localhost:8000
3. Ouvrir le projet dans VS Code
▼code .
code non reconnu → ouvrir le dossier manuellement4. Configurer la base de données
▼.env → DB_DATABASE=todolist_db DB_USERNAME=root DB_PASSWORD=
php artisan config:clear
5. Schéma de la base (MLD/MCD)
▼Relation : 1 Category → N Tasks (FK category_id + onDelete('cascade'))
6. Migrations
▼C'est quoi une migration ? Un fichier PHP qui décrit la structure d'une table (colonnes, types). Laravel l'exécute pour créer les tables dans MySQL.
Comment ça marche dans le projet ? Les migrations sont dans database/migrations/. Chaque fichier = 1 table. La commande php artisan migrate exécute tout et crée les tables.
php artisan make:migration create_categories_table
php artisan make:migration create_tasks_table
categories - up()
public function up(): void
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
}
tasks - up()
public function up(): void
{
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->foreignId('category_id')->constrained('categories')->onDelete('cascade');
$table->string('title');
$table->boolean('done')->default(false);
$table->date('due_date')->nullable();
$table->timestamps();
});
}
php artisan migrate
7. Models avec relations
▼php artisan make:model Category
php artisan make:model Task
app/Models/Category.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Category extends Model
{
protected $fillable = ['name'];
public function tasks(): HasMany { return $this->hasMany(Task::class); }
}
app/Models/Task.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Task extends Model
{
protected $fillable = ['category_id', 'title', 'done', 'due_date'];
protected $casts = [
'done' => 'boolean',
'due_date' => 'date',
];
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
}
8. Controllers CRUD
▼php artisan make:controller CategoryController --resource
php artisan make:controller TaskController --resource
Pourquoi --resource ? Génère automatiquement les 7 méthodes CRUD (index, create, store, show, edit, update, destroy).
Commande alternative : # php artisan make:controller CategoryController → controller vide
app/Http/Controllers/CategoryController.php
<?php
namespace App\Http\Controllers;
use App\Models\Category;
use Illuminate\Http\Request;
class CategoryController extends Controller
{
public function index()
{
$categories = Category::withCount('tasks')->orderBy('created_at', 'desc')->get();
return view('categories.index', compact('categories'));
}
public function create() { return view('categories.create'); }
public function store(Request $request)
{
$request->validate(['name' => 'required|string|max:255']);
Category::create($request->only('name'));
return redirect()->route('categories.index')->with('success', 'Catégorie créée !');
}
public function show(Category $category)
{
$category->load('tasks');
return view('categories.show', compact('category'));
}
public function edit(Category $category)
{
return view('categories.edit', compact('category'));
}
public function update(Request $request, Category $category)
{
$request->validate(['name' => 'required|string|max:255']);
$category->update($request->only('name'));
return redirect()->route('categories.index')->with('success', 'Catégorie modifiée !');
}
public function destroy(Category $category)
{
$category->delete();
return redirect()->route('categories.index')->with('success', 'Catégorie supprimée !');
}
}
app/Http/Controllers/TaskController.php
<?php
namespace App\Http\Controllers;
use App\Models\Task;
use App\Models\Category;
use Illuminate\Http\Request;
class TaskController extends Controller
{
public function index()
{
$tasks = Task::with('category')->orderBy('created_at', 'desc')->paginate(10);
return view('tasks.index', compact('tasks'));
}
public function create(Request $request)
{
$categories = Category::orderBy('name')->get();
$categoryId = $request->query('category');
return view('tasks.create', compact('categories', 'categoryId'));
}
public function store(Request $request)
{
$request->validate([
'category_id' => 'required|exists:categories,id',
'title' => 'required|string|max:255',
'done' => 'boolean',
'due_date' => 'nullable|date',
]);
$data = $request->only('category_id', 'title', 'due_date');
$data['done'] = $request->boolean('done');
Task::create($data);
return redirect()->route('tasks.index')->with('success', 'Tâche créée !');
}
public function show(Task $task)
{
$task->load('category');
return view('tasks.show', compact('task'));
}
public function edit(Task $task)
{
$categories = Category::orderBy('name')->get();
return view('tasks.edit', compact('task', 'categories'));
}
public function update(Request $request, Task $task)
{
$request->validate([
'category_id' => 'required|exists:categories,id',
'title' => 'required|string|max:255',
'done' => 'boolean',
'due_date' => 'nullable|date',
]);
$data = $request->only('category_id', 'title', 'due_date');
$data['done'] = $request->boolean('done');
$task->update($data);
return redirect()->route('tasks.index')->with('success', 'Tâche modifiée !');
}
public function destroy(Task $task)
{
$task->delete();
return redirect()->route('tasks.index')->with('success', 'Tâche supprimée !');
}
}
9. Routes
▼Fichier routes/web.php :
<?php
use App\Http\Controllers\CategoryController;
use App\Http\Controllers\TaskController;
use Illuminate\Support\Facades\Route;
Route::get('/', fn() => redirect()->route('categories.index'));
Route::resource('categories', CategoryController::class);
Route::resource('tasks', TaskController::class);
Vérifier : php artisan route:list
10-12. Vues Blade (Layout, Categories, Tasks)
▼Récapitulatif : Model, Migration, Controller, Vue
| Élément | Où ? | C'est quoi ? |
|---|---|---|
| Model | app/Models/ | Représente une table. $fillable = colonnes modifiables. Relations hasMany, belongsTo. |
| Migration | database/migrations/ | Fichier qui crée les tables. Schema::create() définit les colonnes. |
| Controller | app/Http/Controllers/ | Reçoit la requête, utilise le Model, retourne une Vue. 7 méthodes CRUD. |
| Vue Blade | resources/views/ | Template HTML avec {{ }} et @if. Hérite du layout via @extends. |
Flux : Route → Controller → Model (DB) → Vue → HTML
Migrations : categories = id, name, timestamps. tasks = id, category_id (FK), title, done, due_date, timestamps.
Models : Category = $fillable ['name'], tasks() hasMany. Task = $fillable ['category_id','title','done','due_date'], $casts, category() belongsTo.
Controllers : index→get+view | create→view | store→validate+create+redirect | show→load+view | edit→view | update→validate+update+redirect | destroy→delete+redirect.
Structure des vues
resources/views/
├── layouts/
│ └── app.blade.php (layout commun : header, CSS, @yield)
├── categories/
│ ├── index.blade.php (liste)
│ ├── create.blade.php (form création)
│ ├── edit.blade.php (form modification)
│ └── show.blade.php (détail + tâches)
└── tasks/
├── index.blade.php (liste + pagination)
├── create.blade.php (form création)
├── edit.blade.php (form modification)
└── show.blade.php (détail)
mkdir resources\views\layouts
mkdir resources\views\categories
mkdir resources\views\tasks
layouts/app.blade.php
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>@yield('title', 'ToDoList') - Laravel 12</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Segoe UI', sans-serif; background: #f0f4f8; color: #333; line-height: 1.6; }
.container { max-width: 900px; margin: 0 auto; padding: 20px; }
header { background: linear-gradient(135deg, #4f46e5, #7c3aed); color: white; padding: 20px 0; margin-bottom: 30px; }
header h1 { font-size: 1.8rem; }
header a { color: white; text-decoration: none; margin-right: 20px; }
.alert { padding: 12px 16px; border-radius: 8px; margin-bottom: 20px; }
.alert-success { background: #d1fae5; color: #065f46; }
.alert-error { background: #fee2e2; color: #991b1b; }
.card { background: white; border-radius: 12px; padding: 24px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,.08); }
.btn { display: inline-block; padding: 10px 20px; border-radius: 8px; text-decoration: none; font-weight: 600; cursor: pointer; border: none; font-size: 14px; }
.btn-primary { background: #4f46e5; color: white; }
.btn-success { background: #059669; color: white; }
.btn-danger { background: #dc2626; color: white; }
.btn-secondary { background: #6b7280; color: white; }
.btn-sm { padding: 6px 12px; font-size: 12px; }
label { display: block; margin-bottom: 6px; font-weight: 600; }
input[type="text"], input[type="date"], select { width: 100%; padding: 10px 12px; border: 2px solid #e5e7eb; border-radius: 8px; margin-bottom: 16px; }
.mb-3 { margin-bottom: 16px; }
.flex { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.pagination { display: flex; gap: 8px; margin-top: 20px; }
.pagination a, .pagination span { padding: 8px 14px; background: #e5e7eb; border-radius: 6px; text-decoration: none; color: #374151; }
.pagination .active { background: #4f46e5; color: white; }
</style>
</head>
<body>
<header>
<div class="container">
<h1>ToDoList Laravel 12</h1>
<a href="{{ route('categories.index') }}">Catégories</a>
<a href="{{ route('tasks.index') }}">Tâches</a>
</div>
</header>
<main class="container">
@if(session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@endif
@if($errors->any())
<div class="alert alert-error">
<ul>
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
@yield('content')
</main>
</body>
</html>
categories/index.blade.php
@extends('layouts.app')
@section('title', 'Catégories')
@section('content')
<div class="card">
<h2>Mes catégories</h2>
<p style="margin: 16px 0">
<a href="{{ route('categories.create') }}" class="btn btn-primary">+ Nouvelle catégorie</a>
</p>
@forelse($categories as $category)
<div style="padding: 16px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center">
<div>
<a href="{{ route('categories.show', $category) }}" style="font-weight: 600; color: #4f46e5; text-decoration: none">{{ $category->name }}</a>
<span style="color: #6b7280; font-size: 14px">({{ $category->tasks_count }} tâches)</span>
</div>
<div class="flex">
<a href="{{ route('categories.edit', $category) }}" class="btn btn-secondary btn-sm">Modifier</a>
<form action="{{ route('categories.destroy', $category) }}" method="POST" style="display: inline" onsubmit="return confirm('Supprimer ?')">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger btn-sm">Supprimer</button>
</form>
</div>
</div>
@empty
<p style="padding: 20px; color: #6b7280">Aucune catégorie.</p>
@endforelse
</div>
@endsection
categories/create.blade.php
@extends('layouts.app')
@section('title', 'Nouvelle catégorie')
@section('content')
<div class="card">
<h2>Nouvelle catégorie</h2>
<form action="{{ route('categories.store') }}" method="POST">
@csrf
<div class="mb-3">
<label for="name">Nom *</label>
<input type="text" name="name" id="name" value="{{ old('name') }}" required>
</div>
<div class="flex">
<button type="submit" class="btn btn-success">Créer</button>
<a href="{{ route('categories.index') }}" class="btn btn-secondary">Annuler</a>
</div>
</form>
</div>
@endsection
categories/edit.blade.php
@extends('layouts.app')
@section('title', 'Modifier la catégorie')
@section('content')
<div class="card">
<h2>Modifier la catégorie</h2>
<form action="{{ route('categories.update', $category) }}" method="POST">
@csrf
@method('PUT')
<div class="mb-3">
<label for="name">Nom *</label>
<input type="text" name="name" id="name" value="{{ old('name', $category->name) }}" required>
</div>
<div class="flex">
<button type="submit" class="btn btn-success">Enregistrer</button>
<a href="{{ route('categories.index') }}" class="btn btn-secondary">Annuler</a>
</div>
</form>
</div>
@endsection
categories/show.blade.php
@extends('layouts.app')
@section('title', $category->name)
@section('content')
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px">
<h2>{{ $category->name }}</h2>
<div class="flex">
<a href="{{ route('tasks.create', ['category' => $category->id]) }}" class="btn btn-primary btn-sm">+ Tâche</a>
<a href="{{ route('categories.edit', $category) }}" class="btn btn-secondary btn-sm">Modifier</a>
<form action="{{ route('categories.destroy', $category) }}" method="POST" style="display: inline" onsubmit="return confirm('Supprimer ?')">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger btn-sm">Supprimer</button>
</form>
</div>
</div>
<h3 style="margin-bottom: 12px">Tâches</h3>
@forelse($category->tasks as $task)
<div style="padding: 12px 16px; border: 1px solid #e5e7eb; border-radius: 8px; margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center">
<div>
<input type="checkbox" disabled {{ $task->done ? 'checked' : '' }}>
<a href="{{ route('tasks.show', $task) }}" style="text-decoration: none; color: inherit">{{ $task->title }}</a>
@if($task->done)
<span style="color: #059669">✓ Fait</span>
@endif
@if($task->due_date)
<span style="color: #6b7280; font-size: 12px">({{ $task->due_date->format('d/m/Y') }})</span>
@endif
</div>
<a href="{{ route('tasks.edit', $task) }}" class="btn btn-secondary btn-sm">Modifier</a>
</div>
@empty
<p style="color: #6b7280">Aucune tâche.</p>
@endforelse
</div>
<p><a href="{{ route('categories.index') }}" class="btn btn-secondary">← Retour</a></p>
@endsection
tasks/index.blade.php
@extends('layouts.app')
@section('title', 'Tâches')
@section('content')
<div class="card">
<h2>Toutes les tâches</h2>
<p style="margin: 16px 0">
<a href="{{ route('tasks.create') }}" class="btn btn-primary">+ Nouvelle tâche</a>
</p>
@forelse($tasks as $task)
<div style="padding: 16px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center">
<div>
<input type="checkbox" disabled {{ $task->done ? 'checked' : '' }}>
<a href="{{ route('tasks.show', $task) }}" style="font-weight: 600; color: #4f46e5; text-decoration: none">{{ $task->title }}</a>
<span style="color: #6b7280; font-size: 14px">— {{ $task->category->name }}</span>
@if($task->done)
<span style="color: #059669">✓</span>
@endif
@if($task->due_date)
<span style="font-size: 12px">({{ $task->due_date->format('d/m/Y') }})</span>
@endif
</div>
<div class="flex">
<a href="{{ route('tasks.edit', $task) }}" class="btn btn-secondary btn-sm">Modifier</a>
<form action="{{ route('tasks.destroy', $task) }}" method="POST" style="display: inline" onsubmit="return confirm('Supprimer ?')">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger btn-sm">Supprimer</button>
</form>
</div>
</div>
@empty
<p style="padding: 20px; color: #6b7280">Aucune tâche.</p>
@endforelse
<div class="pagination" style="margin-top: 20px">{{ $tasks->links() }}</div>
</div>
@endsection
tasks/create.blade.php
@extends('layouts.app')
@section('title', 'Nouvelle tâche')
@section('content')
<div class="card">
<h2>Nouvelle tâche</h2>
<form action="{{ route('tasks.store') }}" method="POST">
@csrf
<div class="mb-3">
<label for="category_id">Catégorie *</label>
<select name="category_id" id="category_id" required>
<option value="">-- Choisir --</option>
@foreach($categories as $cat)
<option value="{{ $cat->id }}" {{ old('category_id', $categoryId) == $cat->id ? 'selected' : '' }}>{{ $cat->name }}</option>
@endforeach
</select>
</div>
<div class="mb-3">
<label for="title">Titre *</label>
<input type="text" name="title" id="title" value="{{ old('title') }}" required>
</div>
<div class="mb-3">
<label><input type="checkbox" name="done" value="1" {{ old('done') ? 'checked' : '' }}> Marquer comme faite</label>
</div>
<div class="mb-3">
<label for="due_date">Date échéance (optionnel)</label>
<input type="date" name="due_date" id="due_date" value="{{ old('due_date') }}">
</div>
<div class="flex">
<button type="submit" class="btn btn-success">Créer</button>
<a href="{{ route('tasks.index') }}" class="btn btn-secondary">Annuler</a>
</div>
</form>
</div>
@endsection
tasks/edit.blade.php
@extends('layouts.app')
@section('title', 'Modifier la tâche')
@section('content')
<div class="card">
<h2>Modifier la tâche</h2>
<form action="{{ route('tasks.update', $task) }}" method="POST">
@csrf
@method('PUT')
<div class="mb-3">
<label for="category_id">Catégorie *</label>
<select name="category_id" id="category_id" required>
@foreach($categories as $cat)
<option value="{{ $cat->id }}" {{ old('category_id', $task->category_id) == $cat->id ? 'selected' : '' }}>{{ $cat->name }}</option>
@endforeach
</select>
</div>
<div class="mb-3">
<label for="title">Titre *</label>
<input type="text" name="title" id="title" value="{{ old('title', $task->title) }}" required>
</div>
<div class="mb-3">
<label><input type="checkbox" name="done" value="1" {{ old('done', $task->done) ? 'checked' : '' }}> Marquer comme faite</label>
</div>
<div class="mb-3">
<label for="due_date">Date échéance (optionnel)</label>
<input type="date" name="due_date" id="due_date" value="{{ old('due_date', $task->due_date?->format('Y-m-d')) }}">
</div>
<div class="flex">
<button type="submit" class="btn btn-success">Enregistrer</button>
<a href="{{ route('tasks.index') }}" class="btn btn-secondary">Annuler</a>
</div>
</form>
</div>
@endsection
tasks/show.blade.php
@extends('layouts.app')
@section('title', $task->title)
@section('content')
<div class="card">
<h2>{{ $task->title }}</h2>
<p><strong>Catégorie :</strong> <a href="{{ route('categories.show', $task->category) }}">{{ $task->category->name }}</a></p>
<p><strong>Statut :</strong> {{ $task->done ? '✓ Fait' : 'Non fait' }}</p>
@if($task->due_date)
<p><strong>Échéance :</strong> {{ $task->due_date->format('d/m/Y') }}</p>
@endif
<div class="flex" style="margin-top: 20px">
<a href="{{ route('tasks.edit', $task) }}" class="btn btn-primary">Modifier</a>
<form action="{{ route('tasks.destroy', $task) }}" method="POST" style="display: inline" onsubmit="return confirm('Supprimer ?')">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger">Supprimer</button>
</form>
<a href="{{ route('tasks.index') }}" class="btn btn-secondary">← Retour</a>
</div>
</div>
@endsection
13. Résumé des commandes
▼| # | Commande |
|---|---|
| 1 | composer create-project laravel/laravel . "^12.0" |
| 2 | code . |
| 3 | php artisan config:clear |
| 4-5 | make:migration create_categories_table + create_tasks_table |
| 6 | php artisan migrate |
| 7-8 | make:model Category + Task |
| 9-10 | make:controller ... --resource |
| 11-12 | mkdir resources\views\categories + tasks |
| 13 | php artisan serve |
| 14 | php artisan route:list |
14. Checklist de debug
▼| Problème | Solution |
|---|---|
| DB / connexion refusée | MySQL démarré ? .env correct ? |
| 419 CSRF | @csrf dans formulaires, config:clear, cache:clear |
| 404 Route | php artisan route:list |
| Migration FK | Ordre categories avant tasks |
| Variable undefined | compact() dans le controller |
| Checkbox | $request->boolean('done') |
| Pagination | paginate(10) + $tasks->links() |
Fin du guide. Consultez le fichier Markdown pour tout le code détaillé.