Logo

Aperçu de l'interface :

Aperçu

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

LogicielVersionLien
PHP8.2+php.net
Composer2.xgetcomposer.org
MySQL5.7+Via XAMPP
VS CodeStablecode.visualstudio.com
XAMPP8.xapachefriends.org

Vérifier :

php -v
composer -v
Exemple php -v et composer -v
Si non reconnu → ajoutez au PATH (dossier XAMPP)
NEXT → Étape 2 : Créer le projet

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

NEXT → Étape 3 : Ouvrir dans VS Code

3. Ouvrir le projet dans VS Code

code .
Si code non reconnu → ouvrir le dossier manuellement
NEXT → Étape 4 : Configurer la base

4. Configurer la base de données

.env → DB_DATABASE=todolist_db DB_USERNAME=root DB_PASSWORD=

php artisan config:clear
NEXT → Étape 5 : Schéma MLD/MCD

5. Schéma de la base (MLD/MCD)

┌─────────────────────┐ ┌─────────────────────┐ │ CATEGORY │ │ TASK │ ├─────────────────────┤ ├─────────────────────┤ │ id (PK) │ 1 N │ id (PK) │ │ name │─────────│ category_id (FK) │ │ created_at │ │ title │ │ updated_at │ │ done (boolean) │ └─────────────────────┘ │ due_date (nullable) │ │ created_at │ │ updated_at │ └─────────────────────┘

Relation : 1 Category → N Tasks (FK category_id + onDelete('cascade'))

NEXT → Étape 6 : Migrations

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
Créer tasks APRÈS categories (ordre des migrations).
NEXT → Étape 7 : Models

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);
    }
}
NEXT → Étape 8 : Controllers

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 !');
    }
}
Checkbox : utiliser $request->boolean('done')
NEXT → Étape 9 : Routes

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

NEXT → Étape 10 : Vues Blade

10-12. Vues Blade (Layout, Categories, Tasks)

Récapitulatif : Model, Migration, Controller, Vue

ÉlémentOù ?C'est quoi ?
Modelapp/Models/Représente une table. $fillable = colonnes modifiables. Relations hasMany, belongsTo.
Migrationdatabase/migrations/Fichier qui crée les tables. Schema::create() définit les colonnes.
Controllerapp/Http/Controllers/Reçoit la requête, utilise le Model, retourne une Vue. 7 méthodes CRUD.
Vue Bladeresources/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
NEXT → Résumé & Debug

13. Résumé des commandes

#Commande
1composer create-project laravel/laravel . "^12.0"
2code .
3php artisan config:clear
4-5make:migration create_categories_table + create_tasks_table
6php artisan migrate
7-8make:model Category + Task
9-10make:controller ... --resource
11-12mkdir resources\views\categories + tasks
13php artisan serve
14php artisan route:list
NEXT → Checklist debug

14. Checklist de debug

ProblèmeSolution
DB / connexion refuséeMySQL démarré ? .env correct ?
419 CSRF@csrf dans formulaires, config:clear, cache:clear
404 Routephp artisan route:list
Migration FKOrdre categories avant tasks
Variable undefinedcompact() dans le controller
Checkbox$request->boolean('done')
Paginationpaginate(10) + $tasks->links()

Fin du guide. Consultez le fichier Markdown pour tout le code détaillé.