# [VERT] EXERCICE 1 : LAMP STACK - SITE E-COMMERCE SIMPLE

## [LISTE] ÉNONCÉ

### Contexte professionnel

Tu es développeur web dans une PME sénégalaise qui vend de l'artisanat local. Le patron veut moderniser son entreprise en créant une **boutique en ligne simple** pour vendre ses produits.

**Exigences du client :**
- Catalogue de produits avec images
- Panier d'achat
- Système de commandes
- Interface d'administration pour gérer les produits
- Base de données pour stocker tout ça

**Budget :** Limité -> Solution open-source complète

**Stack choisie :** LAMP (le standard de l'industrie depuis 20 ans)

---

## [OBJECTIF] POURQUOI LAMP ?

### [OK] Avantages

**1. Maturité et stabilité**
- Technologie éprouvée depuis 1998
- Millions de sites en production (WordPress, Joomla, etc.)
- Documentation abondante
- Communauté immense

**2. Facilité d'apprentissage**
- PHP : syntaxe simple, courbe d'apprentissage douce
- MySQL : SQL standard, interface graphique (phpMyAdmin)
- Apache : configuration claire et lisible

**3. Hébergement universel**
- Disponible chez 99% des hébergeurs
- Souvent inclus gratuitement
- Coût très faible (2-5€/mois)

**4. Écosystème riche**
- CMS prêts à l'emploi (WordPress, Drupal, PrestaShop)
- Frameworks matures (Laravel, Symfony, CodeIgniter)
- Millions de plugins et extensions

**5. Performance correcte**
- Suffisant pour 90% des sites web
- Optimisable avec cache (Varnish, Redis)
- Scaling vertical facile

### [X] Inconvénients

**1. Performance moyenne**
- PHP interprété (plus lent que code compilé)
- Apache gourmand en mémoire
- Moins performant que Nginx pour fichiers statiques

**2. Scalabilité limitée**
- Difficile de gérer des millions de requêtes/seconde
- Scaling horizontal complexe
- Nécessite load balancer pour haute disponibilité

**3. Technologie "vieillissante"**
- Architecture monolithique
- Moins adapté aux microservices
- Pas le meilleur choix pour du temps réel

---

## [GRAPHIQUE] QUAND UTILISER LAMP ?

### [OK] Cas d'usage idéaux

**1. Sites web classiques**
- Sites vitrine
- Blogs
- Portfolios
- Sites d'entreprise

**2. E-commerce petite/moyenne taille**
- Boutiques en ligne < 10 000 produits
- < 1000 commandes/jour
- Traffic < 100 000 visiteurs/mois

**3. Applications CRUD traditionnelles**
- Gestion de clients
- CRM simple
- Systèmes de réservation
- Forums, wikis

**4. Prototypes et MVP**
- Développement rapide
- Validation de concept
- Budget limité

**5. CMS et frameworks existants**
- WordPress (blogs, sites)
- Magento, PrestaShop (e-commerce)
- Moodle (e-learning)

### [X] Éviter LAMP pour

**1. Applications temps réel**
- Chat en direct
- Notifications push
- Jeux en ligne
- Collaboration temps réel

**2. Très haute charge**
- > 1 million requêtes/jour
- Millions d'utilisateurs simultanés
- Big Data

**3. APIs REST modernes**
- Microservices
- Architecture découplée
- -> Préférer MEAN ou Node.js pur

**4. Applications JavaScript-heavy**
- Single Page Applications (React, Vue)
- -> Backend et frontend découplés
- -> Préférer API REST séparée

---

## [CONSTRUCTION] ARCHITECTURE DU PROJET

### Structure globale

```
┌─────────────────────────────────────────────────────┐
│                  NAVIGATEUR                          │
│  (Client - HTML/CSS/JavaScript)                      │
└────────────┬────────────────────────────────────────┘
             │ HTTP/HTTPS
             v
┌─────────────────────────────────────────────────────┐
│                   APACHE                             │
│  • Serveur web                                       │
│  • Gestion requêtes HTTP                             │
│  • Routage vers PHP                                  │
└────────────┬────────────────────────────────────────┘
             │ FastCGI
             v
┌─────────────────────────────────────────────────────┐
│                   PHP-FPM                            │
│  • Exécution code PHP                                │
│  • Logique métier                                    │
│  • Connexion base de données                         │
└────────────┬────────────────────────────────────────┘
             │ PDO/MySQLi
             v
┌─────────────────────────────────────────────────────┐
│                   MySQL                              │
│  • Base de données relationnelle                     │
│  • Stockage produits, commandes, utilisateurs        │
└─────────────────────────────────────────────────────┘
```

### Flux d'une requête

```
1. Client demande : http://shop.local/product.php?id=5
   v
2. Apache reçoit la requête
   v
3. Apache voit extension .php -> Envoie à PHP-FPM
   v
4. PHP exécute product.php :
   - Se connecte à MySQL via PDO
   - Requête : SELECT * FROM products WHERE id = 5
   - Récupère les données
   - Génère HTML avec les infos produit
   v
5. PHP renvoie HTML à Apache
   v
6. Apache renvoie HTML au client
   v
7. Navigateur affiche la page
```

---

## [CODE] INSTALLATION COMPLÈTE

### ÉTAPE 1 : Préparer le système

```bash
# Mise à jour des paquets
sudo apt update && sudo apt upgrade -y

# Installer les outils de base
sudo apt install -y curl wget git vim net-tools
```

---

### ÉTAPE 2 : Installer Apache 2.4

```bash
# Installation
sudo apt install apache2 -y

# Vérifier le démarrage
sudo systemctl status apache2
```

**Résultat attendu :**

```
[BLACK_CIRCLE] apache2.service - The Apache HTTP Server
     Loaded: loaded (/lib/systemd/system/apache2.service; enabled)
     Active: active (running)
```

**Tester dans le navigateur :**

```
http://localhost
```

**Page par défaut d'Apache doit s'afficher.**

---

### ÉTAPE 3 : Installer MySQL 8.0

```bash
# Installation
sudo apt install mysql-server -y

# Démarrer MySQL
sudo systemctl start mysql
sudo systemctl enable mysql

# Sécuriser l'installation
sudo mysql_secure_installation
```

**Répondre aux questions :**

```
Validate password component? Y
Password level: 2 (Strong)
New root password: MySecurePass123!
Remove anonymous users? Y
Disallow root login remotely? Y
Remove test database? Y
Reload privilege tables? Y
```

**Tester la connexion :**

```bash
sudo mysql -u root -p
```

**Entrer le mot de passe -> Prompt MySQL doit apparaître.**

```sql
SHOW DATABASES;
EXIT;
```

---

### ÉTAPE 4 : Installer PHP 8.1 avec extensions

```bash
# Installation PHP et extensions nécessaires
sudo apt install -y php8.1 php8.1-fpm php8.1-mysql php8.1-mbstring \
  php8.1-xml php8.1-curl php8.1-gd php8.1-zip php8.1-intl

# Vérifier la version
php -v
```

**Résultat :**

```
PHP 8.1.x (cli) (built: ...)
```

---

### ÉTAPE 5 : Configurer Apache pour PHP-FPM

```bash
# Activer les modules nécessaires
sudo a2enmod proxy_fcgi setenvif
sudo a2enconf php8.1-fpm

# Redémarrer Apache
sudo systemctl restart apache2
```

**Tester PHP :**

```bash
# Créer fichier de test
echo "<?php phpinfo(); ?>" | sudo tee /var/www/html/info.php
```

**Accéder à :**

```
http://localhost/info.php
```

**Page phpinfo() doit s'afficher avec toutes les infos PHP.**

**[ATTENTION] Supprimer après test (sécurité) :**

```bash
sudo rm /var/www/html/info.php
```

---

## [ARCHIVE] CRÉATION DE LA BASE DE DONNÉES

### ÉTAPE 1 : Conception du schéma

Notre boutique nécessite 4 tables principales :

```
┌─────────────────┐
│   products      │  <- Catalogue produits
├─────────────────┤
│ id (PK)         │
│ name            │
│ description     │
│ price           │
│ stock           │
│ image           │
│ category        │
│ created_at      │
└─────────────────┘
        ^
        │ (foreign key)
        │
┌─────────────────┐
│   orders        │  <- Commandes clients
├─────────────────┤
│ id (PK)         │
│ customer_name   │
│ email           │
│ phone           │
│ address         │
│ total           │
│ status          │
│ created_at      │
└─────────────────┘
        ^
        │ (1-to-many)
        │
┌─────────────────┐
│  order_items    │  <- Articles d'une commande
├─────────────────┤
│ id (PK)         │
│ order_id (FK)   │
│ product_id (FK) │
│ quantity        │
│ price           │
└─────────────────┘
        │
        v
┌─────────────────┐
│   admin_users   │  <- Administrateurs
├─────────────────┤
│ id (PK)         │
│ username        │
│ password_hash   │
│ created_at      │
└─────────────────┘
```

---

### ÉTAPE 2 : Créer la base de données

```bash
# Se connecter à MySQL
sudo mysql -u root -p
```

```sql
-- ═══════════════════════════════════════════════════════════════
-- CRÉATION BASE DE DONNÉES E-COMMERCE
-- ═══════════════════════════════════════════════════════════════

-- Créer la base
CREATE DATABASE shop_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- Utiliser la base
USE shop_db;

-- ───────────────────────────────────────────────────────────────
-- TABLE : products (Produits)
-- ───────────────────────────────────────────────────────────────

CREATE TABLE products (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    description TEXT NOT NULL,
    price DECIMAL(10,2) NOT NULL,
    stock INT NOT NULL DEFAULT 0,
    image VARCHAR(255) DEFAULT 'no-image.jpg',
    category VARCHAR(50) NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    INDEX idx_category (category),
    INDEX idx_price (price)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

/*
 * EXPLICATIONS :
 * 
 * DECIMAL(10,2) :
 * - 10 chiffres au total
 * - 2 après la virgule
 * - Parfait pour prix : 99999999.99
 * - Précision garantie (pas de float qui arrondit mal)
 * 
 * INDEX idx_category :
 * - Accélère les requêtes WHERE category = '...'
 * - Essentiel pour filtres
 * 
 * INDEX idx_price :
 * - Accélère ORDER BY price
 * - Utile pour tri par prix
 */

-- ───────────────────────────────────────────────────────────────
-- TABLE : orders (Commandes)
-- ───────────────────────────────────────────────────────────────

CREATE TABLE orders (
    id INT AUTO_INCREMENT PRIMARY KEY,
    customer_name VARCHAR(100) NOT NULL,
    email VARCHAR(150) NOT NULL,
    phone VARCHAR(20) NOT NULL,
    address TEXT NOT NULL,
    total DECIMAL(10,2) NOT NULL,
    status ENUM('pending', 'confirmed', 'shipped', 'delivered', 'cancelled') 
        DEFAULT 'pending',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    
    INDEX idx_status (status),
    INDEX idx_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

/*
 * ENUM('pending', 'confirmed', ...) :
 * - Type de données limité à certaines valeurs
 * - Impossible d'insérer autre chose
 * - Plus efficace que VARCHAR
 * - Autodocumenté
 * 
 * Exemple :
 * INSERT INTO orders (status) VALUES ('paid'); -- [X] ERREUR
 * INSERT INTO orders (status) VALUES ('pending'); -- [OK] OK
 */

-- ───────────────────────────────────────────────────────────────
-- TABLE : order_items (Articles des commandes)
-- ───────────────────────────────────────────────────────────────

CREATE TABLE order_items (
    id INT AUTO_INCREMENT PRIMARY KEY,
    order_id INT NOT NULL,
    product_id INT NOT NULL,
    quantity INT NOT NULL DEFAULT 1,
    price DECIMAL(10,2) NOT NULL,
    
    FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
    FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE RESTRICT,
    
    INDEX idx_order (order_id),
    INDEX idx_product (product_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

/*
 * FOREIGN KEY avec ON DELETE :
 * 
 * ON DELETE CASCADE :
 * Si la commande est supprimée, supprimer automatiquement
 * tous ses articles
 * 
 * DELETE FROM orders WHERE id = 1;
 * -> Supprime aussi tous les order_items avec order_id = 1
 * 
 * ON DELETE RESTRICT :
 * Empêche la suppression si des enregistrements dépendants existent
 * 
 * DELETE FROM products WHERE id = 5;
 * -> [X] ERREUR si des order_items référencent product_id = 5
 * 
 * Autres options :
 * - SET NULL : Met la FK à NULL
 * - NO ACTION : Comme RESTRICT
 */

-- ───────────────────────────────────────────────────────────────
-- TABLE : admin_users (Administrateurs)
-- ───────────────────────────────────────────────────────────────

CREATE TABLE admin_users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    
    INDEX idx_username (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

/*
 * password_hash VARCHAR(255) :
 * Stocke le hash du mot de passe (jamais en clair !)
 * 
 * password_hash() génère des hashs de ~60 caractères
 * Mais 255 permet les futurs algorithmes plus longs
 * 
 * UNIQUE :
 * Garantit qu'aucun username ne peut être dupliqué
 */

-- ───────────────────────────────────────────────────────────────
-- DONNÉES DE TEST
-- ───────────────────────────────────────────────────────────────

-- Insérer produits de test
INSERT INTO products (name, description, price, stock, category, image) VALUES
('Panier Traditionnel', 'Panier tressé à la main par des artisans locaux. Idéal pour courses ou décoration.', 15000.00, 25, 'Vannerie', 'panier.jpg'),
('Statue Bois Ébène', 'Sculpture traditionnelle en bois d\'ébène représentant une scène de vie quotidienne.', 45000.00, 8, 'Sculpture', 'statue.jpg'),
('Boubou Bazin', 'Boubou traditionnel en bazin richement brodé. Taille L.', 85000.00, 12, 'Textile', 'boubou.jpg'),
('Bracelet Perles', 'Bracelet artisanal en perles de verre colorées.', 8000.00, 50, 'Bijoux', 'bracelet.jpg'),
('Tableau Sable', 'Tableau en sable coloré représentant un paysage du Sahel.', 28000.00, 15, 'Art', 'tableau.jpg'),
('Sac Cuir', 'Sac à main en cuir véritable tanné traditionnellement.', 35000.00, 20, 'Maroquinerie', 'sac.jpg'),
('Djembé Artisanal', 'Djembé traditionnel en bois de lènkè, peau de chèvre tendue.', 55000.00, 10, 'Musique', 'djembe.jpg'),
('Collier Cauris', 'Collier traditionnel en coquillages cauris et perles.', 12000.00, 30, 'Bijoux', 'collier.jpg');

-- Créer un compte admin
-- Mot de passe : admin123 (hashé avec password_hash())
INSERT INTO admin_users (username, password_hash) VALUES
('admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi');

-- Créer une commande de test
INSERT INTO orders (customer_name, email, phone, address, total, status) VALUES
('Moussa Diop', 'moussa@example.com', '+221 77 123 45 67', 'Sicap Liberté 6, Villa 123, Dakar', 115000.00, 'confirmed');

-- Ajouter articles à la commande
SET @order_id = LAST_INSERT_ID();
INSERT INTO order_items (order_id, product_id, quantity, price) VALUES
(@order_id, 1, 2, 15000.00),  -- 2 paniers
(@order_id, 3, 1, 85000.00);  -- 1 boubou

-- ═══════════════════════════════════════════════════════════════
```

**Vérifier l'insertion :**

```sql
SELECT * FROM products;
SELECT * FROM orders;
SELECT * FROM order_items;
```

**Quitter MySQL :**

```sql
EXIT;
```

---

### ÉTAPE 3 : Créer utilisateur MySQL pour l'application

```bash
sudo mysql -u root -p
```

```sql
-- Créer utilisateur dédié
CREATE USER 'shop_user'@'localhost' IDENTIFIED BY 'ShopPass123!';

-- Donner les droits sur la base shop_db
GRANT ALL PRIVILEGES ON shop_db.* TO 'shop_user'@'localhost';

-- Appliquer
FLUSH PRIVILEGES;

-- Quitter
EXIT;
```

---

## [OUTIL] DÉVELOPPEMENT DE L'APPLICATION

### Structure du projet

```
/var/www/shop/
├── config/
│   └── database.php       <- Connexion DB
├── includes/
│   ├── header.php         <- Header HTML commun
│   ├── footer.php         <- Footer HTML commun
│   └── functions.php      <- Fonctions utilitaires
├── public/
│   ├── css/
│   │   └── style.css      <- Styles CSS
│   ├── images/
│   │   └── products/      <- Images produits
│   └── js/
│       └── cart.js        <- Panier JavaScript
├── admin/
│   ├── login.php          <- Connexion admin
│   ├── dashboard.php      <- Tableau de bord
│   ├── products.php       <- Gestion produits
│   └── orders.php         <- Gestion commandes
├── index.php              <- Page d'accueil
├── product.php            <- Détail produit
├── cart.php               <- Panier
├── checkout.php           <- Validation commande
└── order-success.php      <- Confirmation
```

---

### ÉTAPE 1 : Créer la structure

```bash
# Créer les dossiers
sudo mkdir -p /var/www/shop/{config,includes,public/{css,js,images/products},admin}

# Permissions
sudo chown -R www-data:www-data /var/www/shop
sudo find /var/www/shop -type d -exec chmod 755 {} \;
sudo find /var/www/shop -type f -exec chmod 644 {} \;
```

---

### ÉTAPE 2 : Configuration base de données

```bash
sudo nano /var/www/shop/config/database.php
```

```php
<?php
/**
 * ═══════════════════════════════════════════════════════════════
 * CONFIGURATION BASE DE DONNÉES
 * ═══════════════════════════════════════════════════════════════
 */

// Paramètres de connexion
define('DB_HOST', 'localhost');
define('DB_NAME', 'shop_db');
define('DB_USER', 'shop_user');
define('DB_PASS', 'ShopPass123!');
define('DB_CHARSET', 'utf8mb4');

/**
 * Obtenir une connexion PDO à la base de données
 * 
 * @return PDO Instance de connexion
 * @throws PDOException Si la connexion échoue
 */
function getDB() {
    static $pdo = null;
    
    /*
     * static $pdo :
     * Variable statique = conserve sa valeur entre les appels
     * 
     * Premier appel : $pdo = null -> Créer connexion
     * Appels suivants : $pdo existe déjà -> Réutiliser
     * 
     * Avantage : Une seule connexion pour toute la requête
     * (Pattern Singleton)
     */
    
    if ($pdo === null) {
        try {
            $dsn = sprintf(
                "mysql:host=%s;dbname=%s;charset=%s",
                DB_HOST,
                DB_NAME,
                DB_CHARSET
            );
            
            $options = [
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                PDO::ATTR_EMULATE_PREPARES => false,
                PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4"
            ];
            
            $pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
            
        } catch (PDOException $e) {
            // En production, logger l'erreur au lieu de l'afficher
            error_log("Erreur DB : " . $e->getMessage());
            die("Erreur de connexion à la base de données.");
        }
    }
    
    return $pdo;
}

/**
 * Exécuter une requête préparée
 * 
 * @param string $sql Requête SQL avec placeholders
 * @param array $params Paramètres pour la requête
 * @return PDOStatement
 */
function query($sql, $params = []) {
    $pdo = getDB();
    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);
    return $stmt;
}

/**
 * Récupérer tous les résultats
 */
function fetchAll($sql, $params = []) {
    return query($sql, $params)->fetchAll();
}

/**
 * Récupérer un seul résultat
 */
function fetchOne($sql, $params = []) {
    return query($sql, $params)->fetch();
}

/**
 * Récupérer une seule valeur
 */
function fetchColumn($sql, $params = []) {
    return query($sql, $params)->fetchColumn();
}
?>
```

---

### ÉTAPE 3 : Fichiers communs (header/footer)

```bash
sudo nano /var/www/shop/includes/header.php
```

```php
<?php
// Démarrer la session si pas déjà démarrée
if (session_status() === PHP_SESSION_NONE) {
    session_start();
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?php echo $pageTitle ?? 'Boutique Artisanat Sénégalais'; ?></title>
    <link rel="stylesheet" href="/public/css/style.css">
</head>
<body>
    <header class="main-header">
        <div class="container">
            <div class="header-content">
                <h1 class="logo">
                    <a href="/index.php">[SHOPPING_BAGS] Artisanat Sénégal</a>
                </h1>
                <nav class="main-nav">
                    <a href="/index.php">Accueil</a>
                    <a href="/index.php#products">Produits</a>
                    <a href="/cart.php">
                        [SHOPPING_TROLLEY] Panier 
                        <?php if (isset($_SESSION['cart']) && count($_SESSION['cart']) > 0): ?>
                            <span class="cart-count"><?php echo count($_SESSION['cart']); ?></span>
                        <?php endif; ?>
                    </a>
                    <a href="/admin/login.php">Admin</a>
                </nav>
            </div>
        </div>
    </header>
    <main class="main-content">
```

```bash
sudo nano /var/www/shop/includes/footer.php
```

```php
    </main>
    <footer class="main-footer">
        <div class="container">
            <p>&copy; <?php echo date('Y'); ?> Artisanat Sénégal - Tous droits réservés</p>
            <p>Livraison gratuite à Dakar - Paiement à la livraison</p>
        </div>
    </footer>
</body>
</html>
```

---

### ÉTAPE 4 : Fonctions utilitaires

```bash
sudo nano /var/www/shop/includes/functions.php
```

```php
<?php
/**
 * ═══════════════════════════════════════════════════════════════
 * FONCTIONS UTILITAIRES
 * ═══════════════════════════════════════════════════════════════
 */

/**
 * Échapper HTML (protection XSS)
 */
function e($string) {
    return htmlspecialchars($string ?? '', ENT_QUOTES, 'UTF-8');
}

/**
 * Formater un prix en FCFA
 */
function formatPrice($price) {
    return number_format($price, 0, ',', ' ') . ' FCFA';
}

/**
 * Ajouter un produit au panier
 */
function addToCart($productId, $quantity = 1) {
    if (!isset($_SESSION['cart'])) {
        $_SESSION['cart'] = [];
    }
    
    if (isset($_SESSION['cart'][$productId])) {
        $_SESSION['cart'][$productId] += $quantity;
    } else {
        $_SESSION['cart'][$productId] = $quantity;
    }
}

/**
 * Retirer un produit du panier
 */
function removeFromCart($productId) {
    if (isset($_SESSION['cart'][$productId])) {
        unset($_SESSION['cart'][$productId]);
    }
}

/**
 * Calculer le total du panier
 */
function getCartTotal() {
    if (!isset($_SESSION['cart']) || empty($_SESSION['cart'])) {
        return 0;
    }
    
    $total = 0;
    $pdo = getDB();
    
    foreach ($_SESSION['cart'] as $productId => $quantity) {
        $stmt = $pdo->prepare("SELECT price FROM products WHERE id = ?");
        $stmt->execute([$productId]);
        $product = $stmt->fetch();
        
        if ($product) {
            $total += $product['price'] * $quantity;
        }
    }
    
    return $total;
}

/**
 * Vider le panier
 */
function clearCart() {
    $_SESSION['cart'] = [];
}

/**
 * Redirection
 */
function redirect($url) {
    header("Location: $url");
    exit();
}
?>
```

---

Je continue avec la création du CSS et des pages principales dans la prochaine section pour respecter la limite de caractères...

### ÉTAPE 5 : Styles CSS

```bash
sudo nano /var/www/shop/public/css/style.css
```

```css
/* ═══════════════════════════════════════════════════════════════
   BOUTIQUE E-COMMERCE - STYLES
   ═══════════════════════════════════════════════════════════════ */

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Segoe UI', system-ui, sans-serif;
    line-height: 1.6;
    color: #333;
    background: #f5f5f5;
}

.container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 20px;
}

/* Header */
.main-header {
    background: linear-gradient(135deg, #2c5f2d 0%, #97bc62ff 100%);
    color: white;
    padding: 1.5rem 0;
    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}

.header-content {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.logo a {
    color: white;
    text-decoration: none;
    font-size: 1.8rem;
}

.main-nav {
    display: flex;
    gap: 2rem;
}

.main-nav a {
    color: white;
    text-decoration: none;
    font-weight: 500;
    transition: opacity 0.3s;
    position: relative;
}

.main-nav a:hover {
    opacity: 0.8;
}

.cart-count {
    background: #ff4444;
    color: white;
    border-radius: 50%;
    padding: 2px 6px;
    font-size: 0.8rem;
    position: absolute;
    top: -8px;
    right: -10px;
}

/* Hero Section */
.hero {
    background: linear-gradient(rgba(44,95,45,0.8), rgba(151,188,98,0.8)),
                url('/public/images/hero-bg.jpg') center/cover;
    color: white;
    text-align: center;
    padding: 6rem 0;
}

.hero h2 {
    font-size: 3rem;
    margin-bottom: 1rem;
}

.hero p {
    font-size: 1.3rem;
    margin-bottom: 2rem;
}

.btn {
    display: inline-block;
    padding: 1rem 2rem;
    background: white;
    color: #2c5f2d;
    text-decoration: none;
    border-radius: 5px;
    font-weight: bold;
    transition: transform 0.3s;
}

.btn:hover {
    transform: translateY(-2px);
}

/* Products Grid */
.products-section {
    padding: 4rem 0;
}

.section-title {
    text-align: center;
    font-size: 2.5rem;
    color: #2c5f2d;
    margin-bottom: 3rem;
}

.products-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 2rem;
}

.product-card {
    background: white;
    border-radius: 10px;
    overflow: hidden;
    box-shadow: 0 5px 15px rgba(0,0,0,0.1);
    transition: transform 0.3s;
}

.product-card:hover {
    transform: translateY(-5px);
}

.product-image {
    width: 100%;
    height: 250px;
    object-fit: cover;
}

.product-info {
    padding: 1.5rem;
}

.product-name {
    font-size: 1.3rem;
    color: #2c5f2d;
    margin-bottom: 0.5rem;
}

.product-category {
    color: #666;
    font-size: 0.9rem;
    margin-bottom: 0.5rem;
}

.product-price {
    font-size: 1.5rem;
    font-weight: bold;
    color: #97bc62ff;
    margin: 1rem 0;
}

.product-actions {
    display: flex;
    gap: 1rem;
}

.btn-primary {
    flex: 1;
    padding: 0.8rem;
    background: #2c5f2d;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    font-size: 1rem;
    transition: background 0.3s;
}

.btn-primary:hover {
    background: #1e4020;
}

.btn-secondary {
    padding: 0.8rem 1.5rem;
    background: white;
    color: #2c5f2d;
    border: 2px solid #2c5f2d;
    border-radius: 5px;
    text-decoration: none;
    transition: all 0.3s;
}

.btn-secondary:hover {
    background: #2c5f2d;
    color: white;
}

/* Cart Page */
.cart-table {
    width: 100%;
    background: white;
    border-radius: 10px;
    overflow: hidden;
    margin: 2rem 0;
}

.cart-table th,
.cart-table td {
    padding: 1rem;
    text-align: left;
}

.cart-table th {
    background: #2c5f2d;
    color: white;
}

.cart-table tr:nth-child(even) {
    background: #f9f9f9;
}

.cart-total {
    background: white;
    padding: 2rem;
    border-radius: 10px;
    text-align: right;
    margin-top: 2rem;
}

.cart-total h3 {
    font-size: 2rem;
    color: #2c5f2d;
}

/* Forms */
.form-group {
    margin-bottom: 1.5rem;
}

.form-group label {
    display: block;
    margin-bottom: 0.5rem;
    font-weight: 600;
}

.form-group input,
.form-group textarea,
.form-group select {
    width: 100%;
    padding: 0.8rem;
    border: 2px solid #ddd;
    border-radius: 5px;
    font-size: 1rem;
}

.form-group input:focus,
.form-group textarea:focus {
    outline: none;
    border-color: #2c5f2d;
}

/* Messages */
.alert {
    padding: 1rem;
    border-radius: 5px;
    margin: 1rem 0;
}

.alert-success {
    background: #d4edda;
    color: #155724;
    border: 1px solid #c3e6cb;
}

.alert-error {
    background: #f8d7da;
    color: #721c24;
    border: 1px solid #f5c6cb;
}

/* Footer */
.main-footer {
    background: #2c5f2d;
    color: white;
    text-align: center;
    padding: 2rem 0;
    margin-top: 4rem;
}

/* Admin Styles */
.admin-header {
    background: #343a40;
    padding: 1rem 0;
}

.admin-nav a {
    color: white;
    margin-right: 2rem;
}

.table {
    width: 100%;
    background: white;
    border-collapse: collapse;
}

.table th,
.table td {
    padding: 1rem;
    border-bottom: 1px solid #ddd;
}

.table th {
    background: #f8f9fa;
    text-align: left;
}

.badge {
    padding: 0.3rem 0.6rem;
    border-radius: 3px;
    font-size: 0.85rem;
}

.badge-pending {
    background: #ffc107;
    color: #000;
}

.badge-confirmed {
    background: #28a745;
    color: white;
}

.badge-shipped {
    background: #17a2b8;
    color: white;
}
```

---

### ÉTAPE 6 : Page d'accueil avec catalogue

```bash
sudo nano /var/www/shop/index.php
```

```php
<?php
require_once 'config/database.php';
require_once 'includes/functions.php';

session_start();

$pageTitle = 'Accueil - Boutique Artisanat';

// Récupérer tous les produits
$products = fetchAll("SELECT * FROM products ORDER BY created_at DESC");

// Gérer l'ajout au panier
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_to_cart'])) {
    $productId = filter_input(INPUT_POST, 'product_id', FILTER_VALIDATE_INT);
    if ($productId) {
        addToCart($productId);
        $_SESSION['message'] = 'Produit ajouté au panier !';
        redirect('/index.php#products');
    }
}

include 'includes/header.php';
?>

<!-- Hero Section -->
<section class="hero">
    <div class="container">
        <h2>Découvrez l'Artisanat Sénégalais</h2>
        <p>Produits authentiques fabriqués par des artisans locaux</p>
        <a href="#products" class="btn">Voir les produits</a>
    </div>
</section>

<!-- Products Section -->
<section class="products-section" id="products">
    <div class="container">
        <h2 class="section-title">Nos Produits</h2>
        
        <?php if (isset($_SESSION['message'])): ?>
            <div class="alert alert-success">
                <?php 
                echo e($_SESSION['message']); 
                unset($_SESSION['message']);
                ?>
            </div>
        <?php endif; ?>
        
        <div class="products-grid">
            <?php foreach ($products as $product): ?>
                <div class="product-card">
                    <img src="/public/images/products/<?php echo e($product['image']); ?>" 
                         alt="<?php echo e($product['name']); ?>"
                         class="product-image"
                         onerror="this.src='/public/images/products/no-image.jpg'">
                    
                    <div class="product-info">
                        <div class="product-category">
                            <?php echo e($product['category']); ?>
                        </div>
                        <h3 class="product-name">
                            <?php echo e($product['name']); ?>
                        </h3>
                        <p class="product-description">
                            <?php echo e(substr($product['description'], 0, 100)); ?>...
                        </p>
                        <div class="product-price">
                            <?php echo formatPrice($product['price']); ?>
                        </div>
                        
                        <?php if ($product['stock'] > 0): ?>
                            <div class="product-actions">
                                <form method="POST" style="flex: 1;">
                                    <input type="hidden" name="product_id" value="<?php echo $product['id']; ?>">
                                    <button type="submit" name="add_to_cart" class="btn-primary">
                                        [SHOPPING_TROLLEY] Ajouter au panier
                                    </button>
                                </form>
                                <a href="/product.php?id=<?php echo $product['id']; ?>" class="btn-secondary">
                                    Voir détails
                                </a>
                            </div>
                        <?php else: ?>
                            <div class="alert alert-error">
                                Rupture de stock
                            </div>
                        <?php endif; ?>
                    </div>
                </div>
            <?php endforeach; ?>
        </div>
    </div>
</section>

<?php include 'includes/footer.php'; ?>
```

---

### ÉTAPE 7 : Page détail produit

```bash
sudo nano /var/www/shop/product.php
```

```php
<?php
require_once 'config/database.php';
require_once 'includes/functions.php';

session_start();

// Récupérer l'ID du produit
$productId = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);

if (!$productId) {
    redirect('/index.php');
}

// Récupérer le produit
$product = fetchOne("SELECT * FROM products WHERE id = ?", [$productId]);

if (!$product) {
    redirect('/index.php');
}

$pageTitle = e($product['name']) . ' - Boutique';

// Gérer l'ajout au panier
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_to_cart'])) {
    $quantity = filter_input(INPUT_POST, 'quantity', FILTER_VALIDATE_INT);
    if ($quantity && $quantity > 0 && $quantity <= $product['stock']) {
        addToCart($productId, $quantity);
        $_SESSION['message'] = "Produit ajouté au panier (quantité: $quantity)";
        redirect("/product.php?id=$productId");
    }
}

include 'includes/header.php';
?>

<div class="container" style="padding: 3rem 0;">
    <?php if (isset($_SESSION['message'])): ?>
        <div class="alert alert-success">
            <?php 
            echo e($_SESSION['message']); 
            unset($_SESSION['message']);
            ?>
        </div>
    <?php endif; ?>
    
    <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 3rem; background: white; padding: 2rem; border-radius: 10px;">
        <!-- Image du produit -->
        <div>
            <img src="/public/images/products/<?php echo e($product['image']); ?>" 
                 alt="<?php echo e($product['name']); ?>"
                 style="width: 100%; border-radius: 10px;"
                 onerror="this.src='/public/images/products/no-image.jpg'">
        </div>
        
        <!-- Informations du produit -->
        <div>
            <div style="color: #666; margin-bottom: 0.5rem;">
                <?php echo e($product['category']); ?>
            </div>
            
            <h1 style="color: #2c5f2d; margin-bottom: 1rem;">
                <?php echo e($product['name']); ?>
            </h1>
            
            <div style="font-size: 2rem; color: #97bc62ff; font-weight: bold; margin: 1.5rem 0;">
                <?php echo formatPrice($product['price']); ?>
            </div>
            
            <p style="line-height: 1.8; margin: 2rem 0;">
                <?php echo nl2br(e($product['description'])); ?>
            </p>
            
            <div style="background: #f8f9fa; padding: 1rem; border-radius: 5px; margin: 1.5rem 0;">
                <strong>Stock disponible:</strong> <?php echo $product['stock']; ?> unités
            </div>
            
            <?php if ($product['stock'] > 0): ?>
                <form method="POST">
                    <div class="form-group">
                        <label>Quantité:</label>
                        <input type="number" name="quantity" value="1" 
                               min="1" max="<?php echo $product['stock']; ?>"
                               style="width: 100px;">
                    </div>
                    <button type="submit" name="add_to_cart" class="btn-primary" style="width: 100%;">
                        [SHOPPING_TROLLEY] Ajouter au panier
                    </button>
                </form>
            <?php else: ?>
                <div class="alert alert-error">
                    Ce produit est actuellement en rupture de stock.
                </div>
            <?php endif; ?>
            
            <a href="/index.php" class="btn-secondary" style="display: block; text-align: center; margin-top: 1rem;">
                <- Retour au catalogue
            </a>
        </div>
    </div>
</div>

<?php include 'includes/footer.php'; ?>
```

---

### ÉTAPE 8 : Page panier

```bash
sudo nano /var/www/shop/cart.php
```

```php
<?php
require_once 'config/database.php';
require_once 'includes/functions.php';

session_start();

$pageTitle = 'Mon Panier - Boutique';

// Gérer la suppression d'un produit
if (isset($_GET['remove'])) {
    $productId = filter_input(INPUT_GET, 'remove', FILTER_VALIDATE_INT);
    if ($productId) {
        removeFromCart($productId);
        redirect('/cart.php');
    }
}

// Gérer la mise à jour des quantités
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_cart'])) {
    foreach ($_POST['quantity'] as $productId => $quantity) {
        $productId = (int)$productId;
        $quantity = (int)$quantity;
        
        if ($quantity > 0) {
            $_SESSION['cart'][$productId] = $quantity;
        } else {
            removeFromCart($productId);
        }
    }
    redirect('/cart.php');
}

// Récupérer les produits du panier
$cartItems = [];
if (isset($_SESSION['cart']) && !empty($_SESSION['cart'])) {
    $productIds = array_keys($_SESSION['cart']);
    $placeholders = str_repeat('?,', count($productIds) - 1) . '?';
    $cartItems = fetchAll(
        "SELECT * FROM products WHERE id IN ($placeholders)",
        $productIds
    );
}

$total = getCartTotal();

include 'includes/header.php';
?>

<div class="container" style="padding: 3rem 0;">
    <h1 class="section-title">Mon Panier</h1>
    
    <?php if (empty($cartItems)): ?>
        <div style="text-align: center; padding: 4rem; background: white; border-radius: 10px;">
            <p style="font-size: 1.3rem; color: #666;">Votre panier est vide</p>
            <a href="/index.php" class="btn-primary" style="margin-top: 2rem; display: inline-block;">
                Continuer mes achats
            </a>
        </div>
    <?php else: ?>
        <form method="POST">
            <table class="cart-table">
                <thead>
                    <tr>
                        <th>Produit</th>
                        <th>Prix unitaire</th>
                        <th>Quantité</th>
                        <th>Sous-total</th>
                        <th>Action</th>
                    </tr>
                </thead>
                <tbody>
                    <?php foreach ($cartItems as $item): ?>
                        <?php $quantity = $_SESSION['cart'][$item['id']]; ?>
                        <tr>
                            <td>
                                <div style="display: flex; align-items: center; gap: 1rem;">
                                    <img src="/public/images/products/<?php echo e($item['image']); ?>" 
                                         style="width: 80px; height: 80px; object-fit: cover; border-radius: 5px;"
                                         onerror="this.src='/public/images/products/no-image.jpg'">
                                    <div>
                                        <strong><?php echo e($item['name']); ?></strong><br>
                                        <small><?php echo e($item['category']); ?></small>
                                    </div>
                                </div>
                            </td>
                            <td><?php echo formatPrice($item['price']); ?></td>
                            <td>
                                <input type="number" 
                                       name="quantity[<?php echo $item['id']; ?>]" 
                                       value="<?php echo $quantity; ?>"
                                       min="1" 
                                       max="<?php echo $item['stock']; ?>"
                                       style="width: 80px; padding: 0.5rem;">
                            </td>
                            <td>
                                <strong><?php echo formatPrice($item['price'] * $quantity); ?></strong>
                            </td>
                            <td>
                                <a href="?remove=<?php echo $item['id']; ?>" 
                                   style="color: #dc3545; text-decoration: none;"
                                   onclick="return confirm('Supprimer ce produit ?')">
                                    [SUPPRIMER] Supprimer
                                </a>
                            </td>
                        </tr>
                    <?php endforeach; ?>
                </tbody>
            </table>
            
            <div style="display: flex; justify-content: space-between; margin-top: 2rem;">
                <button type="submit" name="update_cart" class="btn-secondary">
                    Mettre à jour le panier
                </button>
                <a href="/index.php" class="btn-secondary">
                    Continuer mes achats
                </a>
            </div>
        </form>
        
        <div class="cart-total">
            <h3>Total: <?php echo formatPrice($total); ?></h3>
            <a href="/checkout.php" class="btn-primary" style="margin-top: 1rem; display: inline-block;">
                Passer la commande
            </a>
        </div>
    <?php endif; ?>
</div>

<?php include 'includes/footer.php'; ?>
```

---

Je continue avec les pages de checkout, admin et le Virtual Host Apache dans la suite...

### ÉTAPE 9 : Page de validation de commande (checkout)

```bash
sudo nano /var/www/shop/checkout.php
```

```php
<?php
require_once 'config/database.php';
require_once 'includes/functions.php';

session_start();

$pageTitle = 'Finaliser ma commande - Boutique';

// Vérifier que le panier n'est pas vide
if (!isset($_SESSION['cart']) || empty($_SESSION['cart'])) {
    redirect('/cart.php');
}

$errors = [];
$success = false;

// Traiter le formulaire
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // Récupérer et valider les données
    $customerName = trim($_POST['customer_name'] ?? '');
    $email = trim($_POST['email'] ?? '');
    $phone = trim($_POST['phone'] ?? '');
    $address = trim($_POST['address'] ?? '');
    
    // Validation
    if (empty($customerName)) {
        $errors[] = "Le nom est obligatoire";
    }
    
    if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $errors[] = "Email invalide";
    }
    
    if (empty($phone)) {
        $errors[] = "Le téléphone est obligatoire";
    }
    
    if (empty($address)) {
        $errors[] = "L'adresse de livraison est obligatoire";
    }
    
    // Si pas d'erreurs, créer la commande
    if (empty($errors)) {
        try {
            $pdo = getDB();
            $pdo->beginTransaction();
            
            // Calculer le total
            $total = getCartTotal();
            
            // Insérer la commande
            $stmt = $pdo->prepare("
                INSERT INTO orders (customer_name, email, phone, address, total, status)
                VALUES (?, ?, ?, ?, ?, 'pending')
            ");
            $stmt->execute([$customerName, $email, $phone, $address, $total]);
            $orderId = $pdo->lastInsertId();
            
            // Insérer les articles de la commande
            $stmtItem = $pdo->prepare("
                INSERT INTO order_items (order_id, product_id, quantity, price)
                VALUES (?, ?, ?, ?)
            ");
            
            $stmtUpdateStock = $pdo->prepare("
                UPDATE products SET stock = stock - ? WHERE id = ?
            ");
            
            foreach ($_SESSION['cart'] as $productId => $quantity) {
                // Récupérer le prix actuel
                $product = fetchOne("SELECT price, stock FROM products WHERE id = ?", [$productId]);
                
                // Vérifier le stock
                if ($product['stock'] < $quantity) {
                    throw new Exception("Stock insuffisant pour le produit #$productId");
                }
                
                // Insérer l'article
                $stmtItem->execute([$orderId, $productId, $quantity, $product['price']]);
                
                // Décrémenter le stock
                $stmtUpdateStock->execute([$quantity, $productId]);
            }
            
            $pdo->commit();
            
            // Vider le panier
            clearCart();
            
            // Rediriger vers la page de succès
            redirect("/order-success.php?order=$orderId");
            
        } catch (Exception $e) {
            $pdo->rollBack();
            $errors[] = "Erreur lors de la création de la commande : " . $e->getMessage();
        }
    }
}

// Récupérer les produits du panier pour affichage
$cartItems = [];
if (isset($_SESSION['cart']) && !empty($_SESSION['cart'])) {
    $productIds = array_keys($_SESSION['cart']);
    $placeholders = str_repeat('?,', count($productIds) - 1) . '?';
    $cartItems = fetchAll(
        "SELECT * FROM products WHERE id IN ($placeholders)",
        $productIds
    );
}

$total = getCartTotal();

include 'includes/header.php';
?>

<div class="container" style="padding: 3rem 0;">
    <h1 class="section-title">Finaliser ma commande</h1>
    
    <div style="display: grid; grid-template-columns: 2fr 1fr; gap: 2rem;">
        <!-- Formulaire de commande -->
        <div style="background: white; padding: 2rem; border-radius: 10px;">
            <h2 style="color: #2c5f2d; margin-bottom: 1.5rem;">Informations de livraison</h2>
            
            <?php if (!empty($errors)): ?>
                <div class="alert alert-error">
                    <?php foreach ($errors as $error): ?>
                        <p><?php echo e($error); ?></p>
                    <?php endforeach; ?>
                </div>
            <?php endif; ?>
            
            <form method="POST">
                <div class="form-group">
                    <label>Nom complet *</label>
                    <input type="text" name="customer_name" 
                           value="<?php echo e($_POST['customer_name'] ?? ''); ?>" 
                           required>
                </div>
                
                <div class="form-group">
                    <label>Email *</label>
                    <input type="email" name="email" 
                           value="<?php echo e($_POST['email'] ?? ''); ?>" 
                           required>
                </div>
                
                <div class="form-group">
                    <label>Téléphone *</label>
                    <input type="tel" name="phone" 
                           value="<?php echo e($_POST['phone'] ?? ''); ?>" 
                           placeholder="+221 77 123 45 67"
                           required>
                </div>
                
                <div class="form-group">
                    <label>Adresse de livraison *</label>
                    <textarea name="address" rows="4" required><?php echo e($_POST['address'] ?? ''); ?></textarea>
                    <small>Indiquez le quartier, la rue, villa/immeuble, étage si nécessaire</small>
                </div>
                
                <div style="background: #f8f9fa; padding: 1rem; border-radius: 5px; margin: 1rem 0;">
                    <p><strong>Mode de paiement :</strong> Paiement à la livraison (espèces)</p>
                    <p><strong>Frais de livraison :</strong> Gratuit à Dakar</p>
                </div>
                
                <button type="submit" class="btn-primary" style="width: 100%;">
                    Valider ma commande
                </button>
            </form>
        </div>
        
        <!-- Récapitulatif commande -->
        <div>
            <div style="background: white; padding: 2rem; border-radius: 10px;">
                <h3 style="color: #2c5f2d; margin-bottom: 1rem;">Récapitulatif</h3>
                
                <?php foreach ($cartItems as $item): ?>
                    <?php $quantity = $_SESSION['cart'][$item['id']]; ?>
                    <div style="display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid #eee;">
                        <div>
                            <strong><?php echo e($item['name']); ?></strong><br>
                            <small>Quantité: <?php echo $quantity; ?></small>
                        </div>
                        <div>
                            <?php echo formatPrice($item['price'] * $quantity); ?>
                        </div>
                    </div>
                <?php endforeach; ?>
                
                <div style="margin-top: 1.5rem; padding-top: 1.5rem; border-top: 2px solid #2c5f2d;">
                    <div style="display: flex; justify-content: space-between; font-size: 1.3rem;">
                        <strong>Total:</strong>
                        <strong style="color: #97bc62ff;"><?php echo formatPrice($total); ?></strong>
                    </div>
                </div>
            </div>
            
            <a href="/cart.php" class="btn-secondary" style="display: block; text-align: center; margin-top: 1rem;">
                Modifier mon panier
            </a>
        </div>
    </div>
</div>

<?php include 'includes/footer.php'; ?>
```

---

### ÉTAPE 10 : Page de confirmation de commande

```bash
sudo nano /var/www/shop/order-success.php
```

```php
<?php
require_once 'config/database.php';
require_once 'includes/functions.php';

session_start();

$orderId = filter_input(INPUT_GET, 'order', FILTER_VALIDATE_INT);

if (!$orderId) {
    redirect('/index.php');
}

// Récupérer les détails de la commande
$order = fetchOne("SELECT * FROM orders WHERE id = ?", [$orderId]);

if (!$order) {
    redirect('/index.php');
}

// Récupérer les articles de la commande
$items = fetchAll("
    SELECT oi.*, p.name, p.image 
    FROM order_items oi
    JOIN products p ON oi.product_id = p.id
    WHERE oi.order_id = ?
", [$orderId]);

$pageTitle = 'Commande confirmée - Boutique';

include 'includes/header.php';
?>

<div class="container" style="padding: 3rem 0;">
    <div style="max-width: 800px; margin: 0 auto; background: white; padding: 3rem; border-radius: 10px; text-align: center;">
        <div style="font-size: 4rem; margin-bottom: 1rem;">[OK]</div>
        
        <h1 style="color: #2c5f2d; margin-bottom: 1rem;">Commande confirmée !</h1>
        
        <p style="font-size: 1.2rem; color: #666; margin-bottom: 2rem;">
            Merci <?php echo e($order['customer_name']); ?> pour votre commande !
        </p>
        
        <div style="background: #f8f9fa; padding: 1.5rem; border-radius: 5px; margin: 2rem 0;">
            <p><strong>Numéro de commande :</strong> #<?php echo str_pad($order['id'], 6, '0', STR_PAD_LEFT); ?></p>
            <p><strong>Date :</strong> <?php echo date('d/m/Y à H:i', strtotime($order['created_at'])); ?></p>
            <p><strong>Montant total :</strong> <?php echo formatPrice($order['total']); ?></p>
        </div>
        
        <div style="text-align: left; margin: 2rem 0;">
            <h3 style="color: #2c5f2d; margin-bottom: 1rem;">Articles commandés :</h3>
            <?php foreach ($items as $item): ?>
                <div style="display: flex; justify-content: space-between; padding: 1rem; border-bottom: 1px solid #eee;">
                    <div>
                        <?php echo e($item['name']); ?> (x<?php echo $item['quantity']; ?>)
                    </div>
                    <div>
                        <?php echo formatPrice($item['price'] * $item['quantity']); ?>
                    </div>
                </div>
            <?php endforeach; ?>
        </div>
        
        <div style="text-align: left; background: #e7f3e8; padding: 1.5rem; border-radius: 5px; margin: 2rem 0;">
            <h3 style="color: #2c5f2d; margin-bottom: 0.5rem;">[PACKAGE] Livraison</h3>
            <p><strong>Adresse :</strong> <?php echo nl2br(e($order['address'])); ?></p>
            <p><strong>Téléphone :</strong> <?php echo e($order['phone']); ?></p>
            <p style="margin-top: 1rem;">
                Nous vous contacterons sous 24h pour confirmer la livraison.<br>
                Paiement à la livraison (espèces uniquement).
            </p>
        </div>
        
        <div style="background: #fff3cd; padding: 1rem; border-radius: 5px; margin: 2rem 0;">
            <p>[EMAIL] Un email de confirmation a été envoyé à : <strong><?php echo e($order['email']); ?></strong></p>
        </div>
        
        <a href="/index.php" class="btn-primary" style="display: inline-block; margin-top: 2rem;">
            Retour à l'accueil
        </a>
    </div>
</div>

<?php include 'includes/footer.php'; ?>
```

---

### ÉTAPE 11 : Interface d'administration - Login

```bash
sudo nano /var/www/shop/admin/login.php
```

```php
<?php
require_once '../config/database.php';
require_once '../includes/functions.php';

session_start();

// Si déjà connecté, rediriger
if (isset($_SESSION['admin_logged_in'])) {
    redirect('/admin/dashboard.php');
}

$error = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $username = trim($_POST['username'] ?? '');
    $password = $_POST['password'] ?? '';
    
    if ($username && $password) {
        $admin = fetchOne("SELECT * FROM admin_users WHERE username = ?", [$username]);
        
        if ($admin && password_verify($password, $admin['password_hash'])) {
            $_SESSION['admin_logged_in'] = true;
            $_SESSION['admin_id'] = $admin['id'];
            $_SESSION['admin_username'] = $admin['username'];
            redirect('/admin/dashboard.php');
        } else {
            $error = 'Identifiants incorrects';
        }
    } else {
        $error = 'Veuillez remplir tous les champs';
    }
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Connexion Admin</title>
    <link rel="stylesheet" href="/public/css/style.css">
</head>
<body>
    <div style="min-height: 100vh; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #2c5f2d 0%, #97bc62ff 100%);">
        <div style="background: white; padding: 3rem; border-radius: 10px; width: 100%; max-width: 400px;">
            <h1 style="text-align: center; color: #2c5f2d; margin-bottom: 2rem;">
                [SECURISE] Administration
            </h1>
            
            <?php if ($error): ?>
                <div class="alert alert-error">
                    <?php echo e($error); ?>
                </div>
            <?php endif; ?>
            
            <form method="POST">
                <div class="form-group">
                    <label>Nom d'utilisateur</label>
                    <input type="text" name="username" required autofocus>
                </div>
                
                <div class="form-group">
                    <label>Mot de passe</label>
                    <input type="password" name="password" required>
                </div>
                
                <button type="submit" class="btn-primary" style="width: 100%;">
                    Se connecter
                </button>
            </form>
            
            <p style="text-align: center; margin-top: 2rem;">
                <a href="/index.php" style="color: #2c5f2d;"><- Retour au site</a>
            </p>
        </div>
    </div>
</body>
</html>
```

---

### ÉTAPE 12 : Dashboard Admin

```bash
sudo nano /var/www/shop/admin/dashboard.php
```

```php
<?php
require_once '../config/database.php';
require_once '../includes/functions.php';

session_start();

// Vérifier l'authentification
if (!isset($_SESSION['admin_logged_in'])) {
    redirect('/admin/login.php');
}

// Statistiques
$stats = [
    'total_products' => fetchColumn("SELECT COUNT(*) FROM products"),
    'total_orders' => fetchColumn("SELECT COUNT(*) FROM orders"),
    'pending_orders' => fetchColumn("SELECT COUNT(*) FROM orders WHERE status = 'pending'"),
    'total_revenue' => fetchColumn("SELECT SUM(total) FROM orders WHERE status != 'cancelled'") ?? 0
];

// Dernières commandes
$recentOrders = fetchAll("
    SELECT * FROM orders 
    ORDER BY created_at DESC 
    LIMIT 10
");

$pageTitle = 'Dashboard Admin';
?>
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?php echo $pageTitle; ?></title>
    <link rel="stylesheet" href="/public/css/style.css">
</head>
<body>
    <header class="admin-header">
        <div class="container">
            <div style="display: flex; justify-content: space-between; align-items: center;">
                <h1 style="color: white;">Administration</h1>
                <nav class="admin-nav">
                    <a href="/admin/dashboard.php">Dashboard</a>
                    <a href="/admin/products.php">Produits</a>
                    <a href="/admin/orders.php">Commandes</a>
                    <a href="/admin/logout.php">Déconnexion</a>
                </nav>
            </div>
        </div>
    </header>
    
    <div class="container" style="padding: 3rem 0;">
        <h2>Tableau de bord</h2>
        
        <!-- Statistiques -->
        <div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 2rem; margin: 2rem 0;">
            <div style="background: white; padding: 2rem; border-radius: 10px; text-align: center;">
                <div style="font-size: 3rem;">[PACKAGE]</div>
                <div style="font-size: 2rem; font-weight: bold; color: #2c5f2d;">
                    <?php echo $stats['total_products']; ?>
                </div>
                <div>Produits</div>
            </div>
            
            <div style="background: white; padding: 2rem; border-radius: 10px; text-align: center;">
                <div style="font-size: 3rem;">[SHOPPING_TROLLEY]</div>
                <div style="font-size: 2rem; font-weight: bold; color: #2c5f2d;">
                    <?php echo $stats['total_orders']; ?>
                </div>
                <div>Commandes</div>
            </div>
            
            <div style="background: white; padding: 2rem; border-radius: 10px; text-align: center;">
                <div style="font-size: 3rem;">[HOURGLASS_WITH_FLOWING_SAND]</div>
                <div style="font-size: 2rem; font-weight: bold; color: #ffc107;">
                    <?php echo $stats['pending_orders']; ?>
                </div>
                <div>En attente</div>
            </div>
            
            <div style="background: white; padding: 2rem; border-radius: 10px; text-align: center;">
                <div style="font-size: 3rem;">[ARGENT]</div>
                <div style="font-size: 1.5rem; font-weight: bold; color: #28a745;">
                    <?php echo formatPrice($stats['total_revenue']); ?>
                </div>
                <div>Chiffre d'affaires</div>
            </div>
        </div>
        
        <!-- Dernières commandes -->
        <div style="background: white; padding: 2rem; border-radius: 10px; margin-top: 2rem;">
            <h3 style="margin-bottom: 1rem;">Dernières commandes</h3>
            
            <table class="table">
                <thead>
                    <tr>
                        <th>N°</th>
                        <th>Client</th>
                        <th>Date</th>
                        <th>Montant</th>
                        <th>Statut</th>
                        <th>Action</th>
                    </tr>
                </thead>
                <tbody>
                    <?php foreach ($recentOrders as $order): ?>
                        <tr>
                            <td>#<?php echo str_pad($order['id'], 6, '0', STR_PAD_LEFT); ?></td>
                            <td><?php echo e($order['customer_name']); ?></td>
                            <td><?php echo date('d/m/Y H:i', strtotime($order['created_at'])); ?></td>
                            <td><?php echo formatPrice($order['total']); ?></td>
                            <td>
                                <span class="badge badge-<?php echo $order['status']; ?>">
                                    <?php echo ucfirst($order['status']); ?>
                                </span>
                            </td>
                            <td>
                                <a href="/admin/orders.php?id=<?php echo $order['id']; ?>">Détails</a>
                            </td>
                        </tr>
                    <?php endforeach; ?>
                </tbody>
            </table>
        </div>
    </div>
</body>
</html>
```

---

### ÉTAPE 13 : Déconnexion Admin

```bash
sudo nano /var/www/shop/admin/logout.php
```

```php
<?php
session_start();
session_destroy();
header('Location: /admin/login.php');
exit();
```

---

### ÉTAPE 14 : Virtual Host Apache

```bash
sudo nano /etc/apache2/sites-available/shop.conf
```

```apache
<VirtualHost *:80>
    ServerName shop.local
    ServerAdmin admin@shop.local
    DocumentRoot /var/www/shop
    
    <Directory /var/www/shop>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
        DirectoryIndex index.php index.html
        
        # Protection fichiers sensibles
        <FilesMatch "^\.">
            Require all denied
        </FilesMatch>
    </Directory>
    
    # Protection dossier config
    <Directory /var/www/shop/config>
        Require all denied
    </Directory>
    
    ErrorLog ${APACHE_LOG_DIR}/shop-error.log
    CustomLog ${APACHE_LOG_DIR}/shop-access.log combined
</VirtualHost>
```

**Activer le site :**

```bash
sudo a2ensite shop.conf
sudo systemctl reload apache2
```

**Ajouter dans `/etc/hosts` (sur ton PC) :**

```
192.168.1.100    shop.local
```

---

### ÉTAPE 15 : Images de test

```bash
# Créer une image placeholder
sudo mkdir -p /var/www/shop/public/images/products

# Télécharger ou créer des images de test
# Pour l'instant, on peut utiliser des placeholders
```

**Permissions finales :**

```bash
sudo chown -R www-data:www-data /var/www/shop
sudo find /var/www/shop -type d -exec chmod 755 {} \;
sudo find /var/www/shop -type f -exec chmod 644 {} \;
```

---

## [OK] TESTS DE VALIDATION

### Test 1 : Page d'accueil

```
http://shop.local
```

- [ ] Les 8 produits s'affichent
- [ ] Les prix sont formatés en FCFA
- [ ] Le bouton "Ajouter au panier" fonctionne
- [ ] Le compteur du panier s'incrémente

---

### Test 2 : Détail produit

```
http://shop.local/product.php?id=1
```

- [ ] Image, nom, description s'affichent
- [ ] Prix et stock visibles
- [ ] Sélection de quantité fonctionne
- [ ] Ajout au panier réussi

---

### Test 3 : Panier

```
http://shop.local/cart.php
```

- [ ] Produits ajoutés s'affichent
- [ ] Modification de quantité fonctionne
- [ ] Suppression de produit fonctionne
- [ ] Total calculé correctement

---

### Test 4 : Commande

```
http://shop.local/checkout.php
```

- [ ] Formulaire s'affiche
- [ ] Validation fonctionne (champs obligatoires)
- [ ] Commande créée en base de données
- [ ] Stock décrémenté
- [ ] Page de confirmation affichée
- [ ] Panier vidé

**Vérifier en base :**

```bash
sudo mysql -u root -p shop_db
```

```sql
SELECT * FROM orders ORDER BY id DESC LIMIT 1;
SELECT * FROM order_items WHERE order_id = (SELECT MAX(id) FROM orders);
SELECT name, stock FROM products;
```

---

### Test 5 : Administration

```
http://shop.local/admin/login.php
```

- [ ] Page de login affichée
- [ ] Connexion avec admin/admin123
- [ ] Dashboard accessible
- [ ] Statistiques correctes
- [ ] Dernières commandes affichées

---

## [ROUGE] ERREURS COURANTES

### Erreur 1 : "Connection refused" MySQL

```
SQLSTATE[HY000] [2002] Connection refused
```

**Solution :**

```bash
sudo systemctl start mysql
sudo systemctl enable mysql
```

---

### Erreur 2 : Page blanche (erreur PHP)

**Activer affichage erreurs temporairement :**

```bash
sudo nano /etc/php/8.1/fpm/php.ini
```

```ini
display_errors = On
error_reporting = E_ALL
```

```bash
sudo systemctl restart php8.1-fpm
```

---

### Erreur 3 : Images ne s'affichent pas

**Vérifier permissions :**

```bash
ls -la /var/www/shop/public/images/products/
```

**Corriger si nécessaire :**

```bash
sudo chmod 755 /var/www/shop/public/images/products/
```

---

## [IMPORTANT] POINTS CLÉS LAMP

**1. Architecture 4 couches**
- Linux (OS)
- Apache (Serveur web)
- MySQL (Base de données)
- PHP (Langage de programmation)

**2. Avantages**
- Stack la plus utilisée au monde
- Documentation abondante
- Hébergement facile et pas cher
- Parfait pour débuter

**3. Limitations**
- Performance moyenne comparé à des stacks modernes
- Moins adapté au temps réel
- Scaling complexe pour haute charge

---

## [RAPIDE] POUR ALLER PLUS LOIN

**1. Ajouter un vrai système de paiement**
- Intégrer Orange Money API
- Wave Senegal API
- PayPal

**2. Améliorer l'admin**
- Gestion complète des produits (CRUD)
- Upload d'images
- Statistiques avancées
- Export PDF des commandes

**3. Optimisations**
- Cache avec Memcached/Redis
- CDN pour images
- Minification CSS/JS
- Lazy loading images

**4. Fonctionnalités supplémentaires**
- Système d'avis produits
- Wishlist
- Code promo / réductions
- Newsletter
- Recherche produits

---

## [COURS] CONCLUSION EXERCICE LAMP

**[OK] Tu as créé une boutique e-commerce complète avec LAMP !**

**Compétences acquises :**
- Installation et configuration stack LAMP
- Développement PHP procédural
- MySQL : schéma relationnel, requêtes complexes
- PDO : requêtes préparées, transactions
- Sessions PHP
- Sécurité : XSS, SQL injection, authentification
- Architecture MVC basique

**Temps réalisé :** 4-6 heures

**Prochaine étape :** LEMP Stack avec Nginx (performance améliorée) ! [RAPIDE]

# [JAUNE] EXERCICE 2 : LEMP STACK - API REST POUR APPLICATION MOBILE

## [LISTE] ÉNONCÉ

### Contexte professionnel

Tu es développeur backend dans une startup qui lance une **application mobile de réservation de restaurants** à Dakar. L'équipe frontend développe l'app mobile (React Native), et tu dois créer l'**API REST backend** qui va gérer :

- Liste des restaurants (avec filtres)
- Détails d'un restaurant (menu, horaires, photos)
- Système de réservation
- Authentification utilisateurs (JWT)
- Avis et notes

**Contraintes techniques :**
- Performance critique (app mobile = connexion lente)
- API RESTful (JSON uniquement)
- Authentification par tokens
- Documentation API claire

**Stack choisie :** LEMP (pour la performance supérieure à LAMP)

---

## [OBJECTIF] POURQUOI LEMP ?

### [OK] Avantages par rapport à LAMP

**1. Performance supérieure**
- Nginx traite 2 à 3 fois plus de requêtes qu'Apache
- Consommation mémoire réduite (30-40% de moins)
- Meilleur pour contenu statique (images, CSS, JS)
- Architecture événementielle vs processus

**Comparaison chiffrée :**
```
Apache (mod_php) :
- 2000 requêtes/seconde
- 150 MB RAM pour 100 connexions simultanées

Nginx (PHP-FPM) :
- 5000-6000 requêtes/seconde
- 50 MB RAM pour 100 connexions simultanées
```

**2. Configuration claire et simple**
- Syntaxe plus lisible qu'Apache
- Moins de modules à gérer
- Configuration centralisée

**3. Proxy inverse natif**
- Parfait pour architectures modernes
- Load balancing intégré
- Cache HTTP performant

**4. Gestion connexions simultanées**
- Architecture asynchrone (event-driven)
- Pas de limite théorique de connexions
- Idéal pour WebSockets, long-polling

**5. Ressources optimisées**
- Faible empreinte mémoire
- CPU moins sollicité
- Parfait pour serveurs avec ressources limitées

### [X] Inconvénients

**1. .htaccess non supporté**
- Toute config doit être dans le fichier server block
- Moins flexible pour utilisateurs sans accès root
- Migration depuis Apache nécessite réécriture

**2. Traitement PHP externe**
- Nginx ne peut pas exécuter PHP directement
- Obligé d'utiliser PHP-FPM (pas forcément un défaut)
- Configuration plus complexe que mod_php

**3. Moins de modules tiers**
- Écosystème plus petit qu'Apache
- Certains modules Apache n'ont pas d'équivalent
- Modules compilés dans Nginx (pas chargement dynamique)

**4. Courbe d'apprentissage**
- Syntaxe différente d'Apache
- Concepts nouveaux (upstream, location, etc.)
- Documentation moins abondante en français

---

## [GRAPHIQUE] QUAND UTILISER LEMP ?

### [OK] Cas d'usage idéaux

**1. APIs REST haute performance**
- Applications mobiles (iOS/Android)
- Microservices
- API publiques avec fort trafic
- IoT (nombreux devices connectés)

**2. Sites à fort trafic**
- > 10 000 visiteurs/jour
- Pics de charge importants
- Médias/actualités
- Plateformes SaaS

**3. Applications temps réel**
- Chat en ligne
- Notifications push
- Live streaming
- Gaming multiplayer

**4. Serveurs de fichiers statiques**
- CDN personnel
- Hébergement images/vidéos
- Assets frontend (SPA)

**5. Reverse proxy / Load balancer**
- Devant applications Node.js, Python, Ruby
- Distribution de charge
- Cache applicatif

**6. Ressources limitées**
- VPS avec peu de RAM (<2GB)
- Serveurs partagés
- Budget hosting limité

### [X] Éviter LEMP pour

**1. Hébergement mutualisé classique**
- Pas d'accès SSH/root
- .htaccess nécessaire
- -> Utiliser LAMP

**2. CMS traditionnels sans optimisation**
- Certains plugins WordPress incompatibles
- Joomla, Drupal avec .htaccess custom
- -> Possible mais demande adaptations

**3. Applications legacy**
- Code dépendant de mod_rewrite Apache
- Applications anciennes non maintenues
- -> Migration complexe

**4. Équipe sans compétences Nginx**
- Si toute l'équipe connaît seulement Apache
- Pas de temps pour formation
- -> Rester sur LAMP temporairement

---

## [CONSTRUCTION] ARCHITECTURE DU PROJET

### Vue d'ensemble

```
┌─────────────────────────────────────────────────────┐
│              APPLICATION MOBILE                      │
│  (React Native - iOS/Android)                        │
└────────────┬────────────────────────────────────────┘
             │ HTTPS + JSON
             v
┌─────────────────────────────────────────────────────┐
│                   NGINX                              │
│  • Reverse proxy                                     │
│  • SSL termination                                   │
│  • Rate limiting                                     │
│  • Cache HTTP                                        │
└────────────┬────────────────────────────────────────┘
             │ FastCGI
             v
┌─────────────────────────────────────────────────────┐
│                  PHP-FPM                             │
│  • API REST                                          │
│  • Business logic                                    │
│  • JWT authentication                                │
└────────────┬────────────────────────────────────────┘
             │ MySQLi/PDO
             v
┌─────────────────────────────────────────────────────┐
│                   MySQL 8.0                          │
│  • Restaurants                                       │
│  • Réservations                                      │
│  • Utilisateurs                                      │
│  • Avis                                              │
└─────────────────────────────────────────────────────┘
```

### Architecture API REST

```
API Endpoints :

┌────────────────────────────────────────────────┐
│ PUBLIC (sans auth)                             │
├────────────────────────────────────────────────┤
│ GET  /api/restaurants          Liste           │
│ GET  /api/restaurants/:id      Détails         │
│ GET  /api/restaurants/:id/menu Menu            │
│ POST /api/auth/register        Inscription     │
│ POST /api/auth/login           Connexion       │
└────────────────────────────────────────────────┘

┌────────────────────────────────────────────────┐
│ PROTÉGÉ (JWT requis)                           │
├────────────────────────────────────────────────┤
│ POST   /api/reservations       Créer           │
│ GET    /api/reservations       Mes réservations│
│ DELETE /api/reservations/:id   Annuler         │
│ POST   /api/reviews            Poster avis     │
│ GET    /api/profile            Mon profil      │
│ PUT    /api/profile            Modifier profil │
└────────────────────────────────────────────────┘
```

### Format de réponse JSON

```json
// Success
{
  "success": true,
  "data": {
    "id": 1,
    "name": "Restaurant Le Lagon"
  }
}

// Error
{
  "success": false,
  "error": {
    "code": "INVALID_TOKEN",
    "message": "Token d'authentification invalide"
  }
}

// Liste paginée
{
  "success": true,
  "data": [...],
  "pagination": {
    "page": 1,
    "perPage": 20,
    "total": 45,
    "totalPages": 3
  }
}
```

---

## [CODE] INSTALLATION COMPLÈTE

### ÉTAPE 1 : Installer Nginx

```bash
# Mise à jour
sudo apt update

# Installation
sudo apt install nginx -y

# Démarrer et activer
sudo systemctl start nginx
sudo systemctl enable nginx

# Vérifier
sudo systemctl status nginx
```

**Résultat :**

```
[BLACK_CIRCLE] nginx.service - A high performance web server
     Loaded: loaded
     Active: active (running)
```

**Tester :**

```
http://localhost
```

**Page "Welcome to nginx!" doit s'afficher.**

---

### ÉTAPE 2 : Installer MySQL

```bash
# Installation
sudo apt install mysql-server -y

# Sécurisation
sudo mysql_secure_installation
```

**Réponses recommandées :**

```
Validate password component? Y
Password level: 2 (Strong)
Root password: MySecurePass123!
Remove anonymous users? Y
Disallow root login remotely? Y
Remove test database? Y
Reload privilege tables? Y
```

---

### ÉTAPE 3 : Installer PHP 8.1 avec extensions

```bash
# Installation PHP-FPM et extensions
sudo apt install -y php8.1-fpm php8.1-mysql php8.1-mbstring \
  php8.1-xml php8.1-curl php8.1-gd php8.1-zip php8.1-bcmath

# Vérifier
php -v
```

**Vérifier PHP-FPM :**

```bash
sudo systemctl status php8.1-fpm
```

---

### ÉTAPE 4 : Installer Composer (gestionnaire de dépendances PHP)

```bash
# Télécharger Composer
curl -sS https://getcomposer.org/installer | php

# Installer globalement
sudo mv composer.phar /usr/local/bin/composer

# Vérifier
composer --version
```

---

## [ARCHIVE] CRÉATION DE LA BASE DE DONNÉES

### ÉTAPE 1 : Schéma de la base

```bash
sudo mysql -u root -p
```

```sql
-- ═══════════════════════════════════════════════════════════════
-- BASE DE DONNÉES API RESTAURANTS
-- ═══════════════════════════════════════════════════════════════

CREATE DATABASE resto_api CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE resto_api;

-- ───────────────────────────────────────────────────────────────
-- TABLE : users (Utilisateurs)
-- ───────────────────────────────────────────────────────────────

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(150) NOT NULL UNIQUE,
    phone VARCHAR(20),
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    INDEX idx_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- ───────────────────────────────────────────────────────────────
-- TABLE : restaurants
-- ───────────────────────────────────────────────────────────────

CREATE TABLE restaurants (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    description TEXT,
    cuisine_type VARCHAR(50) NOT NULL,
    address TEXT NOT NULL,
    phone VARCHAR(20) NOT NULL,
    email VARCHAR(150),
    opening_hours JSON,
    price_range ENUM('€', '€€', '€€€', '€€€€') DEFAULT '€€',
    rating DECIMAL(2,1) DEFAULT 0.0,
    photo_url VARCHAR(255),
    latitude DECIMAL(10, 8),
    longitude DECIMAL(11, 8),
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    INDEX idx_cuisine (cuisine_type),
    INDEX idx_rating (rating),
    INDEX idx_location (latitude, longitude)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

/*
 * JSON pour opening_hours :
 * {
 *   "monday": {"open": "11:00", "close": "23:00"},
 *   "tuesday": {"open": "11:00", "close": "23:00"},
 *   ...
 * }
 * 
 * DECIMAL(10,8) et DECIMAL(11,8) pour coordonnées GPS :
 * Précision nécessaire pour géolocalisation
 */

-- ───────────────────────────────────────────────────────────────
-- TABLE : menu_items (Articles du menu)
-- ───────────────────────────────────────────────────────────────

CREATE TABLE menu_items (
    id INT AUTO_INCREMENT PRIMARY KEY,
    restaurant_id INT NOT NULL,
    name VARCHAR(200) NOT NULL,
    description TEXT,
    price DECIMAL(10,2) NOT NULL,
    category VARCHAR(50) NOT NULL,
    photo_url VARCHAR(255),
    is_available BOOLEAN DEFAULT TRUE,
    
    FOREIGN KEY (restaurant_id) REFERENCES restaurants(id) ON DELETE CASCADE,
    INDEX idx_restaurant (restaurant_id),
    INDEX idx_category (category)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- ───────────────────────────────────────────────────────────────
-- TABLE : reservations
-- ───────────────────────────────────────────────────────────────

CREATE TABLE reservations (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    restaurant_id INT NOT NULL,
    reservation_date DATE NOT NULL,
    reservation_time TIME NOT NULL,
    number_of_people INT NOT NULL,
    status ENUM('pending', 'confirmed', 'cancelled', 'completed') DEFAULT 'pending',
    special_requests TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (restaurant_id) REFERENCES restaurants(id) ON DELETE CASCADE,
    
    INDEX idx_user (user_id),
    INDEX idx_restaurant (restaurant_id),
    INDEX idx_date (reservation_date),
    INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- ───────────────────────────────────────────────────────────────
-- TABLE : reviews (Avis)
-- ───────────────────────────────────────────────────────────────

CREATE TABLE reviews (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    restaurant_id INT NOT NULL,
    rating INT NOT NULL CHECK (rating BETWEEN 1 AND 5),
    comment TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (restaurant_id) REFERENCES restaurants(id) ON DELETE CASCADE,
    
    UNIQUE KEY unique_user_restaurant (user_id, restaurant_id),
    INDEX idx_restaurant (restaurant_id),
    INDEX idx_rating (rating)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

/*
 * CHECK (rating BETWEEN 1 AND 5) :
 * Contrainte : rating doit être entre 1 et 5
 * 
 * UNIQUE KEY unique_user_restaurant :
 * Un utilisateur ne peut laisser qu'UN SEUL avis par restaurant
 */

-- ═══════════════════════════════════════════════════════════════
-- DONNÉES DE TEST
-- ═══════════════════════════════════════════════════════════════

-- Restaurants
INSERT INTO restaurants (name, description, cuisine_type, address, phone, price_range, opening_hours, latitude, longitude, photo_url) VALUES
('Le Lagon', 'Restaurant de fruits de mer avec vue sur l''océan. Spécialités de poissons frais et thiof braisé.', 'Fruits de mer', 'Almadies, Route de Ngor, Dakar', '+221 33 820 10 20', '€€€', 
 '{"monday":{"open":"12:00","close":"23:00"},"tuesday":{"open":"12:00","close":"23:00"},"wednesday":{"open":"12:00","close":"23:00"},"thursday":{"open":"12:00","close":"23:00"},"friday":{"open":"12:00","close":"00:00"},"saturday":{"open":"12:00","close":"00:00"},"sunday":{"open":"12:00","close":"23:00"}}',
 14.7392, -17.5144, 'lagon.jpg'),

('Chez Loutcha', 'Cuisine sénégalaise traditionnelle dans un cadre authentique. Thiéboudienne, yassa, mafé...', 'Sénégalaise', 'Plateau, Avenue Pompidou, Dakar', '+221 33 821 54 32', '€€', 
 '{"monday":{"open":"11:00","close":"22:00"},"tuesday":{"open":"11:00","close":"22:00"},"wednesday":{"open":"11:00","close":"22:00"},"thursday":{"open":"11:00","close":"22:00"},"friday":{"open":"11:00","close":"22:00"},"saturday":{"open":"11:00","close":"22:00"},"sunday":{"open":"11:00","close":"22:00"}}',
 14.6928, -17.4467, 'loutcha.jpg'),

('La Fourchette', 'Restaurant français gastronomique. Cuisine raffinée et carte des vins exceptionnelle.', 'Française', 'Fann Résidence, Rue FN-23, Dakar', '+221 33 824 11 79', '€€€€', 
 '{"monday":"closed","tuesday":{"open":"19:00","close":"23:00"},"wednesday":{"open":"19:00","close":"23:00"},"thursday":{"open":"19:00","close":"23:00"},"friday":{"open":"19:00","close":"23:30"},"saturday":{"open":"19:00","close":"23:30"},"sunday":{"open":"12:00","close":"15:00"}}',
 14.6957, -17.4583, 'fourchette.jpg'),

('Teranga Grill', 'Grillades et brochettes dans une ambiance conviviale. Terrasse agréable.', 'Grill', 'Sacré-Cœur 3, VDN, Dakar', '+221 77 123 45 67', '€', 
 '{"monday":{"open":"18:00","close":"01:00"},"tuesday":{"open":"18:00","close":"01:00"},"wednesday":{"open":"18:00","close":"01:00"},"thursday":{"open":"18:00","close":"01:00"},"friday":{"open":"18:00","close":"02:00"},"saturday":{"open":"18:00","close":"02:00"},"sunday":{"open":"18:00","close":"01:00"}}',
 14.7156, -17.4669, 'teranga.jpg'),

('Sushi Dakar', 'Restaurant japonais moderne. Sushis, sashimis et plats chauds. Poissons ultra-frais.', 'Japonaise', 'Mamelles, près Radisson Blu, Dakar', '+221 33 869 24 24', '€€€', 
 '{"monday":"closed","tuesday":{"open":"12:00","close":"14:30"},"tuesday_dinner":{"open":"19:00","close":"22:30"},"wednesday":{"open":"12:00","close":"14:30"},"wednesday_dinner":{"open":"19:00","close":"22:30"},"thursday":{"open":"12:00","close":"14:30"},"thursday_dinner":{"open":"19:00","close":"22:30"},"friday":{"open":"12:00","close":"14:30"},"friday_dinner":{"open":"19:00","close":"23:00"},"saturday":{"open":"12:00","close":"14:30"},"saturday_dinner":{"open":"19:00","close":"23:00"},"sunday":{"open":"12:00","close":"14:30"},"sunday_dinner":{"open":"19:00","close":"22:30"}}',
 14.7253, -17.4789, 'sushi.jpg');

-- Menu items (pour Le Lagon)
INSERT INTO menu_items (restaurant_id, name, description, price, category) VALUES
(1, 'Thiof braisé', 'Thiof entier braisé aux épices, servi avec riz cantonnais et légumes', 12500, 'Plats'),
(1, 'Carpaccio de dorade', 'Dorade rose marinée citron vert et huile d''olive', 7500, 'Entrées'),
(1, 'Crevettes grillées', 'Grosses crevettes grillées sauce piquante maison', 15000, 'Plats'),
(1, 'Salade de poulpe', 'Poulpe mariné, tomates, oignons rouges, persil', 8000, 'Entrées'),
(1, 'Tarte citron meringuée', 'Tarte citron avec meringue italienne', 4500, 'Desserts');

-- Utilisateur de test (mot de passe: test123)
INSERT INTO users (name, email, phone, password_hash) VALUES
('Abdou Diallo', 'abdou@example.com', '+221 77 555 11 22', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi');

-- Avis de test
INSERT INTO reviews (user_id, restaurant_id, rating, comment) VALUES
(1, 1, 5, 'Excellent restaurant ! Poissons ultra frais et service impeccable. Je recommande vivement.'),
(1, 2, 4, 'Très bon thiéboudienne, authentique et copieux. Ambiance chaleureuse.');

-- ═══════════════════════════════════════════════════════════════

-- Créer utilisateur pour l'API
CREATE USER 'resto_api'@'localhost' IDENTIFIED BY 'ApiPass123!';
GRANT ALL PRIVILEGES ON resto_api.* TO 'resto_api'@'localhost';
FLUSH PRIVILEGES;

EXIT;
```

---

## [OUTIL] DÉVELOPPEMENT DE L'API

### Structure du projet

```
/var/www/resto-api/
├── public/
│   └── index.php          <- Point d'entrée unique
├── src/
│   ├── Config/
│   │   └── Database.php   <- Connexion DB
│   ├── Controllers/
│   │   ├── AuthController.php
│   │   ├── RestaurantController.php
│   │   ├── ReservationController.php
│   │   └── ReviewController.php
│   ├── Models/
│   │   ├── User.php
│   │   ├── Restaurant.php
│   │   ├── Reservation.php
│   │   └── Review.php
│   ├── Middleware/
│   │   └── AuthMiddleware.php
│   └── Utils/
│       ├── JWT.php
│       └── Response.php
├── composer.json          <- Dépendances
└── .env                   <- Configuration
```

---

### ÉTAPE 1 : Créer la structure

```bash
# Créer les dossiers
sudo mkdir -p /var/www/resto-api/{public,src/{Config,Controllers,Models,Middleware,Utils}}

# Permissions
sudo chown -R www-data:www-data /var/www/resto-api
sudo chmod -R 755 /var/www/resto-api
```

---

### ÉTAPE 2 : Initialiser Composer

```bash
cd /var/www/resto-api
sudo -u www-data composer init --no-interaction
```

**Créer `composer.json` :**

```bash
sudo nano /var/www/resto-api/composer.json
```

```json
{
    "name": "app/resto-api",
    "description": "API REST pour réservation restaurants",
    "type": "project",
    "require": {
        "php": ">=8.1",
        "firebase/php-jwt": "^6.0"
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    }
}
```

**Installer les dépendances :**

```bash
cd /var/www/resto-api
sudo -u www-data composer install
```

---

### ÉTAPE 3 : Configuration (.env)

```bash
sudo nano /var/www/resto-api/.env
```

```ini
# Database
DB_HOST=localhost
DB_NAME=resto_api
DB_USER=resto_api
DB_PASS=ApiPass123!

# JWT
JWT_SECRET=votre_cle_secrete_tres_longue_et_complexe_123456789
JWT_EXPIRY=86400

# API
API_VERSION=v1
```

---

### ÉTAPE 4 : Classe Database

```bash
sudo nano /var/www/resto-api/src/Config/Database.php
```

```php
<?php
namespace App\Config;

use PDO;
use PDOException;

class Database {
    private static $instance = null;
    private $connection;
    
    private function __construct() {
        $env = parse_ini_file(__DIR__ . '/../../.env');
        
        $dsn = sprintf(
            "mysql:host=%s;dbname=%s;charset=utf8mb4",
            $env['DB_HOST'],
            $env['DB_NAME']
        );
        
        $options = [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false,
        ];
        
        try {
            $this->connection = new PDO(
                $dsn, 
                $env['DB_USER'], 
                $env['DB_PASS'], 
                $options
            );
        } catch (PDOException $e) {
            http_response_code(500);
            die(json_encode([
                'success' => false,
                'error' => ['message' => 'Database connection failed']
            ]));
        }
    }
    
    public static function getInstance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    public function getConnection() {
        return $this->connection;
    }
}
?>
```

---

### ÉTAPE 5 : Utilitaire JWT

```bash
sudo nano /var/www/resto-api/src/Utils/JWT.php
```

```php
<?php
namespace App\Utils;

use Firebase\JWT\JWT as FirebaseJWT;
use Firebase\JWT\Key;

class JWT {
    private static $secret;
    private static $expiry;
    
    public static function init() {
        $env = parse_ini_file(__DIR__ . '/../../.env');
        self::$secret = $env['JWT_SECRET'];
        self::$expiry = $env['JWT_EXPIRY'];
    }
    
    public static function encode($userId) {
        self::init();
        
        $payload = [
            'iss' => 'resto-api',
            'iat' => time(),
            'exp' => time() + self::$expiry,
            'userId' => $userId
        ];
        
        return FirebaseJWT::encode($payload, self::$secret, 'HS256');
    }
    
    public static function decode($token) {
        self::init();
        
        try {
            $decoded = FirebaseJWT::decode($token, new Key(self::$secret, 'HS256'));
            return $decoded->userId;
        } catch (\Exception $e) {
            return null;
        }
    }
}
?>
```

---

### ÉTAPE 6 : Utilitaire Response

```bash
sudo nano /var/www/resto-api/src/Utils/Response.php
```

```php
<?php
namespace App\Utils;

class Response {
    public static function json($data, $status = 200) {
        http_response_code($status);
        header('Content-Type: application/json');
        echo json_encode($data, JSON_UNESCAPED_UNICODE);
        exit;
    }
    
    public static function success($data, $pagination = null) {
        $response = ['success' => true, 'data' => $data];
        if ($pagination) {
            $response['pagination'] = $pagination;
        }
        self::json($response);
    }
    
    public static function error($message, $code = 'ERROR', $status = 400) {
        self::json([
            'success' => false,
            'error' => [
                'code' => $code,
                'message' => $message
            ]
        ], $status);
    }
}
?>
```

---

### ÉTAPE 7 : Middleware d'authentification

```bash
sudo nano /var/www/resto-api/src/Middleware/AuthMiddleware.php
```

```php
<?php
namespace App\Middleware;

use App\Utils\JWT;
use App\Utils\Response;

class AuthMiddleware {
    public static function authenticate() {
        $headers = getallheaders();
        
        if (!isset($headers['Authorization'])) {
            Response::error('Token manquant', 'MISSING_TOKEN', 401);
        }
        
        $authHeader = $headers['Authorization'];
        
        // Format : "Bearer TOKEN"
        if (!preg_match('/Bearer\s(\S+)/', $authHeader, $matches)) {
            Response::error('Format de token invalide', 'INVALID_TOKEN_FORMAT', 401);
        }
        
        $token = $matches[1];
        $userId = JWT::decode($token);
        
        if (!$userId) {
            Response::error('Token invalide ou expiré', 'INVALID_TOKEN', 401);
        }
        
        return $userId;
    }
}
?>
```

---

### ÉTAPE 8 : Controller d'authentification

```bash
sudo nano /var/www/resto-api/src/Controllers/AuthController.php
```

```php
<?php
namespace App\Controllers;

use App\Config\Database;
use App\Utils\JWT;
use App\Utils\Response;

class AuthController {
    private $db;
    
    public function __construct() {
        $this->db = Database::getInstance()->getConnection();
    }
    
    public function register() {
        $data = json_decode(file_get_contents('php://input'), true);
        
        // Validation
        if (empty($data['name']) || empty($data['email']) || empty($data['password'])) {
            Response::error('Données incomplètes', 'INCOMPLETE_DATA');
        }
        
        if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
            Response::error('Email invalide', 'INVALID_EMAIL');
        }
        
        if (strlen($data['password']) < 6) {
            Response::error('Le mot de passe doit contenir au moins 6 caractères', 'WEAK_PASSWORD');
        }
        
        // Vérifier si email existe déjà
        $stmt = $this->db->prepare("SELECT id FROM users WHERE email = ?");
        $stmt->execute([$data['email']]);
        
        if ($stmt->fetch()) {
            Response::error('Cet email est déjà utilisé', 'EMAIL_EXISTS');
        }
        
        // Créer l'utilisateur
        $passwordHash = password_hash($data['password'], PASSWORD_DEFAULT);
        
        $stmt = $this->db->prepare("
            INSERT INTO users (name, email, phone, password_hash) 
            VALUES (?, ?, ?, ?)
        ");
        
        $stmt->execute([
            $data['name'],
            $data['email'],
            $data['phone'] ?? null,
            $passwordHash
        ]);
        
        $userId = $this->db->lastInsertId();
        $token = JWT::encode($userId);
        
        Response::success([
            'userId' => $userId,
            'token' => $token,
            'name' => $data['name'],
            'email' => $data['email']
        ], 201);
    }
    
    public function login() {
        $data = json_decode(file_get_contents('php://input'), true);
        
        if (empty($data['email']) || empty($data['password'])) {
            Response::error('Email et mot de passe requis', 'INCOMPLETE_DATA');
        }
        
        $stmt = $this->db->prepare("SELECT * FROM users WHERE email = ?");
        $stmt->execute([$data['email']]);
        $user = $stmt->fetch();
        
        if (!$user || !password_verify($data['password'], $user['password_hash'])) {
            Response::error('Identifiants incorrects', 'INVALID_CREDENTIALS', 401);
        }
        
        $token = JWT::encode($user['id']);
        
        Response::success([
            'userId' => $user['id'],
            'token' => $token,
            'name' => $user['name'],
            'email' => $user['email']
        ]);
    }
}
?>
```

---

### ÉTAPE 9 : Controller restaurants

```bash
sudo nano /var/www/resto-api/src/Controllers/RestaurantController.php
```

```php
<?php
namespace App\Controllers;

use App\Config\Database;
use App\Utils\Response;

class RestaurantController {
    private $db;
    
    public function __construct() {
        $this->db = Database::getInstance()->getConnection();
    }
    
    public function list() {
        $page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
        $perPage = isset($_GET['perPage']) ? (int)$_GET['perPage'] : 20;
        $cuisine = $_GET['cuisine'] ?? null;
        $priceRange = $_GET['priceRange'] ?? null;
        
        $offset = ($page - 1) * $perPage;
        
        // Construction requête avec filtres
        $sql = "SELECT * FROM restaurants WHERE is_active = 1";
        $params = [];
        
        if ($cuisine) {
            $sql .= " AND cuisine_type = ?";
            $params[] = $cuisine;
        }
        
        if ($priceRange) {
            $sql .= " AND price_range = ?";
            $params[] = $priceRange;
        }
        
        // Compter le total
        $countStmt = $this->db->prepare(str_replace("*", "COUNT(*)", $sql));
        $countStmt->execute($params);
        $total = $countStmt->fetchColumn();
        
        // Récupérer les restaurants
        $sql .= " ORDER BY rating DESC LIMIT ? OFFSET ?";
        $params[] = $perPage;
        $params[] = $offset;
        
        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);
        $restaurants = $stmt->fetchAll();
        
        // Décoder JSON opening_hours
        foreach ($restaurants as &$resto) {
            $resto['opening_hours'] = json_decode($resto['opening_hours'], true);
        }
        
        Response::success($restaurants, [
            'page' => $page,
            'perPage' => $perPage,
            'total' => $total,
            'totalPages' => ceil($total / $perPage)
        ]);
    }
    
    public function get($id) {
        $stmt = $this->db->prepare("
            SELECT * FROM restaurants WHERE id = ? AND is_active = 1
        ");
        $stmt->execute([$id]);
        $restaurant = $stmt->fetch();
        
        if (!$restaurant) {
            Response::error('Restaurant non trouvé', 'NOT_FOUND', 404);
        }
        
        // Récupérer le menu
        $menuStmt = $this->db->prepare("
            SELECT * FROM menu_items 
            WHERE restaurant_id = ? AND is_available = 1
            ORDER BY category, name
        ");
        $menuStmt->execute([$id]);
        $menu = $menuStmt->fetchAll();
        
        // Récupérer les avis
        $reviewsStmt = $this->db->prepare("
            SELECT r.*, u.name as user_name
            FROM reviews r
            JOIN users u ON r.user_id = u.id
            WHERE r.restaurant_id = ?
            ORDER BY r.created_at DESC
            LIMIT 10
        ");
        $reviewsStmt->execute([$id]);
        $reviews = $reviewsStmt->fetchAll();
        
        $restaurant['opening_hours'] = json_decode($restaurant['opening_hours'], true);
        $restaurant['menu'] = $menu;
        $restaurant['reviews'] = $reviews;
        
        Response::success($restaurant);
    }
}
?>
```

---

Je continue avec le Controller de réservations et le point d'entrée de l'API dans la prochaine section...

### ÉTAPE 10 : Controller réservations

```bash
sudo nano /var/www/resto-api/src/Controllers/ReservationController.php
```

```php
<?php
namespace App\Controllers;

use App\Config\Database;
use App\Utils\Response;
use App\Middleware\AuthMiddleware;

class ReservationController {
    private $db;
    private $userId;
    
    public function __construct() {
        $this->db = Database::getInstance()->getConnection();
        $this->userId = AuthMiddleware::authenticate();
    }
    
    public function create() {
        $data = json_decode(file_get_contents('php://input'), true);
        
        // Validation
        $required = ['restaurant_id', 'reservation_date', 'reservation_time', 'number_of_people'];
        foreach ($required as $field) {
            if (empty($data[$field])) {
                Response::error("Le champ $field est requis", 'INCOMPLETE_DATA');
            }
        }
        
        // Vérifier que le restaurant existe
        $stmt = $this->db->prepare("SELECT id FROM restaurants WHERE id = ? AND is_active = 1");
        $stmt->execute([$data['restaurant_id']]);
        if (!$stmt->fetch()) {
            Response::error('Restaurant non trouvé', 'RESTAURANT_NOT_FOUND', 404);
        }
        
        // Vérifier que la date n'est pas dans le passé
        $reservationDateTime = $data['reservation_date'] . ' ' . $data['reservation_time'];
        if (strtotime($reservationDateTime) < time()) {
            Response::error('Impossible de réserver dans le passé', 'INVALID_DATE');
        }
        
        // Créer la réservation
        $stmt = $this->db->prepare("
            INSERT INTO reservations (
                user_id, restaurant_id, reservation_date, 
                reservation_time, number_of_people, special_requests
            ) VALUES (?, ?, ?, ?, ?, ?)
        ");
        
        $stmt->execute([
            $this->userId,
            $data['restaurant_id'],
            $data['reservation_date'],
            $data['reservation_time'],
            $data['number_of_people'],
            $data['special_requests'] ?? null
        ]);
        
        $reservationId = $this->db->lastInsertId();
        
        Response::success([
            'reservationId' => $reservationId,
            'message' => 'Réservation créée avec succès'
        ], 201);
    }
    
    public function list() {
        $stmt = $this->db->prepare("
            SELECT r.*, rest.name as restaurant_name, rest.address
            FROM reservations r
            JOIN restaurants rest ON r.restaurant_id = rest.id
            WHERE r.user_id = ?
            ORDER BY r.reservation_date DESC, r.reservation_time DESC
        ");
        
        $stmt->execute([$this->userId]);
        $reservations = $stmt->fetchAll();
        
        Response::success($reservations);
    }
    
    public function cancel($id) {
        // Vérifier que la réservation appartient à l'utilisateur
        $stmt = $this->db->prepare("
            SELECT * FROM reservations 
            WHERE id = ? AND user_id = ?
        ");
        $stmt->execute([$id, $this->userId]);
        $reservation = $stmt->fetch();
        
        if (!$reservation) {
            Response::error('Réservation non trouvée', 'NOT_FOUND', 404);
        }
        
        if ($reservation['status'] === 'cancelled') {
            Response::error('Cette réservation est déjà annulée', 'ALREADY_CANCELLED');
        }
        
        // Annuler
        $stmt = $this->db->prepare("
            UPDATE reservations SET status = 'cancelled' WHERE id = ?
        ");
        $stmt->execute([$id]);
        
        Response::success(['message' => 'Réservation annulée']);
    }
}
?>
```

---

### ÉTAPE 11 : Point d'entrée de l'API (Router)

```bash
sudo nano /var/www/resto-api/public/index.php
```

```php
<?php
/**
 * ═══════════════════════════════════════════════════════════════
 * API REST - POINT D'ENTRÉE
 * ═══════════════════════════════════════════════════════════════
 */

require_once __DIR__ . '/../vendor/autoload.php';

use App\Controllers\AuthController;
use App\Controllers\RestaurantController;
use App\Controllers\ReservationController;
use App\Utils\Response;

// Headers CORS
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

// Répondre aux preflight requests
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(200);
    exit;
}

// Récupérer la méthode HTTP et l'URL
$method = $_SERVER['REQUEST_METHOD'];
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

// Supprimer /api/v1 du début
$uri = preg_replace('#^/api/v1#', '', $uri);

// Router simple
try {
    // ───────────────────────────────────────────────────────────
    // AUTHENTIFICATION (public)
    // ───────────────────────────────────────────────────────────
    
    if ($method === 'POST' && $uri === '/auth/register') {
        $controller = new AuthController();
        $controller->register();
    }
    
    if ($method === 'POST' && $uri === '/auth/login') {
        $controller = new AuthController();
        $controller->login();
    }
    
    // ───────────────────────────────────────────────────────────
    // RESTAURANTS (public)
    // ───────────────────────────────────────────────────────────
    
    if ($method === 'GET' && $uri === '/restaurants') {
        $controller = new RestaurantController();
        $controller->list();
    }
    
    if ($method === 'GET' && preg_match('#^/restaurants/(\d+)$#', $uri, $matches)) {
        $controller = new RestaurantController();
        $controller->get($matches[1]);
    }
    
    // ───────────────────────────────────────────────────────────
    // RÉSERVATIONS (protégé)
    // ───────────────────────────────────────────────────────────
    
    if ($method === 'POST' && $uri === '/reservations') {
        $controller = new ReservationController();
        $controller->create();
    }
    
    if ($method === 'GET' && $uri === '/reservations') {
        $controller = new ReservationController();
        $controller->list();
    }
    
    if ($method === 'DELETE' && preg_match('#^/reservations/(\d+)$#', $uri, $matches)) {
        $controller = new ReservationController();
        $controller->cancel($matches[1]);
    }
    
    // ───────────────────────────────────────────────────────────
    // ROUTE NON TROUVÉE
    // ───────────────────────────────────────────────────────────
    
    Response::error('Route non trouvée', 'NOT_FOUND', 404);
    
} catch (Exception $e) {
    Response::error('Erreur serveur: ' . $e->getMessage(), 'SERVER_ERROR', 500);
}
?>
```

---

### ÉTAPE 12 : Configuration Nginx

```bash
sudo nano /etc/nginx/sites-available/resto-api
```

```nginx
# ═══════════════════════════════════════════════════════════════
# CONFIGURATION NGINX - API REST RESTAURANTS
# ═══════════════════════════════════════════════════════════════

server {
    listen 80;
    server_name api.resto.local;
    
    root /var/www/resto-api/public;
    index index.php;
    
    # Logs
    access_log /var/log/nginx/resto-api-access.log;
    error_log /var/log/nginx/resto-api-error.log;
    
    # ───────────────────────────────────────────────────────────
    # CONFIGURATION API
    # ───────────────────────────────────────────────────────────
    
    # Toutes les requêtes vers /api/v1 vont vers index.php
    location /api/v1 {
        try_files $uri $uri/ /index.php?$query_string;
    }
    
    # ───────────────────────────────────────────────────────────
    # PHP-FPM
    # ───────────────────────────────────────────────────────────
    
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.1-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
    
    # ───────────────────────────────────────────────────────────
    # SÉCURITÉ
    # ───────────────────────────────────────────────────────────
    
    # Bloquer accès aux fichiers cachés
    location ~ /\. {
        deny all;
    }
    
    # Bloquer accès direct aux fichiers sensibles
    location ~ \.(env|json|lock)$ {
        deny all;
    }
    
    # ───────────────────────────────────────────────────────────
    # RATE LIMITING (optionnel)
    # ───────────────────────────────────────────────────────────
    
    # Limite: 100 requêtes par minute par IP
    limit_req_zone $binary_remote_addr zone=api:10m rate=100r/m;
    limit_req zone=api burst=20 nodelay;
}
```

**Activer le site :**

```bash
# Créer lien symbolique
sudo ln -s /etc/nginx/sites-available/resto-api /etc/nginx/sites-enabled/

# Tester la configuration
sudo nginx -t

# Recharger Nginx
sudo systemctl reload nginx
```

---

### ÉTAPE 13 : Ajouter dans hosts

**Sur ton PC :**

```
192.168.1.100    api.resto.local
```

---

## [OK] TESTS DE L'API

### Test 1 : Inscription

```bash
curl -X POST http://api.resto.local/api/v1/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Marie Seck",
    "email": "marie@example.com",
    "phone": "+221 77 888 99 00",
    "password": "password123"
  }'
```

**Résultat attendu :**

```json
{
  "success": true,
  "data": {
    "userId": 2,
    "token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
    "name": "Marie Seck",
    "email": "marie@example.com"
  }
}
```

---

### Test 2 : Connexion

```bash
curl -X POST http://api.resto.local/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "abdou@example.com",
    "password": "test123"
  }'
```

---

### Test 3 : Liste restaurants

```bash
curl http://api.resto.local/api/v1/restaurants
```

**Avec filtres :**

```bash
curl "http://api.resto.local/api/v1/restaurants?cuisine=Sénégalaise&page=1"
```

---

### Test 4 : Détails restaurant

```bash
curl http://api.resto.local/api/v1/restaurants/1
```

---

### Test 5 : Créer réservation (avec token)

```bash
TOKEN="eyJ0eXAiOiJKV1QiLCJhbGc..."

curl -X POST http://api.resto.local/api/v1/reservations \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "restaurant_id": 1,
    "reservation_date": "2026-01-15",
    "reservation_time": "20:00:00",
    "number_of_people": 4,
    "special_requests": "Table près de la fenêtre svp"
  }'
```

---

### Test 6 : Mes réservations

```bash
curl -H "Authorization: Bearer $TOKEN" \
  http://api.resto.local/api/v1/reservations
```

---

### Test 7 : Annuler réservation

```bash
curl -X DELETE \
  -H "Authorization: Bearer $TOKEN" \
  http://api.resto.local/api/v1/reservations/1
```

---

## [GRAPHIQUE] TESTS DE PERFORMANCE

### Benchmark Apache vs Nginx

```bash
# Installer Apache Bench
sudo apt install apache2-utils -y

# Test Nginx
ab -n 1000 -c 100 http://api.resto.local/api/v1/restaurants

# Résultats typiques Nginx :
# Requests per second: 5000-6000
# Time per request: 16-20ms
```

---

## [ROUGE] ERREURS COURANTES

### Erreur 1 : 502 Bad Gateway

**Cause :** PHP-FPM ne tourne pas

```bash
sudo systemctl start php8.1-fpm
sudo systemctl enable php8.1-fpm
```

---

### Erreur 2 : Token JWT invalide

**Cause :** Secret JWT différent ou token expiré

**Vérifier `.env` :**

```ini
JWT_SECRET=la_même_clé_partout
JWT_EXPIRY=86400
```

---

### Erreur 3 : CORS bloqué

**Ajouter dans Nginx :**

```nginx
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
```

---

## [IMPORTANT] POINTS CLÉS LEMP

**1. Architecture**
- Nginx = Reverse proxy + serveur web
- PHP-FPM = Pool de processus PHP séparés
- MySQL = Base de données relationnelle

**2. Avantages sur LAMP**
- 2-3x plus performant
- Consommation RAM réduite de 40%
- Meilleur pour APIs REST

**3. Cas d'usage**
- APIs mobiles
- Sites à fort trafic
- Applications temps réel

---

## [RAPIDE] POUR ALLER PLUS LOIN

**1. HTTPS avec Let's Encrypt**

```bash
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d api.resto.local
```

---

**2. Cache Nginx**

```nginx
# Dans server block
fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=API:100m;
fastcgi_cache API;
fastcgi_cache_valid 200 60m;
```

---

**3. Monitoring avec Nginx Status**

```nginx
location /nginx_status {
    stub_status on;
    access_log off;
    allow 127.0.0.1;
    deny all;
}
```

---

**4. Load Balancing**

```nginx
upstream api_backend {
    server localhost:9001;
    server localhost:9002;
    server localhost:9003;
}

location /api {
    proxy_pass http://api_backend;
}
```

---

## [COURS] CONCLUSION EXERCICE LEMP

**[OK] Tu as créé une API REST performante avec LEMP !**

**Compétences acquises :**
- Installation et configuration Nginx
- API REST avec architecture MVC
- Authentification JWT
- Routing API
- Performance optimization
- CORS et sécurité API

**Performance :** 3x plus rapide que LAMP

**Prochaine étape :** LAPP Stack avec PostgreSQL (robustesse entreprise) ! [RAPIDE]

# [JAUNE] EXERCICE 3 : LAPP STACK - SYSTÈME DE GESTION DE BIBLIOTHÈQUE

## [LISTE] ÉNONCÉ

### Contexte professionnel

Tu es développeur dans une université qui souhaite moderniser sa **bibliothèque universitaire**. Le système actuel est sur papier et Excel. La direction veut une application web pour :

- Cataloguer les livres (titre, auteur, ISBN, catégorie)
- Gérer les emprunts et retours
- Suivre les retards et calculer les pénalités
- Générer des rapports statistiques
- Recherche avancée (full-text search)
- Réservation de livres

**Exigences techniques :**
- **Intégrité des données** critique (pas de perte de transactions)
- **Requêtes complexes** (jointures, agrégations, statistiques)
- **ACID** complet (transactions bancaires-like)
- **Recherche full-text** performante
- **Scalabilité** (50 000+ livres, 10 000+ étudiants)

**Stack choisie :** LAPP (PostgreSQL pour sa robustesse et ses fonctionnalités avancées)

---

## [OBJECTIF] POURQUOI LAPP (PostgreSQL) ?

### [OK] Avantages par rapport à MySQL

**1. Conformité ACID stricte**
- Transactions 100% fiables
- Isolation parfaite (MVCC - Multi-Version Concurrency Control)
- Pas de perte de données même en cas de crash
- Lock management sophistiqué

**Exemple concret :**
```sql
-- PostgreSQL : Transaction atomique garantie
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- Si crash ici, TOUT est annulé (rien n'est perdu)
COMMIT;

-- MySQL MyISAM : Pas de transaction
-- MySQL InnoDB : Transactions mais moins robustes
```

**2. Types de données avancés**
- **JSON/JSONB** : Stockage et requêtes JSON natives
- **Array** : Tableaux natifs
- **UUID** : Identifiants uniques universels
- **hstore** : Paires clé-valeur
- **Géospatial** (PostGIS) : Coordonnées, distances, zones
- **Full-text search** intégré

**3. Requêtes complexes performantes**
- **CTE (Common Table Expressions)** : Requêtes récursives
- **Window functions** : Classements, moyennes glissantes
- **Full outer joins**
- **Agrégations avancées**

**Exemple :**
```sql
-- Classement des livres les plus empruntés par catégorie
WITH ranked_books AS (
  SELECT 
    b.title, 
    b.category,
    COUNT(l.id) as loan_count,
    RANK() OVER (PARTITION BY b.category ORDER BY COUNT(l.id) DESC) as rank
  FROM books b
  LEFT JOIN loans l ON b.id = l.book_id
  GROUP BY b.id, b.category
)
SELECT * FROM ranked_books WHERE rank <= 3;

-- Cette requête est complexe pour MySQL < 8.0
```

**4. Extensibilité**
- **Extensions** : PostGIS, pg_trgm, hstore, etc.
- **Langages procéduraux** : PL/pgSQL, PL/Python, PL/Perl
- **Fonctions personnalisées**
- **Triggers avancés**

**5. Concurrence optimale**
- **MVCC** : Lecteurs ne bloquent jamais les écrivains
- Pas de "table locks" globaux
- Meilleure performance en multi-utilisateurs

**6. Standards SQL**
- Conformité SQL:2016 quasi-complète
- Syntaxe standard (portable)
- Respecte les normes ANSI

### [X] Inconvénients

**1. Consommation RAM**
- PostgreSQL plus gourmand que MySQL
- Nécessite configuration fine (shared_buffers, work_mem)
- Minimum 1GB RAM recommandé (vs 512MB MySQL)

**2. Complexité**
- Courbe d'apprentissage plus raide
- Configuration plus complexe
- Administration nécessite expertise

**3. Réplication**
- Réplication MySQL plus simple (historiquement)
- PostgreSQL rattrape mais moins mature sur ce point
- Nécessite outils tiers pour HA avancée

**4. Performance INSERT massifs**
- MySQL peut être plus rapide pour bulk inserts
- PostgreSQL optimisé pour OLTP (transactions)
- Nécessite tuning pour gros volumes

**5. Écosystème**
- Moins d'hébergeurs proposent PostgreSQL
- phpPgAdmin moins mature que phpMyAdmin
- Moins de tutoriels/ressources en français

---

## [GRAPHIQUE] QUAND UTILISER LAPP ?

### [OK] Cas d'usage idéaux

**1. Applications critiques (finance, santé)**
- Banques, assurances
- Dossiers médicaux électroniques
- Systèmes de paie
- -> Intégrité des données primordiale

**2. Applications complexes**
- ERP (Enterprise Resource Planning)
- CRM avancés
- Systèmes de gestion universitaire
- -> Requêtes complexes, jointures multiples

**3. Big Data / Analytics**
- Entrepôts de données (Data Warehouses)
- Rapports statistiques avancés
- Business Intelligence
- -> Window functions, CTE, agrégations

**4. Applications géospatiales**
- SIG (Systèmes d'Information Géographique)
- Applications de livraison
- Géolocalisation
- -> PostGIS extension

**5. Applications multi-tenant**
- SaaS avec isolation parfaite
- Schemas PostgreSQL (namespaces)
- Row-level security

**6. Recherche full-text avancée**
- Moteurs de recherche internes
- Archives documentaires
- Bases de connaissances
- -> pg_trgm, ts_vector

### [X] Éviter LAPP pour

**1. Applications simples**
- Blogs personnels
- Sites vitrines
- Landing pages
- -> MySQL/MariaDB suffit

**2. Hébergement mutualisé**
- Peu d'hébergeurs supportent PostgreSQL
- Plus cher que MySQL
- -> LAMP plus accessible

**3. Équipe MySQL-only**
- Migration coûteuse
- Formation nécessaire
- -> Garder MySQL temporairement

**4. Besoin de réplication simple**
- MySQL master-slave plus simple
- PostgreSQL nécessite expertise
- -> Envisager MySQL/MariaDB

---

## [CONSTRUCTION] ARCHITECTURE DU PROJET

### Vue d'ensemble

```
┌─────────────────────────────────────────────────────┐
│              INTERFACE WEB                           │
│  (HTML/CSS/JS - Interface bibliothécaire/étudiant)   │
└────────────┬────────────────────────────────────────┘
             │ HTTP
             v
┌─────────────────────────────────────────────────────┐
│                   APACHE 2.4                         │
│  • Serveur web                                       │
│  • Virtual hosts (public + admin)                    │
└────────────┬────────────────────────────────────────┘
             │ PHP-FPM
             v
┌─────────────────────────────────────────────────────┐
│                  PHP 8.1                             │
│  • Logique métier                                    │
│  • Gestion emprunts/retours                          │
│  • Calcul pénalités                                  │
│  • Génération rapports                               │
└────────────┬────────────────────────────────────────┘
             │ PDO PostgreSQL
             v
┌─────────────────────────────────────────────────────┐
│               POSTGRESQL 14                          │
│  • Catalogue livres (50 000+)                        │
│  • Utilisateurs (étudiants, staff)                   │
│  • Emprunts (transactions ACID)                      │
│  • Historique complet                                │
│  • Full-text search                                  │
└─────────────────────────────────────────────────────┘
```

### Schéma de base de données

```
┌──────────────┐
│   books      │  <- Catalogue
├──────────────┤
│ id (PK)      │
│ isbn (UNIQUE)│
│ title        │
│ authors[]    │  <- Array PostgreSQL
│ category     │
│ publisher    │
│ year         │
│ copies_total │
│ copies_avail │
│ search_vec   │  <- tsvector (full-text)
└──────────────┘
      ^
      │ (1-to-many)
      │
┌──────────────┐
│    loans     │  <- Emprunts
├──────────────┤
│ id (PK)      │
│ book_id (FK) │
│ user_id (FK) │
│ loaned_at    │
│ due_date     │
│ returned_at  │
│ fine_amount  │
│ status       │
└──────────────┘
      │
      v (many-to-one)
┌──────────────┐
│    users     │  <- Étudiants/Staff
├──────────────┤
│ id (PK)      │
│ student_id   │
│ name         │
│ email        │
│ role         │
│ active       │
└──────────────┘
```

---

## [CODE] INSTALLATION COMPLÈTE

### ÉTAPE 1 : Installer PostgreSQL 14

```bash
# Importer la clé GPG du repo officiel PostgreSQL
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -

# Ajouter le repository
echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" | sudo tee /etc/apt/sources.list.d/pgdg.list

# Mettre à jour et installer
sudo apt update
sudo apt install postgresql-14 postgresql-contrib-14 -y

# Vérifier
sudo systemctl status postgresql
```

**Résultat :**

```
[BLACK_CIRCLE] postgresql.service - PostgreSQL RDBMS
     Loaded: loaded
     Active: active (exited)
```

---

### ÉTAPE 2 : Configurer PostgreSQL

**Se connecter en tant que postgres :**

```bash
sudo -u postgres psql
```

**Tu es maintenant dans le shell PostgreSQL :**

```
postgres=#
```

**Créer un mot de passe pour postgres :**

```sql
ALTER USER postgres WITH PASSWORD 'PostgresPass123!';
```

**Quitter :**

```sql
\q
```

---

**Permettre les connexions locales :**

```bash
sudo nano /etc/postgresql/14/main/pg_hba.conf
```

**Modifier la ligne :**

```
# IPv4 local connections:
host    all             all             127.0.0.1/32            md5
```

**Remplacer `peer` par `md5` pour authentification par mot de passe.**

**Redémarrer PostgreSQL :**

```bash
sudo systemctl restart postgresql
```

---

### ÉTAPE 3 : Installer Apache et PHP avec extension PostgreSQL

```bash
# Apache
sudo apt install apache2 -y

# PHP avec PostgreSQL
sudo apt install -y php8.1 php8.1-fpm php8.1-pgsql php8.1-mbstring \
  php8.1-xml php8.1-curl php8.1-gd php8.1-zip

# Configurer Apache pour PHP-FPM
sudo a2enmod proxy_fcgi setenvif
sudo a2enconf php8.1-fpm

# Redémarrer
sudo systemctl restart apache2
sudo systemctl restart php8.1-fpm
```

---

## [ARCHIVE] CRÉATION DE LA BASE DE DONNÉES

### ÉTAPE 1 : Créer la base et l'utilisateur

```bash
sudo -u postgres psql
```

```sql
-- ═══════════════════════════════════════════════════════════════
-- BASE DE DONNÉES BIBLIOTHÈQUE
-- ═══════════════════════════════════════════════════════════════

-- Créer la base
CREATE DATABASE library_db 
  WITH ENCODING 'UTF8' 
  LC_COLLATE='fr_FR.UTF-8' 
  LC_CTYPE='fr_FR.UTF-8'
  TEMPLATE=template0;

-- Se connecter à la base
\c library_db

-- Activer les extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";     -- UUID
CREATE EXTENSION IF NOT EXISTS "pg_trgm";       -- Recherche floue
CREATE EXTENSION IF NOT EXISTS "unaccent";      -- Supprimer accents

-- ───────────────────────────────────────────────────────────────
-- TYPES ÉNUMÉRÉS
-- ───────────────────────────────────────────────────────────────

CREATE TYPE user_role AS ENUM ('student', 'staff', 'admin');
CREATE TYPE loan_status AS ENUM ('active', 'returned', 'overdue');

-- ───────────────────────────────────────────────────────────────
-- TABLE : users
-- ───────────────────────────────────────────────────────────────

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    student_id VARCHAR(20) UNIQUE NOT NULL,
    name VARCHAR(200) NOT NULL,
    email VARCHAR(150) UNIQUE NOT NULL,
    phone VARCHAR(20),
    role user_role DEFAULT 'student',
    password_hash VARCHAR(255) NOT NULL,
    active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_student_id ON users(student_id);

/*
 * SERIAL = AUTO_INCREMENT de PostgreSQL
 * Crée automatiquement une séquence
 * 
 * user_role = Type énuméré custom
 * Plus strict que VARCHAR
 */

-- ───────────────────────────────────────────────────────────────
-- TABLE : books
-- ───────────────────────────────────────────────────────────────

CREATE TABLE books (
    id SERIAL PRIMARY KEY,
    isbn VARCHAR(13) UNIQUE,
    title VARCHAR(500) NOT NULL,
    authors TEXT[] NOT NULL,                    -- Array de texte
    category VARCHAR(100) NOT NULL,
    publisher VARCHAR(200),
    publication_year INTEGER,
    pages INTEGER,
    language VARCHAR(10) DEFAULT 'fr',
    description TEXT,
    copies_total INTEGER DEFAULT 1 CHECK (copies_total > 0),
    copies_available INTEGER DEFAULT 1 CHECK (copies_available >= 0),
    location VARCHAR(50),
    search_vector tsvector,                     -- Pour full-text search
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    CONSTRAINT check_copies CHECK (copies_available <= copies_total)
);

CREATE INDEX idx_books_isbn ON books(isbn);
CREATE INDEX idx_books_category ON books(category);
CREATE INDEX idx_books_authors ON books USING GIN(authors);
CREATE INDEX idx_books_search ON books USING GIN(search_vector);

/*
 * TEXT[] = Array de texte
 * Exemple : {'Victor Hugo', 'Alexandre Dumas'}
 * 
 * tsvector = Type spécial pour recherche full-text
 * Stocke le texte décomposé en tokens
 * 
 * GIN index = Generalized Inverted Index
 * Optimisé pour arrays et full-text
 * 
 * CHECK constraints = Contraintes de validation
 * Garantit l'intégrité des données
 */

-- Trigger pour mettre à jour search_vector automatiquement
CREATE OR REPLACE FUNCTION update_search_vector() 
RETURNS TRIGGER AS $$
BEGIN
    NEW.search_vector := 
        setweight(to_tsvector('french', unaccent(coalesce(NEW.title, ''))), 'A') ||
        setweight(to_tsvector('french', unaccent(array_to_string(NEW.authors, ' '))), 'B') ||
        setweight(to_tsvector('french', unaccent(coalesce(NEW.description, ''))), 'C');
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER books_search_update 
BEFORE INSERT OR UPDATE ON books
FOR EACH ROW EXECUTE FUNCTION update_search_vector();

/*
 * to_tsvector('french', ...) :
 * Convertit le texte en vecteur de recherche
 * 'french' = Configuration linguistique (stemming français)
 * 
 * unaccent() :
 * Supprime les accents
 * "été" -> "ete"
 * 
 * setweight(..., 'A') :
 * Poids pour le ranking
 * A = Plus important (titre)
 * B = Important (auteurs)
 * C = Moins important (description)
 * 
 * || = Concaténation de tsvector
 */

-- ───────────────────────────────────────────────────────────────
-- TABLE : loans
-- ───────────────────────────────────────────────────────────────

CREATE TABLE loans (
    id SERIAL PRIMARY KEY,
    book_id INTEGER NOT NULL REFERENCES books(id) ON DELETE RESTRICT,
    user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    loaned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    due_date DATE NOT NULL,
    returned_at TIMESTAMP,
    status loan_status DEFAULT 'active',
    fine_amount NUMERIC(10,2) DEFAULT 0.00,
    notes TEXT,
    
    CONSTRAINT check_dates CHECK (due_date >= loaned_at::date)
);

CREATE INDEX idx_loans_book ON loans(book_id);
CREATE INDEX idx_loans_user ON loans(user_id);
CREATE INDEX idx_loans_status ON loans(status);
CREATE INDEX idx_loans_due_date ON loans(due_date) WHERE status = 'active';

/*
 * NUMERIC(10,2) = Nombre décimal précis
 * 10 chiffres au total, 2 après la virgule
 * Parfait pour montants financiers
 * 
 * Index conditionnel :
 * WHERE status = 'active'
 * Index seulement les lignes actives
 * Plus petit, plus rapide
 */

-- ───────────────────────────────────────────────────────────────
-- TABLE : reservations
-- ───────────────────────────────────────────────────────────────

CREATE TABLE reservations (
    id SERIAL PRIMARY KEY,
    book_id INTEGER NOT NULL REFERENCES books(id) ON DELETE CASCADE,
    user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    reserved_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    expires_at TIMESTAMP NOT NULL,
    status VARCHAR(20) DEFAULT 'pending',
    notified BOOLEAN DEFAULT FALSE,
    
    UNIQUE(book_id, user_id, status)
);

CREATE INDEX idx_reservations_book ON reservations(book_id);
CREATE INDEX idx_reservations_user ON reservations(user_id);

-- ═══════════════════════════════════════════════════════════════
-- DONNÉES DE TEST
-- ═══════════════════════════════════════════════════════════════

-- Utilisateurs
INSERT INTO users (student_id, name, email, phone, role, password_hash) VALUES
('E2024001', 'Amadou Diallo', 'amadou@univ.sn', '+221 77 111 22 33', 'student', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi'),
('E2024002', 'Fatou Sall', 'fatou@univ.sn', '+221 77 222 33 44', 'student', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi'),
('S2024001', 'Ousmane Kane', 'ousmane@univ.sn', '+221 77 333 44 55', 'staff', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi');

-- Livres
INSERT INTO books (isbn, title, authors, category, publisher, publication_year, pages, description, copies_total, copies_available, location) VALUES
('9782070360567', 'Les Misérables', ARRAY['Victor Hugo'], 'Littérature française', 'Gallimard', 1862, 1664, 'Roman fleuve de Victor Hugo publié en 1862. Histoire de Jean Valjean, Cosette et Fantine.', 5, 5, 'A-12'),
('9782070413119', 'Le Petit Prince', ARRAY['Antoine de Saint-Exupéry'], 'Littérature française', 'Gallimard', 1943, 96, 'Conte philosophique et poétique sur l''enfance et la condition humaine.', 10, 8, 'A-15'),
('9782253004226', 'L''Étranger', ARRAY['Albert Camus'], 'Littérature française', 'Gallimard', 1942, 186, 'Roman d''Albert Camus sur l''absurdité de la condition humaine.', 7, 5, 'A-20'),
('9782070368228', 'Une si longue lettre', ARRAY['Mariama Bâ'], 'Littérature africaine', 'Serpent à Plumes', 1979, 131, 'Roman épistolaire de Mariama Bâ sur la condition de la femme au Sénégal.', 8, 6, 'B-05'),
('9782246852094', 'Le Ventre de l''Atlantique', ARRAY['Fatou Diome'], 'Littérature africaine', 'Anne Carrière', 2003, 295, 'Roman de Fatou Diome sur l''immigration et l''identité.', 6, 4, 'B-08'),
('9780262033848', 'Introduction to Algorithms', ARRAY['Thomas H. Cormen', 'Charles E. Leiserson', 'Ronald L. Rivest', 'Clifford Stein'], 'Informatique', 'MIT Press', 2009, 1312, 'Manuel de référence sur les algorithmes et structures de données.', 15, 10, 'C-10'),
('9780134685991', 'Effective Java', ARRAY['Joshua Bloch'], 'Informatique', 'Addison-Wesley', 2017, 416, 'Guide des meilleures pratiques en Java.', 12, 9, 'C-15'),
('9782100790128', 'Mathématiques pour l''économie', ARRAY['Knut Sydsæter', 'Peter Hammond'], 'Mathématiques', 'Dunod', 2018, 896, 'Manuel de mathématiques appliquées à l''économie.', 20, 15, 'D-05');

-- Emprunts
INSERT INTO loans (book_id, user_id, due_date, status) VALUES
(2, 1, CURRENT_DATE + INTERVAL '14 days', 'active'),
(3, 1, CURRENT_DATE + INTERVAL '14 days', 'active'),
(5, 2, CURRENT_DATE + INTERVAL '7 days', 'active');

-- Mettre à jour les copies disponibles
UPDATE books SET copies_available = copies_available - 1 WHERE id IN (2, 3, 5);

-- ═══════════════════════════════════════════════════════════════

-- Créer utilisateur pour l'application
CREATE USER library_app WITH PASSWORD 'LibraryPass123!';
GRANT ALL PRIVILEGES ON DATABASE library_db TO library_app;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO library_app;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO library_app;

\q
```

---

## [OUTIL] DÉVELOPPEMENT DE L'APPLICATION

### Structure du projet

```
/var/www/library/
├── config/
│   └── database.php
├── includes/
│   ├── header.php
│   ├── footer.php
│   └── functions.php
├── public/
│   └── css/
│       └── style.css
├── index.php              <- Catalogue
├── search.php             <- Recherche
├── book.php               <- Détails livre
├── my-loans.php           <- Mes emprunts
├── borrow.php             <- Emprunter
├── return.php             <- Retourner
└── admin/
    ├── dashboard.php
    ├── books.php
    ├── loans.php
    └── users.php
```

---

### ÉTAPE 1 : Configuration base de données

```bash
sudo mkdir -p /var/www/library/{config,includes,public/css,admin}
sudo nano /var/www/library/config/database.php
```

```php
<?php
/**
 * ═══════════════════════════════════════════════════════════════
 * CONFIGURATION POSTGRESQL
 * ═══════════════════════════════════════════════════════════════
 */

define('DB_HOST', 'localhost');
define('DB_PORT', '5432');
define('DB_NAME', 'library_db');
define('DB_USER', 'library_app');
define('DB_PASS', 'LibraryPass123!');

function getDB() {
    static $pdo = null;
    
    if ($pdo === null) {
        try {
            $dsn = sprintf(
                "pgsql:host=%s;port=%s;dbname=%s",
                DB_HOST,
                DB_PORT,
                DB_NAME
            );
            
            $options = [
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                PDO::ATTR_EMULATE_PREPARES => false,
            ];
            
            $pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
            
            // Configuration PostgreSQL
            $pdo->exec("SET NAMES 'UTF8'");
            $pdo->exec("SET search_path TO public");
            
        } catch (PDOException $e) {
            error_log("Erreur DB : " . $e->getMessage());
            die("Erreur de connexion à la base de données.");
        }
    }
    
    return $pdo;
}

function query($sql, $params = []) {
    $pdo = getDB();
    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);
    return $stmt;
}

function fetchAll($sql, $params = []) {
    return query($sql, $params)->fetchAll();
}

function fetchOne($sql, $params = []) {
    return query($sql, $params)->fetch();
}

function fetchColumn($sql, $params = []) {
    return query($sql, $params)->fetchColumn();
}
?>
```

---

### ÉTAPE 2 : Fonctions utilitaires

```bash
sudo nano /var/www/library/includes/functions.php
```

```php
<?php
require_once __DIR__ . '/../config/database.php';

function e($string) {
    return htmlspecialchars($string ?? '', ENT_QUOTES, 'UTF-8');
}

/**
 * Recherche full-text avec PostgreSQL
 */
function searchBooks($query, $limit = 20) {
    if (empty($query)) {
        return [];
    }
    
    $sql = "
        SELECT 
            id, title, authors, category, 
            copies_available,
            ts_rank(search_vector, plainto_tsquery('french', unaccent(?))) as rank
        FROM books
        WHERE search_vector @@ plainto_tsquery('french', unaccent(?))
        ORDER BY rank DESC
        LIMIT ?
    ";
    
    /*
     * plainto_tsquery('french', ...) :
     * Convertit la requête utilisateur en requête full-text
     * 'french' = Configuration linguistique
     * 
     * @@ = Opérateur de correspondance full-text
     * search_vector @@ query = "le vecteur correspond à la requête"
     * 
     * ts_rank(...) :
     * Calcule un score de pertinence
     * Plus le score est élevé, plus c'est pertinent
     * 
     * unaccent() :
     * "été" trouvera aussi "ete"
     */
    
    return fetchAll($sql, [$query, $query, $limit]);
}

/**
 * Vérifier si un livre est disponible
 */
function isBookAvailable($bookId) {
    $sql = "SELECT copies_available FROM books WHERE id = ?";
    $available = fetchColumn($sql, [$bookId]);
    return $available > 0;
}

/**
 * Emprunter un livre (transaction ACID)
 */
function borrowBook($bookId, $userId) {
    $pdo = getDB();
    
    try {
        $pdo->beginTransaction();
        
        // Vérifier disponibilité
        if (!isBookAvailable($bookId)) {
            throw new Exception("Livre non disponible");
        }
        
        // Créer l'emprunt
        $dueDate = date('Y-m-d', strtotime('+14 days'));
        
        $sql = "
            INSERT INTO loans (book_id, user_id, due_date, status)
            VALUES (?, ?, ?, 'active')
        ";
        query($sql, [$bookId, $userId, $dueDate]);
        
        // Décrémenter copies disponibles
        $sql = "
            UPDATE books 
            SET copies_available = copies_available - 1,
                updated_at = CURRENT_TIMESTAMP
            WHERE id = ?
        ";
        query($sql, [$bookId]);
        
        $pdo->commit();
        return true;
        
    } catch (Exception $e) {
        $pdo->rollBack();
        error_log("Erreur emprunt : " . $e->getMessage());
        return false;
    }
}

/**
 * Retourner un livre
 */
function returnBook($loanId) {
    $pdo = getDB();
    
    try {
        $pdo->beginTransaction();
        
        // Récupérer l'emprunt
        $loan = fetchOne("SELECT * FROM loans WHERE id = ?", [$loanId]);
        
        if (!$loan || $loan['status'] !== 'active') {
            throw new Exception("Emprunt invalide");
        }
        
        // Calculer pénalité si retard
        $fine = 0;
        $dueDate = new DateTime($loan['due_date']);
        $today = new DateTime();
        
        if ($today > $dueDate) {
            $days = $dueDate->diff($today)->days;
            $fine = $days * 500; // 500 FCFA par jour de retard
        }
        
        // Marquer comme retourné
        $sql = "
            UPDATE loans 
            SET returned_at = CURRENT_TIMESTAMP,
                status = 'returned',
                fine_amount = ?
            WHERE id = ?
        ";
        query($sql, [$fine, $loanId]);
        
        // Incrémenter copies disponibles
        $sql = "
            UPDATE books 
            SET copies_available = copies_available + 1,
                updated_at = CURRENT_TIMESTAMP
            WHERE id = ?
        ";
        query($sql, [$loan['book_id']]);
        
        $pdo->commit();
        return ['success' => true, 'fine' => $fine];
        
    } catch (Exception $e) {
        $pdo->rollBack();
        error_log("Erreur retour : " . $e->getMessage());
        return ['success' => false];
    }
}

/**
 * Statistiques avancées avec Window Functions
 */
function getStatistics() {
    $sql = "
        WITH monthly_stats AS (
            SELECT 
                DATE_TRUNC('month', loaned_at) as month,
                COUNT(*) as total_loans,
                COUNT(DISTINCT user_id) as active_users,
                SUM(CASE WHEN status = 'overdue' THEN 1 ELSE 0 END) as overdue_count
            FROM loans
            WHERE loaned_at >= CURRENT_DATE - INTERVAL '6 months'
            GROUP BY DATE_TRUNC('month', loaned_at)
        )
        SELECT 
            TO_CHAR(month, 'YYYY-MM') as month,
            total_loans,
            active_users,
            overdue_count,
            ROUND(AVG(total_loans) OVER (ORDER BY month ROWS BETWEEN 2 PRECEDING AND CURRENT ROW), 2) as moving_avg
        FROM monthly_stats
        ORDER BY month DESC
    ";
    
    /*
     * DATE_TRUNC('month', ...) :
     * Tronque à la précision du mois
     * 2024-01-15 -> 2024-01-01
     * 
     * WINDOW FUNCTION :
     * AVG(...) OVER (ORDER BY month ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)
     * 
     * Calcule la moyenne mobile sur 3 mois :
     * - 2 PRECEDING = 2 mois précédents
     * - CURRENT ROW = Mois actuel
     * 
     * Exemple :
     * Jan: 100 emprunts
     * Fev: 120 emprunts
     * Mar: 110 emprunts -> Moyenne = (100+120+110)/3 = 110
     */
    
    return fetchAll($sql);
}
?>
```

---

Je continue avec les pages de l'interface dans la suite...

### ÉTAPE 3 : Page d'accueil (catalogue)

```bash
sudo nano /var/www/library/index.php
```

```php
<?php
session_start();
require_once 'config/database.php';
require_once 'includes/functions.php';

$pageTitle = 'Bibliothèque Universitaire - Catalogue';

// Filtres
$category = $_GET['category'] ?? null;
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$perPage = 20;
$offset = ($page - 1) * $perPage;

// Construire requête
$sql = "SELECT * FROM books WHERE 1=1";
$params = [];

if ($category) {
    $sql .= " AND category = ?";
    $params[] = $category;
}

// Compter total
$countSql = str_replace("*", "COUNT(*)", $sql);
$total = fetchColumn($countSql, $params);

// Récupérer livres
$sql .= " ORDER BY title LIMIT ? OFFSET ?";
$params[] = $perPage;
$params[] = $offset;
$books = fetchAll($sql, $params);

// Récupérer catégories
$categories = fetchAll("
    SELECT category, COUNT(*) as count 
    FROM books 
    GROUP BY category 
    ORDER BY category
");

include 'includes/header.php';
?>

<div class="container">
    <h1>[DOCS] Catalogue de la Bibliothèque</h1>
    
    <!-- Filtres -->
    <div class="filters">
        <a href="index.php" class="filter-btn <?php echo !$category ? 'active' : ''; ?>">
            Tous les livres (<?php echo $total; ?>)
        </a>
        <?php foreach ($categories as $cat): ?>
            <a href="?category=<?php echo urlencode($cat['category']); ?>" 
               class="filter-btn <?php echo $category === $cat['category'] ? 'active' : ''; ?>">
                <?php echo e($cat['category']); ?> (<?php echo $cat['count']; ?>)
            </a>
        <?php endforeach; ?>
    </div>
    
    <!-- Liste des livres -->
    <div class="books-grid">
        <?php foreach ($books as $book): ?>
            <div class="book-card">
                <h3><?php echo e($book['title']); ?></h3>
                <p class="authors">
                    <?php 
                    // PostgreSQL retourne array comme chaîne "{item1,item2}"
                    $authors = trim($book['authors'], '{}');
                    echo e($authors); 
                    ?>
                </p>
                <p class="category"><?php echo e($book['category']); ?></p>
                <p class="availability">
                    <?php if ($book['copies_available'] > 0): ?>
                        <span class="available">[OK] Disponible (<?php echo $book['copies_available']; ?>)</span>
                    <?php else: ?>
                        <span class="unavailable">[X] Emprunté</span>
                    <?php endif; ?>
                </p>
                <a href="book.php?id=<?php echo $book['id']; ?>" class="btn">
                    Voir détails
                </a>
            </div>
        <?php endforeach; ?>
    </div>
    
    <!-- Pagination -->
    <?php if ($total > $perPage): ?>
        <div class="pagination">
            <?php 
            $totalPages = ceil($total / $perPage);
            for ($i = 1; $i <= $totalPages; $i++): 
            ?>
                <a href="?page=<?php echo $i; ?><?php echo $category ? '&category=' . urlencode($category) : ''; ?>" 
                   class="<?php echo $i === $page ? 'active' : ''; ?>">
                    <?php echo $i; ?>
                </a>
            <?php endfor; ?>
        </div>
    <?php endif; ?>
</div>

<?php include 'includes/footer.php'; ?>
```

---

### ÉTAPE 4 : Page de recherche full-text

```bash
sudo nano /var/www/library/search.php
```

```php
<?php
session_start();
require_once 'config/database.php';
require_once 'includes/functions.php';

$pageTitle = 'Recherche - Bibliothèque';

$query = $_GET['q'] ?? '';
$results = [];

if ($query) {
    $results = searchBooks($query);
}

include 'includes/header.php';
?>

<div class="container">
    <h1>[RECHERCHE] Recherche dans le catalogue</h1>
    
    <form method="GET" class="search-form">
        <input type="text" 
               name="q" 
               value="<?php echo e($query); ?>" 
               placeholder="Rechercher un livre, auteur, description..."
               autofocus>
        <button type="submit">Rechercher</button>
    </form>
    
    <?php if ($query): ?>
        <p class="search-info">
            <?php echo count($results); ?> résultat(s) pour "<?php echo e($query); ?>"
        </p>
        
        <?php if (empty($results)): ?>
            <div class="no-results">
                <p>Aucun résultat trouvé.</p>
                <p>Essayez avec des termes différents ou moins spécifiques.</p>
            </div>
        <?php else: ?>
            <div class="search-results">
                <?php foreach ($results as $book): ?>
                    <div class="result-item">
                        <h3><?php echo e($book['title']); ?></h3>
                        <p class="authors">
                            <?php echo e(trim($book['authors'], '{}')); ?>
                        </p>
                        <p class="category"><?php echo e($book['category']); ?></p>
                        <p class="score">
                            Pertinence: 
                            <?php 
                            $stars = min(5, ceil($book['rank'] * 10));
                            echo str_repeat('*', $stars); 
                            ?>
                        </p>
                        <p class="availability">
                            <?php if ($book['copies_available'] > 0): ?>
                                <span class="available">[OK] Disponible</span>
                            <?php else: ?>
                                <span class="unavailable">[X] Emprunté</span>
                            <?php endif; ?>
                        </p>
                        <a href="book.php?id=<?php echo $book['id']; ?>" class="btn">
                            Voir détails
                        </a>
                    </div>
                <?php endforeach; ?>
            </div>
        <?php endif; ?>
    <?php endif; ?>
</div>

<?php include 'includes/footer.php'; ?>
```

---

### ÉTAPE 5 : Page mes emprunts

```bash
sudo nano /var/www/library/my-loans.php
```

```php
<?php
session_start();
require_once 'config/database.php';
require_once 'includes/functions.php';

// Vérifier connexion
if (!isset($_SESSION['user_id'])) {
    header('Location: login.php');
    exit;
}

$pageTitle = 'Mes Emprunts';
$userId = $_SESSION['user_id'];

// Récupérer emprunts actifs
$activeLoans = fetchAll("
    SELECT 
        l.*,
        b.title,
        b.authors,
        b.isbn,
        CASE 
            WHEN l.due_date < CURRENT_DATE THEN 
                (CURRENT_DATE - l.due_date) * 500
            ELSE 0
        END as current_fine
    FROM loans l
    JOIN books b ON l.book_id = b.id
    WHERE l.user_id = ? AND l.status = 'active'
    ORDER BY l.due_date ASC
", [$userId]);

// Récupérer historique
$history = fetchAll("
    SELECT 
        l.*,
        b.title,
        b.authors
    FROM loans l
    JOIN books b ON l.book_id = b.id
    WHERE l.user_id = ? AND l.status = 'returned'
    ORDER BY l.returned_at DESC
    LIMIT 20
", [$userId]);

include 'includes/header.php';
?>

<div class="container">
    <h1>[GUIDE] Mes Emprunts</h1>
    
    <!-- Emprunts actifs -->
    <h2>Emprunts en cours</h2>
    
    <?php if (empty($activeLoans)): ?>
        <p>Vous n'avez aucun emprunt en cours.</p>
    <?php else: ?>
        <table class="loans-table">
            <thead>
                <tr>
                    <th>Livre</th>
                    <th>Auteurs</th>
                    <th>Date d'emprunt</th>
                    <th>Date de retour</th>
                    <th>Pénalité</th>
                    <th>Action</th>
                </tr>
            </thead>
            <tbody>
                <?php foreach ($activeLoans as $loan): ?>
                    <?php
                    $isOverdue = strtotime($loan['due_date']) < time();
                    $daysLeft = ceil((strtotime($loan['due_date']) - time()) / 86400);
                    ?>
                    <tr class="<?php echo $isOverdue ? 'overdue' : ''; ?>">
                        <td><?php echo e($loan['title']); ?></td>
                        <td><?php echo e(trim($loan['authors'], '{}')); ?></td>
                        <td><?php echo date('d/m/Y', strtotime($loan['loaned_at'])); ?></td>
                        <td>
                            <?php echo date('d/m/Y', strtotime($loan['due_date'])); ?>
                            <?php if ($isOverdue): ?>
                                <span class="badge-danger">En retard !</span>
                            <?php elseif ($daysLeft <= 3): ?>
                                <span class="badge-warning">Plus que <?php echo $daysLeft; ?> jour(s)</span>
                            <?php endif; ?>
                        </td>
                        <td>
                            <?php if ($loan['current_fine'] > 0): ?>
                                <strong><?php echo number_format($loan['current_fine'], 0, ',', ' '); ?> FCFA</strong>
                            <?php else: ?>
                                -
                            <?php endif; ?>
                        </td>
                        <td>
                            <a href="return.php?id=<?php echo $loan['id']; ?>" 
                               class="btn btn-sm"
                               onclick="return confirm('Confirmer le retour ?')">
                                Retourner
                            </a>
                        </td>
                    </tr>
                <?php endforeach; ?>
            </tbody>
        </table>
    <?php endif; ?>
    
    <!-- Historique -->
    <h2 style="margin-top: 3rem;">Historique</h2>
    
    <?php if (empty($history)): ?>
        <p>Aucun historique.</p>
    <?php else: ?>
        <table class="history-table">
            <thead>
                <tr>
                    <th>Livre</th>
                    <th>Emprunté le</th>
                    <th>Retourné le</th>
                    <th>Pénalité</th>
                </tr>
            </thead>
            <tbody>
                <?php foreach ($history as $loan): ?>
                    <tr>
                        <td><?php echo e($loan['title']); ?></td>
                        <td><?php echo date('d/m/Y', strtotime($loan['loaned_at'])); ?></td>
                        <td><?php echo date('d/m/Y', strtotime($loan['returned_at'])); ?></td>
                        <td>
                            <?php if ($loan['fine_amount'] > 0): ?>
                                <?php echo number_format($loan['fine_amount'], 0, ',', ' '); ?> FCFA
                            <?php else: ?>
                                -
                            <?php endif; ?>
                        </td>
                    </tr>
                <?php endforeach; ?>
            </tbody>
        </table>
    <?php endif; ?>
</div>

<?php include 'includes/footer.php'; ?>
```

---

### ÉTAPE 6 : Configuration Apache

```bash
sudo nano /etc/apache2/sites-available/library.conf
```

```apache
<VirtualHost *:80>
    ServerName library.local
    ServerAdmin admin@library.local
    DocumentRoot /var/www/library
    
    <Directory /var/www/library>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
        DirectoryIndex index.php
    </Directory>
    
    ErrorLog ${APACHE_LOG_DIR}/library-error.log
    CustomLog ${APACHE_LOG_DIR}/library-access.log combined
</VirtualHost>
```

**Activer :**

```bash
sudo a2ensite library.conf
sudo systemctl reload apache2
```

---

## [OK] TESTS DE VALIDATION

### Test 1 : Connexion PostgreSQL

```bash
psql -U library_app -d library_db -h localhost
```

**Entrer mot de passe -> Shell PostgreSQL doit s'ouvrir.**

---

### Test 2 : Recherche full-text

```sql
-- Dans psql
SELECT title, ts_rank(search_vector, plainto_tsquery('french', 'victor hugo')) as rank
FROM books
WHERE search_vector @@ plainto_tsquery('french', 'victor hugo')
ORDER BY rank DESC;
```

**Résultat : "Les Misérables" doit apparaître.**

---

### Test 3 : Transaction ACID

```bash
# Terminal 1
psql -U library_app -d library_db
```

```sql
BEGIN;
UPDATE books SET copies_available = 0 WHERE id = 1;
-- Ne pas COMMIT
```

```bash
# Terminal 2 (autre session)
psql -U library_app -d library_db
```

```sql
SELECT copies_available FROM books WHERE id = 1;
-- Résultat : Valeur AVANT update (isolation)
```

---

### Test 4 : Window Functions (statistiques)

```sql
SELECT * FROM getStatistics();
```

---

## [GRAPHIQUE] PostgreSQL vs MySQL - Comparaison pratique

### Requête complexe : Top 3 livres par catégorie

**PostgreSQL (simple) :**

```sql
SELECT * FROM (
  SELECT 
    title, category,
    ROW_NUMBER() OVER (PARTITION BY category ORDER BY copies_total DESC) as rank
  FROM books
) ranked
WHERE rank <= 3;
```

**MySQL < 8.0 (complexe) :**

```sql
SELECT b1.*
FROM books b1
WHERE (
  SELECT COUNT(*)
  FROM books b2
  WHERE b2.category = b1.category 
    AND b2.copies_total >= b1.copies_total
) <= 3
ORDER BY category, copies_total DESC;
```

---

## [ROUGE] ERREURS COURANTES

### Erreur 1 : "could not connect to server"

```bash
sudo systemctl start postgresql
sudo systemctl enable postgresql
```

---

### Erreur 2 : Array mal parsé en PHP

**PostgreSQL retourne `{item1,item2}`**

**Solution :**

```php
// Méthode 1 : Trim simple
$authors = trim($book['authors'], '{}');

// Méthode 2 : Parse complet
$authors = pg_parse_array($book['authors']);
```

---

## [IMPORTANT] POINTS CLÉS LAPP

**1. PostgreSQL = Robustesse**
- ACID strict
- Transactions fiables
- Intégrité des données

**2. Types avancés**
- Arrays
- JSON/JSONB
- tsvector (full-text)

**3. Requêtes puissantes**
- Window functions
- CTE récursifs
- Full outer joins

**4. Extensions**
- pg_trgm (recherche floue)
- PostGIS (géospatial)
- uuid-ossp

---

## [RAPIDE] POUR ALLER PLUS LOIN

**1. Recherche floue avec pg_trgm**

```sql
CREATE EXTENSION pg_trgm;

SELECT title, similarity(title, 'Les Miserables') as sim
FROM books
WHERE title % 'Les Miserables'  -- % = Opérateur similarité
ORDER BY sim DESC;
```

---

**2. Partitionnement (gros volumes)**

```sql
-- Partitionner loans par année
CREATE TABLE loans (
    ...
) PARTITION BY RANGE (loaned_at);

CREATE TABLE loans_2024 PARTITION OF loans
    FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');
```

---

**3. Réplication streaming**

```bash
# Serveur principal
wal_level = replica
max_wal_senders = 3

# Serveur secondaire
pg_basebackup -h primary_host -D /var/lib/postgresql/14/main -U replication -P
```

---

## [COURS] CONCLUSION EXERCICE LAPP

**[OK] Tu as créé un système de bibliothèque robuste avec PostgreSQL !**

**Compétences acquises :**
- Installation PostgreSQL
- Types avancés (arrays, tsvector)
- Full-text search
- Window functions
- Transactions ACID
- CTE et requêtes complexes
- Extensions PostgreSQL

**Robustesse :** Aucune perte de données garantie

**Prochaine étape :** MEAN Stack - JavaScript full-stack moderne ! [RAPIDE]

# [ROUGE] EXERCICE 4 : MEAN STACK - GESTION DE TÂCHES COLLABORATIVE

## [LISTE] ÉNONCÉ

### Contexte professionnel

Tu es développeur full-stack dans une startup tech. L'équipe a besoin d'un **outil de gestion de projets et tâches** collaboratif en temps réel, à la Trello/Asana mais simplifié.

**Fonctionnalités requises :**
- Création de projets et tableaux
- Gestion de tâches (créer, modifier, déplacer, archiver)
- Collaboration temps réel (plusieurs utilisateurs simultanés)
- Assignation de tâches
- Commentaires et pièces jointes
- Notifications en temps réel
- Dashboard avec statistiques

**Contraintes techniques :**
- **SPA (Single Page Application)** : Interface fluide sans rechargement
- **Temps réel** : WebSockets pour synchronisation live
- **API REST** : Backend découplé du frontend
- **NoSQL** : Schéma flexible pour évolution rapide
- **JavaScript full-stack** : Une seule langue pour tout

**Stack choisie :** MEAN (moderne, performant, JavaScript partout)

---

## [OBJECTIF] POURQUOI MEAN ?

### [OK] Avantages uniques

**1. JavaScript partout (Isomorphisme)**
- **Une seule langue** : Frontend + Backend + Base de données
- **Partage de code** : Modèles, validations, utilitaires
- **Courbe d'apprentissage** : Maîtriser un seul langage
- **Équipe polyvalente** : Devs peuvent toucher à tout

**Exemple concret :**
```javascript
// Modèle User (partagé backend/frontend)
class User {
  constructor(data) {
    this.name = data.name;
    this.email = data.email;
  }
  
  validate() {
    return this.name && this.email.includes('@');
  }
}

// Backend (Node.js)
const user = new User(req.body);
if (!user.validate()) { ... }

// Frontend (Angular)
const user = new User(formData);
if (!user.validate()) { ... }
```

**2. Performance temps réel**
- **Node.js** : Event-driven, non-bloquant
- **WebSockets** : Connexions persistantes
- **Async/Await** : Gestion asynchrone moderne
- **Parfait pour** : Chat, notifications, collaboration live

**3. Scalabilité horizontale**
- **Stateless** : Facile d'ajouter des serveurs
- **MongoDB** : Sharding natif
- **Microservices** : Architecture moderne

**4. Écosystème npm**
- **1,5 million** de packages
- Tout existe déjà (auth, upload, email, etc.)
- Communauté très active

**5. JSON natif partout**
- MongoDB : Stockage JSON (BSON)
- Node.js : Manipulation JSON native
- Angular : Communication HTTP JSON
- -> Pas de conversion de format !

**6. Développement rapide**
- Prototypage ultra-rapide
- Hot-reload (développement)
- CLI puissants (Angular CLI, Express Generator)

### [X] Inconvénients

**1. Jeunesse de la stack**
- Technologies récentes (< 15 ans)
- Changements fréquents
- Breaking changes possibles

**2. Absence de typage strict (sans TypeScript)**
- JavaScript faiblement typé
- Bugs runtime possibles
- -> Résolu avec TypeScript (recommandé)

**3. NoSQL pas adapté partout**
- Transactions limitées (avant MongoDB 4.0)
- Pas de jointures natives
- Denormalisation nécessaire

**4. Callback hell (historique)**
- Code imbriqué difficile à lire
- -> Résolu avec Promises et async/await

**5. SEO complexe (SPA)**
- Contenu chargé côté client
- Crawlers ont du mal
- -> Résolu avec SSR (Angular Universal)

**6. Consommation mémoire**
- Node.js gourmand en RAM
- MongoDB aussi
- Minimum 2GB RAM recommandé

---

## [GRAPHIQUE] QUAND UTILISER MEAN ?

### [OK] Cas d'usage idéaux

**1. Applications temps réel**
- **Chat** (Slack, Discord)
- **Collaboration live** (Google Docs, Figma)
- **Tableaux de bord** temps réel
- **Jeux** multijoueurs
- **Streaming** de données

**2. Single Page Applications (SPA)**
- **Interfaces riches** et interactives
- **Applications web** qui ressemblent à des apps natives
- **Dashboards** complexes
- **Outils internes** d'entreprise

**3. API REST modernes**
- **Microservices**
- **BFF (Backend For Frontend)**
- **API publiques**
- **Mobile backend**

**4. Prototypes et MVP**
- Développement ultra-rapide
- Validation de concept
- Startups early-stage

**5. Applications à fort trafic**
- Architecture event-driven
- Non-bloquant
- Scalabilité horizontale

**6. Applications JavaScript-first**
- Équipe full-stack JavaScript
- Code partagé frontend/backend
- Écosystème npm

### [X] Éviter MEAN pour

**1. Applications transactionnelles complexes**
- Banque, comptabilité
- Transactions ACID critiques
- -> Préférer PostgreSQL/MySQL

**2. Sites statiques ou blogs simples**
- WordPress, Jekyll suffisent
- SEO crucial
- -> JAMstack ou CMS classique

**3. Applications legacy**
- Migration coûteuse
- Équipe sans compétences JS
- -> Garder stack existante

**4. Besoin de requêtes SQL complexes**
- Rapports complexes
- Jointures multiples
- -> PostgreSQL + Node.js

**5. Environnement Windows .NET**
- Infrastructure Microsoft
- Équipe C#/.NET
- -> ASP.NET Core

---

## [CONSTRUCTION] ARCHITECTURE DU PROJET

### Vue d'ensemble

```
┌─────────────────────────────────────────────────────┐
│                 ANGULAR 15                           │
│  • SPA (Single Page Application)                     │
│  • TypeScript                                        │
│  • Components, Services, Routing                     │
│  • RxJS (Observables)                                │
└────────────┬────────────────────────────────────────┘
             │ HTTP REST + WebSocket
             v
┌─────────────────────────────────────────────────────┐
│              EXPRESS.JS 4                            │
│  • API REST                                          │
│  • WebSocket (Socket.io)                             │
│  • Middleware (auth, CORS, etc.)                     │
│  • Routes                                            │
└────────────┬────────────────────────────────────────┘
             │
             v
┌─────────────────────────────────────────────────────┐
│               NODE.JS 18                             │
│  • Runtime JavaScript                                │
│  • Event-driven, non-blocking                        │
│  • Async/Await                                       │
└────────────┬────────────────────────────────────────┘
             │ Mongoose ODM
             v
┌─────────────────────────────────────────────────────┐
│               MONGODB 6                              │
│  • Base de données NoSQL                             │
│  • Documents JSON (BSON)                             │
│  • Collections : projects, tasks, users              │
└─────────────────────────────────────────────────────┘
```

### Architecture applicative

```
Frontend (Angular)
├── src/
│   ├── app/
│   │   ├── components/
│   │   │   ├── dashboard/
│   │   │   ├── projects/
│   │   │   ├── tasks/
│   │   │   └── auth/
│   │   ├── services/
│   │   │   ├── api.service.ts
│   │   │   ├── auth.service.ts
│   │   │   ├── socket.service.ts
│   │   │   └── task.service.ts
│   │   ├── models/
│   │   │   ├── project.model.ts
│   │   │   ├── task.model.ts
│   │   │   └── user.model.ts
│   │   └── guards/
│   │       └── auth.guard.ts
│   └── assets/

Backend (Express + Node.js)
├── server.js
├── models/
│   ├── User.js
│   ├── Project.js
│   └── Task.js
├── routes/
│   ├── auth.js
│   ├── projects.js
│   └── tasks.js
├── middleware/
│   ├── auth.js
│   └── validation.js
└── config/
    └── database.js
```

### Schéma MongoDB (Collections)

```javascript
// Collection : users
{
  _id: ObjectId("..."),
  name: "Fatou Sall",
  email: "fatou@example.com",
  password: "hash...",
  avatar: "url...",
  createdAt: ISODate("2024-01-10T...")
}

// Collection : projects
{
  _id: ObjectId("..."),
  title: "Refonte site web",
  description: "Moderniser le site corporate",
  owner: ObjectId("user_id"),     // Référence
  members: [                       // Array d'IDs
    ObjectId("user1"),
    ObjectId("user2")
  ],
  status: "active",
  createdAt: ISODate("...")
}

// Collection : tasks
{
  _id: ObjectId("..."),
  projectId: ObjectId("project_id"),
  title: "Créer maquettes",
  description: "Maquettes Figma pour 5 pages",
  status: "in-progress",           // todo, in-progress, done
  priority: "high",
  assignedTo: ObjectId("user_id"),
  dueDate: ISODate("2024-01-20"),
  tags: ["design", "ui/ux"],       // Array
  comments: [                       // Sous-documents
    {
      userId: ObjectId("..."),
      text: "Super travail !",
      createdAt: ISODate("...")
    }
  ],
  createdAt: ISODate("..."),
  updatedAt: ISODate("...")
}
```

---

## [CODE] INSTALLATION COMPLÈTE

### ÉTAPE 1 : Installer Node.js et npm

```bash
# NodeSource repository (version 18 LTS)
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -

# Installer Node.js
sudo apt install -y nodejs

# Vérifier
node --version  # v18.x.x
npm --version   # 9.x.x
```

---

### ÉTAPE 2 : Installer MongoDB

```bash
# Importer la clé GPG
curl -fsSL https://www.mongodb.org/static/pgp/server-6.0.asc | \
   sudo gpg -o /usr/share/keyrings/mongodb-server-6.0.gpg --dearmor

# Ajouter le repository
echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-6.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/6.0 multiverse" | \
   sudo tee /etc/apt/sources.list.d/mongodb-org-6.0.list

# Installer
sudo apt update
sudo apt install -y mongodb-org

# Démarrer
sudo systemctl start mongod
sudo systemctl enable mongod

# Vérifier
sudo systemctl status mongod
```

---

### ÉTAPE 3 : Installer Angular CLI

```bash
# Installer globalement
sudo npm install -g @angular/cli@15

# Vérifier
ng version
```

---

## [OUTIL] DÉVELOPPEMENT BACKEND (Express + Node.js)

### ÉTAPE 1 : Créer le projet backend

```bash
# Créer dossier
mkdir -p /var/www/taskapp/backend
cd /var/www/taskapp/backend

# Initialiser npm
npm init -y

# Installer dépendances
npm install express mongoose cors dotenv bcryptjs jsonwebtoken socket.io

# Installer dépendances de développement
npm install --save-dev nodemon
```

**Explication des packages :**

- **express** : Framework web minimaliste
- **mongoose** : ODM (Object Document Mapper) pour MongoDB
- **cors** : Gestion Cross-Origin Resource Sharing
- **dotenv** : Variables d'environnement
- **bcryptjs** : Hashage de mots de passe
- **jsonwebtoken** : Authentification JWT
- **socket.io** : WebSockets temps réel
- **nodemon** : Auto-restart en développement

---

### ÉTAPE 2 : Configuration (.env)

```bash
nano .env
```

```ini
# MongoDB
MONGODB_URI=mongodb://localhost:27017/taskapp

# JWT
JWT_SECRET=votre_cle_secrete_ultra_longue_et_complexe_123456
JWT_EXPIRY=7d

# Server
PORT=3000
NODE_ENV=development
```

---

### ÉTAPE 3 : Connexion MongoDB (config/database.js)

```bash
mkdir config
nano config/database.js
```

```javascript
const mongoose = require('mongoose');

const connectDB = async () => {
  try {
    await mongoose.connect(process.env.MONGODB_URI, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });
    
    console.log('[OK] MongoDB connecté');
    
    // Événements de connexion
    mongoose.connection.on('error', (err) => {
      console.error('[X] Erreur MongoDB:', err);
    });
    
    mongoose.connection.on('disconnected', () => {
      console.log('[ATTENTION]  MongoDB déconnecté');
    });
    
  } catch (error) {
    console.error('[X] Erreur connexion MongoDB:', error);
    process.exit(1);
  }
};

module.exports = connectDB;
```

---

### ÉTAPE 4 : Modèles Mongoose

```bash
mkdir models
```

**models/User.js :**

```javascript
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const userSchema = new mongoose.Schema({
  name: {
    type: String,
    required: [true, 'Le nom est requis'],
    trim: true
  },
  email: {
    type: String,
    required: [true, 'L\'email est requis'],
    unique: true,
    lowercase: true,
    trim: true,
    match: [/^\S+@\S+\.\S+$/, 'Email invalide']
  },
  password: {
    type: String,
    required: [true, 'Le mot de passe est requis'],
    minlength: [6, 'Minimum 6 caractères']
  },
  avatar: {
    type: String,
    default: 'https://ui-avatars.com/api/?name=User'
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

// Hash password avant sauvegarde
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  
  this.password = await bcrypt.hash(this.password, 10);
  next();
});

// Méthode pour comparer mot de passe
userSchema.methods.comparePassword = async function(candidatePassword) {
  return await bcrypt.compare(candidatePassword, this.password);
};

// Ne jamais renvoyer le mot de passe
userSchema.methods.toJSON = function() {
  const obj = this.toObject();
  delete obj.password;
  return obj;
};

module.exports = mongoose.model('User', userSchema);
```

**models/Project.js :**

```javascript
const mongoose = require('mongoose');

const projectSchema = new mongoose.Schema({
  title: {
    type: String,
    required: true,
    trim: true
  },
  description: {
    type: String,
    trim: true
  },
  owner: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
    required: true
  },
  members: [{
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User'
  }],
  status: {
    type: String,
    enum: ['active', 'archived', 'completed'],
    default: 'active'
  },
  createdAt: {
    type: Date,
    default: Date.now
  },
  updatedAt: {
    type: Date,
    default: Date.now
  }
});

// Mettre à jour updatedAt automatiquement
projectSchema.pre('save', function(next) {
  this.updatedAt = Date.now();
  next();
});

module.exports = mongoose.model('Project', projectSchema);
```

**models/Task.js :**

```javascript
const mongoose = require('mongoose');

const commentSchema = new mongoose.Schema({
  userId: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
    required: true
  },
  text: {
    type: String,
    required: true
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

const taskSchema = new mongoose.Schema({
  projectId: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Project',
    required: true
  },
  title: {
    type: String,
    required: true,
    trim: true
  },
  description: {
    type: String,
    trim: true
  },
  status: {
    type: String,
    enum: ['todo', 'in-progress', 'review', 'done'],
    default: 'todo'
  },
  priority: {
    type: String,
    enum: ['low', 'medium', 'high', 'urgent'],
    default: 'medium'
  },
  assignedTo: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User'
  },
  dueDate: {
    type: Date
  },
  tags: [{
    type: String,
    trim: true
  }],
  comments: [commentSchema],
  createdAt: {
    type: Date,
    default: Date.now
  },
  updatedAt: {
    type: Date,
    default: Date.now
  }
});

taskSchema.pre('save', function(next) {
  this.updatedAt = Date.now();
  next();
});

module.exports = mongoose.model('Task', taskSchema);
```

---

### ÉTAPE 5 : Middleware d'authentification

```bash
mkdir middleware
nano middleware/auth.js
```

```javascript
const jwt = require('jsonwebtoken');
const User = require('../models/User');

const auth = async (req, res, next) => {
  try {
    // Récupérer le token
    const token = req.header('Authorization')?.replace('Bearer ', '');
    
    if (!token) {
      return res.status(401).json({ error: 'Authentification requise' });
    }
    
    // Vérifier le token
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    
    // Trouver l'utilisateur
    const user = await User.findById(decoded.userId);
    
    if (!user) {
      return res.status(401).json({ error: 'Utilisateur non trouvé' });
    }
    
    // Ajouter à la requête
    req.user = user;
    req.userId = user._id;
    
    next();
  } catch (error) {
    res.status(401).json({ error: 'Token invalide' });
  }
};

module.exports = auth;
```

---

### ÉTAPE 6 : Routes API

**routes/auth.js :**

```javascript
const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken');
const User = require('../models/User');

// Inscription
router.post('/register', async (req, res) => {
  try {
    const { name, email, password } = req.body;
    
    // Vérifier si email existe
    const existing = await User.findOne({ email });
    if (existing) {
      return res.status(400).json({ error: 'Email déjà utilisé' });
    }
    
    // Créer utilisateur
    const user = new User({ name, email, password });
    await user.save();
    
    // Générer token
    const token = jwt.sign(
      { userId: user._id },
      process.env.JWT_SECRET,
      { expiresIn: process.env.JWT_EXPIRY }
    );
    
    res.status(201).json({ user, token });
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

// Connexion
router.post('/login', async (req, res) => {
  try {
    const { email, password } = req.body;
    
    const user = await User.findOne({ email });
    if (!user) {
      return res.status(401).json({ error: 'Identifiants incorrects' });
    }
    
    const isMatch = await user.comparePassword(password);
    if (!isMatch) {
      return res.status(401).json({ error: 'Identifiants incorrects' });
    }
    
    const token = jwt.sign(
      { userId: user._id },
      process.env.JWT_SECRET,
      { expiresIn: process.env.JWT_EXPIRY }
    );
    
    res.json({ user, token });
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

module.exports = router;
```

**routes/tasks.js :**

```javascript
const express = require('express');
const router = express.Router();
const Task = require('../models/Task');
const auth = require('../middleware/auth');

// Toutes les routes nécessitent authentification
router.use(auth);

// Créer tâche
router.post('/', async (req, res) => {
  try {
    const task = new Task({
      ...req.body,
      createdBy: req.userId
    });
    await task.save();
    
    // Émettre événement WebSocket
    req.io.emit('task:created', task);
    
    res.status(201).json(task);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

// Liste tâches d'un projet
router.get('/project/:projectId', async (req, res) => {
  try {
    const tasks = await Task.find({ projectId: req.params.projectId })
      .populate('assignedTo', 'name email avatar')
      .sort({ createdAt: -1 });
    
    res.json(tasks);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

// Mettre à jour tâche
router.patch('/:id', async (req, res) => {
  try {
    const task = await Task.findByIdAndUpdate(
      req.params.id,
      req.body,
      { new: true, runValidators: true }
    );
    
    if (!task) {
      return res.status(404).json({ error: 'Tâche non trouvée' });
    }
    
    // Émettre événement WebSocket
    req.io.emit('task:updated', task);
    
    res.json(task);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

// Ajouter commentaire
router.post('/:id/comments', async (req, res) => {
  try {
    const task = await Task.findById(req.params.id);
    
    if (!task) {
      return res.status(404).json({ error: 'Tâche non trouvée' });
    }
    
    task.comments.push({
      userId: req.userId,
      text: req.body.text
    });
    
    await task.save();
    
    req.io.emit('task:commented', { taskId: task._id, comment: task.comments[task.comments.length - 1] });
    
    res.json(task);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

module.exports = router;
```

---

### ÉTAPE 7 : Serveur principal (server.js)

```javascript
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const http = require('http');
const socketIO = require('socket.io');
const connectDB = require('./config/database');

// Connexion DB
connectDB();

// Express app
const app = express();
const server = http.createServer(app);

// Socket.io
const io = socketIO(server, {
  cors: {
    origin: 'http://localhost:4200', // Angular dev server
    methods: ['GET', 'POST', 'PATCH', 'DELETE']
  }
});

// Middleware
app.use(cors());
app.use(express.json());

// Passer io aux routes
app.use((req, res, next) => {
  req.io = io;
  next();
});

// Routes
app.use('/api/auth', require('./routes/auth'));
app.use('/api/projects', require('./routes/projects'));
app.use('/api/tasks', require('./routes/tasks'));

// WebSocket
io.on('connection', (socket) => {
  console.log('[OK] Client connecté:', socket.id);
  
  socket.on('disconnect', () => {
    console.log('[X] Client déconnecté:', socket.id);
  });
});

// Démarrer serveur
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`[RAPIDE] Serveur démarré sur port ${PORT}`);
});
```

---

### ÉTAPE 8 : Scripts npm (package.json)

```json
{
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  }
}
```

**Démarrer en mode dev :**

```bash
npm run dev
```

---

## [DESIGN] DÉVELOPPEMENT FRONTEND (Angular)

### ÉTAPE 1 : Créer projet Angular

```bash
cd /var/www/taskapp
ng new frontend --routing --style=scss

cd frontend
```

---

### ÉTAPE 2 : Installer dépendances

```bash
npm install socket.io-client
npm install @angular/material @angular/cdk
```

---

### ÉTAPE 3 : Service API

```bash
ng generate service services/api
```

**services/api.service.ts :**

```typescript
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class ApiService {
  private apiUrl = 'http://localhost:3000/api';
  
  constructor(private http: HttpClient) {}
  
  private getHeaders(): HttpHeaders {
    const token = localStorage.getItem('token');
    return new HttpHeaders({
      'Authorization': `Bearer ${token}`
    });
  }
  
  // Auth
  login(credentials: any): Observable<any> {
    return this.http.post(`${this.apiUrl}/auth/login`, credentials);
  }
  
  register(data: any): Observable<any> {
    return this.http.post(`${this.apiUrl}/auth/register`, data);
  }
  
  // Tasks
  getTasks(projectId: string): Observable<any> {
    return this.http.get(`${this.apiUrl}/tasks/project/${projectId}`, {
      headers: this.getHeaders()
    });
  }
  
  createTask(task: any): Observable<any> {
    return this.http.post(`${this.apiUrl}/tasks`, task, {
      headers: this.getHeaders()
    });
  }
  
  updateTask(id: string, updates: any): Observable<any> {
    return this.http.patch(`${this.apiUrl}/tasks/${id}`, updates, {
      headers: this.getHeaders()
    });
  }
}
```

---

### ÉTAPE 4 : Service WebSocket

```bash
ng generate service services/socket
```

**services/socket.service.ts :**

```typescript
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { io, Socket } from 'socket.io-client';

@Injectable({
  providedIn: 'root'
})
export class SocketService {
  private socket: Socket;
  
  constructor() {
    this.socket = io('http://localhost:3000');
  }
  
  listen(eventName: string): Observable<any> {
    return new Observable((subscriber) => {
      this.socket.on(eventName, (data: any) => {
        subscriber.next(data);
      });
    });
  }
  
  emit(eventName: string, data: any): void {
    this.socket.emit(eventName, data);
  }
}
```

---

### ÉTAPE 5 : Component tâches

```bash
ng generate component components/tasks
```

**components/tasks/tasks.component.ts :**

```typescript
import { Component, OnInit } from '@angular/core';
import { ApiService } from '../../services/api.service';
import { SocketService } from '../../services/socket.service';

@Component({
  selector: 'app-tasks',
  templateUrl: './tasks.component.html',
  styleUrls: ['./tasks.component.scss']
})
export class TasksComponent implements OnInit {
  tasks: any[] = [];
  projectId = '...'; // ID du projet actuel
  
  constructor(
    private api: ApiService,
    private socket: SocketService
  ) {}
  
  ngOnInit(): void {
    this.loadTasks();
    this.listenToUpdates();
  }
  
  loadTasks(): void {
    this.api.getTasks(this.projectId).subscribe(
      (tasks) => {
        this.tasks = tasks;
      },
      (error) => {
        console.error('Erreur chargement tâches:', error);
      }
    );
  }
  
  listenToUpdates(): void {
    // Écouter les mises à jour en temps réel
    this.socket.listen('task:created').subscribe((task) => {
      this.tasks.push(task);
    });
    
    this.socket.listen('task:updated').subscribe((updatedTask) => {
      const index = this.tasks.findIndex(t => t._id === updatedTask._id);
      if (index !== -1) {
        this.tasks[index] = updatedTask;
      }
    });
  }
  
  updateStatus(taskId: string, status: string): void {
    this.api.updateTask(taskId, { status }).subscribe(
      (updatedTask) => {
        // Mise à jour locale + WebSocket propagera aux autres clients
        const index = this.tasks.findIndex(t => t._id === taskId);
        if (index !== -1) {
          this.tasks[index] = updatedTask;
        }
      }
    );
  }
}
```

---

## [OK] TESTS DE VALIDATION

### Test 1 : MongoDB connecté

```bash
mongosh
```

```javascript
show dbs
use taskapp
show collections
```

---

### Test 2 : API Backend

```bash
# Inscription
curl -X POST http://localhost:3000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Moussa Diop",
    "email": "moussa@example.com",
    "password": "password123"
  }'
```

---

### Test 3 : WebSocket

**Dans la console navigateur Angular :**

```javascript
import { io } from 'socket.io-client';
const socket = io('http://localhost:3000');

socket.on('connect', () => {
  console.log('Connecté !');
});

socket.on('task:created', (task) => {
  console.log('Nouvelle tâche:', task);
});
```

---

## [IMPORTANT] POINTS CLÉS MEAN

**1. JavaScript partout**
- Une seule langue
- Code partagé
- Équipe polyvalente

**2. Temps réel**
- WebSockets natifs
- Collaboration live
- Event-driven

**3. NoSQL flexible**
- Schéma évolutif
- Performances
- Scalabilité

---

## [COURS] CONCLUSION EXERCICE MEAN

**[OK] Tu as créé une app collaborative temps réel avec MEAN !**

**Compétences acquises :**
- Node.js + Express
- MongoDB + Mongoose
- Angular + TypeScript
- WebSockets (Socket.io)
- API REST moderne
- Architecture SPA

**Modernité :** Stack JavaScript full-stack 2024

**FIN DES 4 EXERCICES ! [BRAVO]**

# [ROUGE] EXERCICE 5 : DÉPLOIEMENT LAMP EN PRODUCTION (ULTRA-DÉTAILLÉ)

## [LISTE] ÉNONCÉ

### Contexte professionnel

Le client de l'exercice 1 (boutique artisanat) est ravi de son site e-commerce. Il souhaite maintenant le **mettre en ligne en production** pour que le monde entier puisse commander ses produits.

**Tu es chargé de :**
1. Louer et configurer un serveur VPS
2. Acheter et configurer un nom de domaine
3. Déployer l'application en production
4. Sécuriser complètement le serveur
5. Configurer SSL/HTTPS avec Let's Encrypt
6. Optimiser les performances
7. Mettre en place monitoring et backups
8. Documenter le tout pour l'équipe

**Budget :** 10-20€/mois (hébergement accessible)

**Objectif :** Site 100% fonctionnel, sécurisé, rapide, accessible 24/7

---

## [OBJECTIF] POURQUOI CET EXERCICE ?

### [OK] Compétences professionnelles réelles

**1. DevOps / SysAdmin**
- Configuration serveur de A à Z
- Sécurisation production
- Monitoring et maintenance
- Gestion des incidents

**2. Infrastructures cloud**
- VPS (Virtual Private Server)
- DNS et domaines
- Réseaux et pare-feu
- CDN et performance

**3. Sécurité**
- Hardening Linux
- SSL/TLS configuration
- Pare-feu (UFW)
- Fail2Ban anti-brute-force
- Backups automatisés

**4. Performance**
- Optimisation Apache
- Cache (Varnish, Redis)
- Compression
- CDN

**5. Professionnalisme**
- Documentation technique
- Procédures de déploiement
- Plan de reprise d'activité (PRA)
- Monitoring 24/7

### [PRO] Valeur sur le marché

Ces compétences sont **extrêmement demandées** :
- Salaire DevOps Junior : 35-50K€/an
- Salaire DevOps Senior : 60-90K€/an
- Freelance : 400-800€/jour

**Cet exercice = Expérience professionnelle concrète**

---

## [CONSTRUCTION] ARCHITECTURE COMPLÈTE

### Vue d'ensemble

```
┌─────────────────────────────────────────────────────────┐
│                    INTERNET                              │
│  • Utilisateurs du monde entier                          │
└────────────┬────────────────────────────────────────────┘
             │
             v DNS (artisanat-senegal.com)
┌─────────────────────────────────────────────────────────┐
│              CLOUDFLARE CDN (Optionnel)                  │
│  • Cache global                                          │
│  • Protection DDoS                                       │
│  • SSL universel                                         │
└────────────┬────────────────────────────────────────────┘
             │ HTTPS
             v
┌─────────────────────────────────────────────────────────┐
│              VPS (CONTABO / OVH / DIGITAL OCEAN)         │
│  • IP publique fixe : 45.XXX.XXX.XXX                     │
│  • OS : Ubuntu 22.04 LTS                                 │
│  • RAM : 2GB minimum                                     │
│  • Stockage : 50GB SSD                                   │
│  • CPU : 2 vCores                                        │
└────────────┬────────────────────────────────────────────┘
             │
    ┌────────┴────────┐
    │                 │
    v                 v
┌─────────┐      ┌─────────────┐
│  UFW    │      │  Fail2Ban   │
│ Firewall│      │ Anti-brute  │
└────┬────┘      └─────────────┘
     │
     v Port 443 (HTTPS) + 80 (HTTP->HTTPS)
┌─────────────────────────────────────────────────────────┐
│                   NGINX (Reverse Proxy)                  │
│  • SSL Termination (Let's Encrypt)                       │
│  • Compression GZIP/Brotli                               │
│  • Cache HTTP                                            │
│  • Load balancing (si nécessaire)                        │
└────────────┬────────────────────────────────────────────┘
             │
             v HTTP local
┌─────────────────────────────────────────────────────────┐
│                   APACHE 2.4                             │
│  • Application PHP                                       │
│  • Virtual Hosts                                         │
│  • ModSecurity (WAF)                                     │
└────────────┬────────────────────────────────────────────┘
             │
             v PHP-FPM
┌─────────────────────────────────────────────────────────┐
│                   PHP 8.1                                │
│  • Application e-commerce                                │
│  • OPcache activé                                        │
└────────────┬────────────────────────────────────────────┘
             │
             v PDO
┌─────────────────────────────────────────────────────────┐
│                   MySQL 8.0                              │
│  • Base de données shop_db                               │
│  • Backups quotidiens                                    │
└─────────────────────────────────────────────────────────┘
```

### Sécurité en couches (Defense in Depth)

```
Couche 1: Cloudflare (DDoS, WAF cloud)
Couche 2: UFW Firewall (Ports ouverts: 22, 80, 443 uniquement)
Couche 3: Fail2Ban (Bannissement auto brute-force)
Couche 4: Nginx (Rate limiting, SSL)
Couche 5: ModSecurity (WAF applicatif)
Couche 6: Application (Input validation, prepared statements)
Couche 7: OS Hardening (Updates auto, permissions strictes)
```

---

## [CODE] PARTIE 1 : CHOIX ET LOCATION DU SERVEUR

### ÉTAPE 1 : Choisir un hébergeur VPS

**Comparatif hébergeurs (Prix/Performances) :**

| Hébergeur | Prix/mois | RAM | SSD | CPU | Bande passante | Localisation |
|-----------|-----------|-----|-----|-----|----------------|--------------|
| **Contabo** | 4,99€ | 4GB | 200GB | 2 | Illimitée | Allemagne, UK, US |
| **OVH VPS** | 7,00€ | 2GB | 40GB | 1 | 100 Mbps | France, Canada |
| **DigitalOcean** | 12$/mois | 2GB | 50GB | 1 | 2TB | USA, Europe, Asie |
| **Vultr** | 10$/mois | 2GB | 55GB | 1 | 2TB | 25 data centers |
| **Hetzner** | 4,90€ | 4GB | 40GB | 2 | 20TB | Allemagne, Finlande |

**[TROPHEE] Recommandation pour débutant :**

**CONTABO** - Meilleur rapport qualité/prix
- 4,99€/mois = café/mois
- 4GB RAM (confortable)
- Support correct
- Datacenter Europe

**OU**

**DigitalOcean** - Interface la plus simple
- Documentation excellente
- Dashboard intuitif
- Tutoriels nombreux
- Plus cher mais "beginner-friendly"

---

### ÉTAPE 2 : Commander le VPS (Exemple Contabo)

**A. Aller sur https://contabo.com**

**B. Choisir "VPS S SSD"**
- RAM : 4 GB
- Stockage : 200 GB SSD
- Prix : 4,99€/mois
- Localisation : Nürnberg (Allemagne - proche Afrique)

**C. Configuration :**

```
Operating System: Ubuntu 22.04 LTS
Hostname: shop-prod-01
Root Password: [GÉNÉRER MOT DE PASSE FORT - Noter dans gestionnaire]

Options :
[x] Managed Service : NON (on fait tout nous-mêmes)
[ ] Backup Space : OUI (5€/mois - 100GB - IMPORTANT !)
```

**D. Valider et payer**

Délai : **15 minutes à 24h** pour activation

**E. Récupérer les informations**

Tu recevras par email :
```
IP Address: 45.XXX.XXX.XXX
Username: root
Password: [celui que tu as défini]
```

**[ATTENTION] IMPORTANT : Noter ces informations dans un gestionnaire de mots de passe (KeePass, Bitwarden)**

---

### ÉTAPE 3 : Première connexion SSH

**Depuis ton PC (Linux/Mac) :**

```bash
ssh root@45.XXX.XXX.XXX
```

**Depuis Windows :**

Télécharger **PuTTY** : https://putty.org

```
Host Name: 45.XXX.XXX.XXX
Port: 22
Connection Type: SSH
```

**Première connexion - Vérifications :**

```bash
# Vérifier l'OS
lsb_release -a
# Résultat : Ubuntu 22.04 LTS

# Vérifier les ressources
free -h
# RAM : 4GB

df -h
# Disque : 200GB

nproc
# CPU : 2 cores
```

**[OK] Si tout OK, on continue !**

---

## [SECURISE] PARTIE 2 : SÉCURISATION INITIALE DU SERVEUR

### ÉTAPE 1 : Mettre à jour le système

```bash
# Mise à jour de la liste des paquets
apt update

# Upgrade de tous les paquets
apt upgrade -y

# Nettoyage
apt autoremove -y
apt autoclean
```

**[TEMPS] Durée : 5-10 minutes**

---

### ÉTAPE 2 : Créer un utilisateur non-root

**[ATTENTION] RÈGLE D'OR : NE JAMAIS travailler en root en production !**

```bash
# Créer utilisateur
adduser deploy

# Exemple de réponses :
# Password: [Mot de passe fort]
# Full Name: Deployer User
# [Entrée pour le reste]
```

**Ajouter aux sudoers (droits admin) :**

```bash
usermod -aG sudo deploy
```

**Vérifier :**

```bash
# Passer à l'utilisateur deploy
su - deploy

# Tester sudo
sudo ls /root
# Doit demander le mot de passe puis afficher le contenu
```

---

### ÉTAPE 3 : Configurer l'authentification par clé SSH

**Sur ton PC (pas sur le serveur) :**

```bash
# Générer une paire de clés SSH
ssh-keygen -t ed25519 -C "votre-email@example.com"

# Appuyer sur Entrée 3 fois (emplacement par défaut, pas de passphrase)
# Fichiers créés :
# ~/.ssh/id_ed25519 (clé PRIVÉE - ne JAMAIS partager)
# ~/.ssh/id_ed25519.pub (clé PUBLIQUE - à copier sur le serveur)
```

**Copier la clé publique sur le serveur :**

```bash
# Méthode 1 : ssh-copy-id (recommandé)
ssh-copy-id deploy@45.XXX.XXX.XXX

# Méthode 2 : Manuellement
cat ~/.ssh/id_ed25519.pub
# Copier le résultat (commence par "ssh-ed25519 AAA...")
```

**Sur le serveur (en tant que deploy) :**

```bash
mkdir -p ~/.ssh
chmod 700 ~/.ssh
nano ~/.ssh/authorized_keys
# Coller la clé publique
# Sauvegarder (Ctrl+O, Entrée, Ctrl+X)

chmod 600 ~/.ssh/authorized_keys
```

**Tester la connexion par clé :**

```bash
# Depuis ton PC
exit  # Déconnecter
ssh deploy@45.XXX.XXX.XXX
# Doit se connecter SANS demander de mot de passe
```

**[OK] Si ça fonctionne, on peut désactiver l'auth par mot de passe**

---

### ÉTAPE 4 : Durcir SSH (Hardening)

```bash
sudo nano /etc/ssh/sshd_config
```

**Modifications à faire :**

```bash
# Désactiver connexion root
PermitRootLogin no

# Désactiver authentification par mot de passe
PasswordAuthentication no

# Désactiver authentification par challenge-response
ChallengeResponseAuthentication no

# Désactiver X11 forwarding (GUI)
X11Forwarding no

# Limiter les tentatives de connexion
MaxAuthTries 3

# Timeout si inactivité
ClientAliveInterval 300
ClientAliveCountMax 2

# Changer le port SSH (optionnel mais recommandé)
# Port 2222
# [ATTENTION] Si tu changes le port, pense à l'autoriser dans UFW !
```

**Redémarrer SSH :**

```bash
sudo systemctl restart ssh
```

**[ATTENTION] NE PAS fermer la session actuelle !**

**Ouvrir une NOUVELLE fenêtre terminal et tester :**

```bash
ssh deploy@45.XXX.XXX.XXX
# Si port changé :
# ssh deploy@45.XXX.XXX.XXX -p 2222
```

**Si ça fonctionne, fermer l'ancienne session.**

**Si ça ne fonctionne PAS, garder l'ancienne session et corriger.**

---

### ÉTAPE 5 : Configurer le pare-feu UFW

```bash
# Installer UFW (normalement déjà installé)
sudo apt install ufw -y

# Politiques par défaut
sudo ufw default deny incoming   # Bloquer tout ce qui entre
sudo ufw default allow outgoing  # Autoriser tout ce qui sort

# Autoriser SSH (IMPORTANT - sinon tu te bloques !)
sudo ufw allow 22/tcp
# OU si port SSH changé :
# sudo ufw allow 2222/tcp

# Autoriser HTTP et HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# Activer UFW
sudo ufw enable
# Confirmer : y

# Vérifier le statut
sudo ufw status verbose
```

**Résultat attendu :**

```
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       Anywhere
80/tcp                     ALLOW       Anywhere
443/tcp                    ALLOW       Anywhere
```

---

### ÉTAPE 6 : Installer et configurer Fail2Ban

**Fail2Ban = Bannit automatiquement les IPs qui font trop de tentatives échouées**

```bash
# Installer
sudo apt install fail2ban -y

# Créer configuration locale
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local
```

**Modifications importantes :**

```ini
[DEFAULT]
# Durée de bannissement (10 minutes)
bantime = 600

# Fenêtre de temps pour compter les échecs (10 minutes)
findtime = 600

# Nombre d'échecs avant ban
maxretry = 3

# Email d'alerte (optionnel)
destemail = votre-email@example.com
sendername = Fail2Ban-VPS
action = %(action_mwl)s

[sshd]
enabled = true
port = 22
# OU si SSH sur port custom :
# port = 2222
maxretry = 3
```

**Démarrer Fail2Ban :**

```bash
sudo systemctl start fail2ban
sudo systemctl enable fail2ban
sudo systemctl status fail2ban
```

**Tester Fail2Ban :**

```bash
# Voir les jails actives
sudo fail2ban-client status

# Voir les détails SSH
sudo fail2ban-client status sshd
```

---

### ÉTAPE 7 : Activer les mises à jour automatiques

**[ATTENTION] Crucial pour la sécurité !**

```bash
# Installer
sudo apt install unattended-upgrades -y

# Configurer
sudo dpkg-reconfigure -plow unattended-upgrades
# Choisir : Yes
```

**Configuration avancée :**

```bash
sudo nano /etc/apt/apt.conf.d/50unattended-upgrades
```

**Décommenter/modifier :**

```
Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";
    "${distro_id}ESMApps:${distro_codename}-apps-security";
};

Unattended-Upgrade::AutoFixInterruptedDpkg "true";
Unattended-Upgrade::MinimalSteps "true";
Unattended-Upgrade::Mail "votre-email@example.com";
Unattended-Upgrade::Automatic-Reboot "false";
```

**Tester :**

```bash
sudo unattended-upgrade --dry-run --debug
```

---

## [WEB] PARTIE 3 : CONFIGURATION DU NOM DE DOMAINE

### ÉTAPE 1 : Acheter un nom de domaine

**Registrars recommandés :**

| Registrar | Prix .com/an | Prix .sn/an | Interface | Support |
|-----------|--------------|-------------|-----------|---------|
| **Namecheap** | 10€ | N/A | ***** | **** |
| **OVH** | 12€ | 15 000 FCFA | **** | **** |
| **Gandi** | 15€ | N/A | **** | ***** |
| **NIC Sénégal** | N/A | 10 000 FCFA | *** | *** |

**Pour cet exercice, prenons :** `artisanat-senegal.com`

**Acheter sur Namecheap :**

1. Aller sur https://www.namecheap.com
2. Rechercher : artisanat-senegal.com
3. Ajouter au panier
4. Options :
   - [ ] WhoisGuard : OUI (masque tes infos personnelles)
   - [ ] Auto-renew : OUI
5. Payer (~10€/an)

---

### ÉTAPE 2 : Configurer les DNS

**Option 1 : Cloudflare (Recommandé - Gratuit)**

**Pourquoi Cloudflare ?**
- [OK] DNS ultra-rapide
- [OK] CDN gratuit
- [OK] Protection DDoS
- [OK] SSL universel
- [OK] Cache automatique
- [OK] Dashboard simple

**A. Créer compte Cloudflare**

1. Aller sur https://www.cloudflare.com
2. S'inscrire (gratuit)
3. Ajouter un site : artisanat-senegal.com

**B. Scanner les enregistrements DNS**

Cloudflare va détecter tes enregistrements existants.

**C. Modifier les nameservers chez Namecheap**

Cloudflare va te donner 2 nameservers :
```
adam.ns.cloudflare.com
lisa.ns.cloudflare.com
```

Sur Namecheap :
1. Dashboard -> Manage domain
2. Nameservers -> Custom DNS
3. Entrer les 2 nameservers Cloudflare
4. Sauvegarder

[TEMPS] **Propagation : 2-24 heures**

**D. Créer les enregistrements DNS dans Cloudflare**

Dans Cloudflare Dashboard -> DNS -> Records :

```
Type    Name    Content (IP)         Proxy   TTL
────────────────────────────────────────────────────
A       @       45.XXX.XXX.XXX       [OK]      Auto
A       www     45.XXX.XXX.XXX       [OK]      Auto
CNAME   shop    artisanat-senegal.com [OK]     Auto
```

**Explications :**

- **@ (root)** : artisanat-senegal.com -> IP serveur
- **www** : www.artisanat-senegal.com -> IP serveur
- **shop** : shop.artisanat-senegal.com -> domaine principal
- **Proxy [CLOUD]** : Trafic passe par Cloudflare (CDN + protection)

**E. Vérifier la propagation DNS**

```bash
# Depuis ton PC
dig artisanat-senegal.com

# OU
nslookup artisanat-senegal.com

# Attendre que ça pointe vers ton IP
```

**Outil en ligne :** https://dnschecker.org

---

## [PACKAGE] PARTIE 4 : INSTALLATION STACK LAMP

### ÉTAPE 1 : Installer Apache

```bash
sudo apt update
sudo apt install apache2 -y

# Vérifier
sudo systemctl status apache2

# Tester
curl http://localhost
# Doit afficher la page par défaut Apache
```

**Tester depuis navigateur :**

```
http://45.XXX.XXX.XXX
```

**Page "Apache2 Ubuntu Default Page" doit s'afficher.**

---

### ÉTAPE 2 : Installer MySQL

```bash
sudo apt install mysql-server -y

# Sécuriser
sudo mysql_secure_installation
```

**Réponses :**

```
Validate password component? Y
Password level: 2 (Strong)
Root password: [GÉNÉRER ET NOTER]
Remove anonymous users? Y
Disallow root login remotely? Y
Remove test database? Y
Reload privilege tables? Y
```

---

### ÉTAPE 3 : Installer PHP

```bash
sudo apt install php8.1 php8.1-fpm php8.1-mysql php8.1-mbstring \
  php8.1-xml php8.1-curl php8.1-gd php8.1-zip php8.1-intl \
  php8.1-opcache -y

# Configurer Apache pour PHP-FPM
sudo a2enmod proxy_fcgi setenvif
sudo a2enconf php8.1-fpm

# Redémarrer
sudo systemctl restart apache2
sudo systemctl restart php8.1-fpm
```

---

### ÉTAPE 4 : Optimiser PHP pour production

```bash
sudo nano /etc/php/8.1/fpm/php.ini
```

**Modifications importantes :**

```ini
; Sécurité
expose_php = Off
display_errors = Off
log_errors = On
error_log = /var/log/php/error.log

; Limites
memory_limit = 256M
upload_max_filesize = 10M
post_max_size = 10M
max_execution_time = 30

; OPcache (CRUCIAL pour performances)
opcache.enable = 1
opcache.memory_consumption = 128
opcache.interned_strings_buffer = 8
opcache.max_accelerated_files = 10000
opcache.revalidate_freq = 60
opcache.fast_shutdown = 1

; Session
session.cookie_httponly = 1
session.cookie_secure = 1
session.use_strict_mode = 1
```

**Créer dossier logs :**

```bash
sudo mkdir -p /var/log/php
sudo chown www-data:www-data /var/log/php
```

**Redémarrer PHP-FPM :**

```bash
sudo systemctl restart php8.1-fpm
```

---

## [RAPIDE] PARTIE 5 : DÉPLOIEMENT DE L'APPLICATION

### ÉTAPE 1 : Préparer les fichiers localement

**Sur ton PC, depuis le projet de l'exercice 1 :**

```bash
cd /var/www/shop

# Créer archive
tar -czf shop.tar.gz .

# OU utiliser Git (recommandé)
git init
git add .
git commit -m "Initial commit"

# Créer repo sur GitHub/GitLab
git remote add origin https://github.com/ton-user/shop.git
git push -u origin main
```

---

### ÉTAPE 2 : Transférer sur le serveur

**Méthode 1 : SCP (si archive) :**

```bash
# Depuis ton PC
scp shop.tar.gz deploy@45.XXX.XXX.XXX:/home/deploy/
```

**Méthode 2 : Git (recommandé) :**

```bash
# Sur le serveur
cd /var/www
sudo mkdir shop
sudo chown deploy:deploy shop
cd shop

git clone https://github.com/ton-user/shop.git .
```

---

### ÉTAPE 3 : Configurer la base de données

```bash
# Sur le serveur
sudo mysql -u root -p
```

```sql
-- Créer la base
CREATE DATABASE shop_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- Créer utilisateur
CREATE USER 'shop_user'@'localhost' IDENTIFIED BY 'MotDePasseProdSecure123!';
GRANT ALL PRIVILEGES ON shop_db.* TO 'shop_user'@'localhost';
FLUSH PRIVILEGES;

-- Importer le schéma (si fichier SQL)
USE shop_db;
SOURCE /home/deploy/shop_schema.sql;

EXIT;
```

**OU créer les tables manuellement (reprendre exercice 1).**

---

### ÉTAPE 4 : Configurer l'application

```bash
cd /var/www/shop

# Éditer config
sudo nano config/database.php
```

**Mettre les bonnes credentials :**

```php
define('DB_HOST', 'localhost');
define('DB_NAME', 'shop_db');
define('DB_USER', 'shop_user');
define('DB_PASS', 'MotDePasseProdSecure123!');
```

---

### ÉTAPE 5 : Permissions correctes

```bash
# Propriétaire
sudo chown -R www-data:www-data /var/www/shop

# Permissions
sudo find /var/www/shop -type d -exec chmod 755 {} \;
sudo find /var/www/shop -type f -exec chmod 644 {} \;

# Fichiers sensibles
sudo chmod 640 /var/www/shop/config/database.php
```

---

### ÉTAPE 6 : Virtual Host Apache

```bash
sudo nano /etc/apache2/sites-available/artisanat-senegal.conf
```

```apache
<VirtualHost *:80>
    ServerName artisanat-senegal.com
    ServerAlias www.artisanat-senegal.com
    ServerAdmin contact@artisanat-senegal.com
    
    DocumentRoot /var/www/shop
    
    <Directory /var/www/shop>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
        DirectoryIndex index.php index.html
    </Directory>
    
    # Logs
    ErrorLog ${APACHE_LOG_DIR}/shop-error.log
    CustomLog ${APACHE_LOG_DIR}/shop-access.log combined
    
    # Sécurité
    <FilesMatch "^\.">
        Require all denied
    </FilesMatch>
    
    <Directory /var/www/shop/config>
        Require all denied
    </Directory>
</VirtualHost>
```

**Activer :**

```bash
sudo a2ensite artisanat-senegal.conf
sudo a2dissite 000-default.conf
sudo systemctl reload apache2
```

**Tester :**

```
http://artisanat-senegal.com
```

**[OK] Le site doit s'afficher !**

---

## [VERROUILLE] PARTIE 6 : HTTPS AVEC LET'S ENCRYPT

### ÉTAPE 1 : Installer Certbot

```bash
sudo apt install certbot python3-certbot-apache -y
```

---

### ÉTAPE 2 : Obtenir certificat SSL

```bash
sudo certbot --apache -d artisanat-senegal.com -d www.artisanat-senegal.com
```

**Réponses :**

```
Email: votre-email@example.com
Terms of Service: A (Agree)
Share email: N (No)
Redirect HTTP to HTTPS: 2 (Yes - TOUJOURS)
```

**Certbot va :**
1. Vérifier que tu contrôles le domaine
2. Générer le certificat
3. Configurer Apache automatiquement
4. Créer un cron pour renouvellement auto

**[TEMPS] Durée : 1-2 minutes**

**[OK] Résultat :**

```
Congratulations! You have successfully enabled HTTPS!
```

---

### ÉTAPE 3 : Vérifier HTTPS

```
https://artisanat-senegal.com
```

**Cadenas vert doit apparaître ! [VERROUILLE][OK]**

---

### ÉTAPE 4 : Configuration SSL avancée

```bash
sudo nano /etc/letsencrypt/options-ssl-apache.conf
```

**Optimisations :**

```apache
SSLProtocol all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
SSLHonorCipherOrder on
SSLCompression off
SSLSessionTickets off

# OCSP Stapling
SSLUseStapling on
SSLStaplingCache "shmcb:logs/stapling-cache(150000)"
```

**Redémarrer Apache :**

```bash
sudo systemctl restart apache2
```

---

### ÉTAPE 5 : Tester la configuration SSL

**Aller sur : https://www.ssllabs.com/ssltest/**

Entrer : `artisanat-senegal.com`

**[OBJECTIF] Objectif : Note A ou A+**

---

## [RAPIDE] PARTIE 7 : OPTIMISATION PERFORMANCES

### ÉTAPE 1 : Activer la compression

```bash
sudo a2enmod deflate
sudo a2enmod headers
```

**Configurer :**

```bash
sudo nano /etc/apache2/mods-enabled/deflate.conf
```

```apache
<IfModule mod_deflate.c>
    AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css
    AddOutputFilterByType DEFLATE application/javascript application/x-javascript
    AddOutputFilterByType DEFLATE application/json
    AddOutputFilterByType DEFLATE application/xml
</IfModule>
```

---

### ÉTAPE 2 : Activer le cache navigateur

```bash
sudo a2enmod expires
```

**Configurer :**

```bash
sudo nano /etc/apache2/sites-enabled/artisanat-senegal-le-ssl.conf
```

**Ajouter dans `<VirtualHost>` :**

```apache
<IfModule mod_expires.c>
    ExpiresActive On
    ExpiresByType image/jpg "access plus 1 year"
    ExpiresByType image/jpeg "access plus 1 year"
    ExpiresByType image/gif "access plus 1 year"
    ExpiresByType image/png "access plus 1 year"
    ExpiresByType text/css "access plus 1 month"
    ExpiresByType application/pdf "access plus 1 month"
    ExpiresByType application/javascript "access plus 1 month"
    ExpiresByType application/x-javascript "access plus 1 month"
    ExpiresByType image/x-icon "access plus 1 year"
    ExpiresDefault "access plus 2 days"
</IfModule>
```

**Redémarrer :**

```bash
sudo systemctl restart apache2
```

---

### ÉTAPE 3 : Configurer OPcache

**Vérifier que OPcache est activé :**

```bash
php -i | grep opcache
```

**Dashboard OPcache (optionnel) :**

```bash
cd /var/www/shop
sudo wget https://raw.githubusercontent.com/rlerdorf/opcache-status/master/opcache.php
sudo mv opcache.php opcache-status.php
sudo chmod 644 opcache-status.php
```

**Protéger par IP :**

```bash
sudo nano /etc/apache2/sites-enabled/artisanat-senegal-le-ssl.conf
```

```apache
<Files "opcache-status.php">
    Require ip TON_IP_PUBLIQUE
</Files>
```

**Accéder :**

```
https://artisanat-senegal.com/opcache-status.php
```

---

## [GRAPHIQUE] PARTIE 8 : MONITORING ET BACKUPS

### ÉTAPE 1 : Installer monitoring (Netdata)

**Netdata = Dashboard temps réel gratuit**

```bash
# Installation automatique
bash <(curl -Ss https://my-netdata.io/kickstart.sh)
```

**Sécuriser l'accès :**

```bash
sudo nano /etc/netdata/netdata.conf
```

```ini
[web]
    bind to = 127.0.0.1
```

**Créer reverse proxy Nginx pour accès sécurisé :**

```bash
sudo apt install nginx -y

sudo nano /etc/nginx/sites-available/netdata
```

```nginx
server {
    listen 80;
    server_name monitoring.artisanat-senegal.com;
    
    location / {
        proxy_pass http://127.0.0.1:19999;
        proxy_set_header Host $host;
    }
}
```

**Activer :**

```bash
sudo ln -s /etc/nginx/sites-available/netdata /etc/nginx/sites-enabled/
sudo systemctl restart nginx

# Obtenir certificat SSL
sudo certbot --nginx -d monitoring.artisanat-senegal.com
```

**Accéder :**

```
https://monitoring.artisanat-senegal.com
```

---

### ÉTAPE 2 : Backups automatiques

**Script de backup :**

```bash
sudo nano /root/backup-shop.sh
```

```bash
#!/bin/bash
# ═══════════════════════════════════════════════════════════════
# SCRIPT DE BACKUP AUTOMATIQUE
# ═══════════════════════════════════════════════════════════════

# Variables
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/root/backups"
DB_NAME="shop_db"
DB_USER="shop_user"
DB_PASS="MotDePasseProdSecure123!"
WEB_DIR="/var/www/shop"

# Créer dossier backup
mkdir -p $BACKUP_DIR

# Backup base de données
mysqldump -u $DB_USER -p$DB_PASS $DB_NAME | gzip > $BACKUP_DIR/db_$DATE.sql.gz

# Backup fichiers
tar -czf $BACKUP_DIR/files_$DATE.tar.gz $WEB_DIR

# Garder seulement les 30 derniers jours
find $BACKUP_DIR -name "*.gz" -mtime +30 -delete

# Log
echo "[$DATE] Backup terminé" >> /var/log/backup.log
```

**Rendre exécutable :**

```bash
sudo chmod +x /root/backup-shop.sh
```

**Tester :**

```bash
sudo /root/backup-shop.sh
ls /root/backups/
```

**Automatiser avec cron (tous les jours à 3h) :**

```bash
sudo crontab -e
```

**Ajouter :**

```
0 3 * * * /root/backup-shop.sh
```

---

### ÉTAPE 3 : Backup externe (optionnel mais recommandé)

**Installer rclone (sync vers cloud) :**

```bash
curl https://rclone.org/install.sh | sudo bash
```

**Configurer Google Drive :**

```bash
rclone config
# Suivre les instructions pour connecter Google Drive
```

**Ajouter au script backup :**

```bash
# À la fin de /root/backup-shop.sh
rclone copy $BACKUP_DIR gdrive:shop-backups
```

---

## [LISTE] PARTIE 9 : DOCUMENTATION FINALE

### Checklist production

```
[x] Serveur VPS loué et configuré
[x] Utilisateur non-root créé
[x] SSH durci (clé uniquement, pas de root)
[x] Pare-feu UFW activé
[x] Fail2Ban configuré
[x] Mises à jour auto activées
[x] Domaine acheté et DNS configurés
[x] Stack LAMP installée
[x] Application déployée
[x] Base de données créée et sécurisée
[x] HTTPS activé (Let's Encrypt)
[x] Certificat SSL grade A+
[x] Compression activée
[x] Cache navigateur configuré
[x] OPcache activé
[x] Monitoring installé (Netdata)
[x] Backups automatiques configurés
[x] Backups externes configurés
```

---

### Commandes de maintenance

```bash
# Vérifier statut services
sudo systemctl status apache2
sudo systemctl status mysql
sudo systemctl status php8.1-fpm

# Logs
sudo tail -f /var/log/apache2/shop-error.log
sudo tail -f /var/log/mysql/error.log
sudo tail -f /var/log/php/error.log

# Espace disque
df -h

# RAM utilisée
free -h

# Top processus
htop

# Connexions actives
sudo netstat -tulpn

# Backups
ls -lh /root/backups/

# Renouvellement SSL (auto, mais pour tester)
sudo certbot renew --dry-run
```

---

## [COURS] CONCLUSION EXERCICE 5

**[BRAVO] FÉLICITATIONS ! Tu as déployé un site LAMP en production !**

**Compétences acquises :**
- [OK] Location et configuration VPS
- [OK] Sécurisation serveur Linux
- [OK] Gestion DNS et domaines
- [OK] Déploiement applications web
- [OK] Configuration HTTPS/SSL
- [OK] Optimisation performances
- [OK] Monitoring temps réel
- [OK] Stratégie de backups
- [OK] Maintenance production

**[PRO] Valeur professionnelle :** Compétences DevOps niveau junior-intermédiaire

**[TEMPS] Temps total :** 6-8 heures (incluant attentes DNS/SSL)

**[RAPIDE] Prochaines étapes :**
1. Ajouter un CDN (Cloudflare déjà configuré)
2. Mettre en place CI/CD (GitHub Actions)
3. Ajouter tests automatisés
4. Configurer alertes (PagerDuty, OpsGenie)
5. Implémenter haute disponibilité (Load Balancer)

**Tu es maintenant capable de gérer un serveur de production de A à Z ! [FORCE]**

# [BLEU] EXERCICE 6 : LAMP + WORDPRESS - SITE PROFESSIONNEL COMPLET

## [LISTE] ÉNONCÉ

### Contexte professionnel

Une **ONG sénégalaise de développement communautaire** te contacte pour créer leur site web institutionnel. Ils veulent :

**Fonctionnalités requises :**
- Site vitrine professionnel
- Blog pour actualités et projets
- Portfolio des réalisations (photos/vidéos)
- Formulaire de contact et don
- Multilingue (Français/Anglais)
- Newsletter
- Espace membres (bénévoles)
- Boutique simple (merchandising)

**Contraintes :**
- **Budget limité** : Solution open-source
- **Pas de développeurs** dans l'équipe : Interface simple pour mise à jour
- **Design moderne** : Représentatif et professionnel
- **SEO optimisé** : Visibilité Google
- **Performance** : Chargement rapide même en Afrique
- **Sécurité** : Protection contre piratage

**Solution retenue :** **WordPress** (42% des sites web mondiaux !)

---

## [OBJECTIF] POURQUOI WORDPRESS ?

### [OK] Avantages massifs

**1. Utilisé par 42% du web mondial**
- Plus de 60 millions de sites
- De blogs personnels à CNN, Sony, Microsoft
- Communauté gigantesque

**2. Gratuité totale**
- WordPress.org = 100% gratuit
- 60 000+ plugins gratuits
- 10 000+ thèmes gratuits
- Pas de licence, pas de limite

**3. Interface ultra-simple (CMS)**
- Éditeur WYSIWYG (What You See Is What You Get)
- Gestion de contenu sans code
- Formation en 1-2 heures
- Idéal pour clients non-techniques

**4. Flexibilité infinie**
- Blog, e-commerce, portfolio, LMS, forum
- Extensible à l'infini avec plugins
- Personnalisable à 100%

**5. SEO natif**
- Structure optimisée Google
- URLs propres
- Plugins SEO puissants (Yoast, Rank Math)
- Sitemap automatique

**6. Écosystème gigantesque**
- 60 000+ plugins
- Développeurs partout
- Documentation infinie
- Support communautaire

**7. Mobile-first**
- Responsive natif
- Apps iOS/Android officielles
- Gestion depuis smartphone

**8. Sécurité mature**
- Équipe dédiée
- Patches réguliers
- Plugins de sécurité robustes
- 2FA, hardening, firewall

### [X] Inconvénients

**1. Performance variable**
- Peut être lent si mal configuré
- Nécessite optimisation
- -> Résolu avec cache + CDN

**2. Cible favorite des hackers**
- 42% du web = cible énorme
- Plugins vulnérables
- -> Résolu avec sécurité stricte

**3. Complexité cachée**
- Simple en surface
- Complexe si personnalisation poussée
- Risque de "plugin hell"

**4. Coût caché (optionnel)**
- Thèmes premium : 50-100€
- Plugins premium : 50-200€/an
- Support premium : 100-500€/an
- -> Évitable avec gratuit

**5. Bloat (surcharge)**
- Beaucoup de code inutilisé
- Base de données peut gonfler
- -> Nettoyage régulier nécessaire

---

## [GRAPHIQUE] QUAND UTILISER WORDPRESS ?

### [OK] Cas d'usage idéaux

**1. Sites vitrines / institutionnels**
- Entreprises, ONG, associations
- Portfolios professionnels
- Sites événements
- **-> 90% des cas**

**2. Blogs / médias**
- Blogs personnels/professionnels
- Magazines en ligne
- Actualités, médias
- **-> Born for this**

**3. E-commerce (WooCommerce)**
- Boutiques en ligne
- Vente de services
- Abonnements
- **-> 28% de l'e-commerce mondial**

**4. Sites communautaires**
- Forums (bbPress)
- Réseaux sociaux (BuddyPress)
- Espaces membres
- **-> Avec plugins adaptés**

**5. Plateformes d'apprentissage (LMS)**
- Cours en ligne
- Formations
- Certifications
- **-> LearnDash, LifterLMS**

**6. Annuaires / listings**
- Répertoires d'entreprises
- Petites annonces
- Immobilier
- **-> Plugins spécialisés**

### [X] Éviter WordPress pour

**1. Applications web complexes**
- SaaS, CRM custom, ERP
- Logique métier complexe
- -> Frameworks (Laravel, Django)

**2. Sites ultra-haute performance**
- Millions de visites/jour
- Latence critique
- -> Headless CMS ou static

**3. Applications temps réel**
- Chat, collaboration live
- WebSockets intensifs
- -> Node.js, Socket.io

**4. APIs pures**
- Pas d'interface
- Microservices
- -> Express, FastAPI

---

## [CONSTRUCTION] ARCHITECTURE WORDPRESS

### Stack technique

```
┌─────────────────────────────────────────────────────┐
│                  NAVIGATEUR                          │
│  • HTML/CSS/JavaScript                               │
│  • Thème WordPress                                   │
└────────────┬────────────────────────────────────────┘
             │ HTTP/HTTPS
             v
┌─────────────────────────────────────────────────────┐
│                   APACHE 2.4                         │
│  • Serveur web                                       │
│  • .htaccess (permalinks, redirections)              │
│  • mod_rewrite                                       │
└────────────┬────────────────────────────────────────┘
             │ PHP-FPM
             v
┌─────────────────────────────────────────────────────┐
│                 WORDPRESS CORE                       │
│  • PHP 8.1                                           │
│  • /wp-admin (tableau de bord)                       │
│  • /wp-content (thèmes, plugins, uploads)            │
│  • /wp-includes (fonctions core)                     │
└────────────┬────────────────────────────────────────┘
             │
    ┌────────┴────────┐
    v                 v
┌─────────┐      ┌─────────────┐
│ THÈME   │      │  PLUGINS    │
│ Design  │      │ Extensions  │
└─────────┘      └─────────────┘
                       v PDO/MySQLi
┌─────────────────────────────────────────────────────┐
│                   MySQL 8.0                          │
│  • wp_posts (articles, pages)                        │
│  • wp_users (utilisateurs)                           │
│  • wp_options (configuration)                        │
│  • wp_postmeta, wp_usermeta, etc.                    │
└─────────────────────────────────────────────────────┘
```

### Structure fichiers WordPress

```
/var/www/ong-senegal/
├── index.php                    <- Point d'entrée
├── wp-config.php               <- Configuration (DB, clés)
├── wp-admin/                   <- Interface admin
│   └── [fichiers core]
├── wp-includes/                <- Fonctions core
│   └── [fichiers core]
├── wp-content/                 <- TOUT le contenu personnalisé
│   ├── themes/                 <- Thèmes
│   │   ├── twentytwentyfour/  (défaut)
│   │   └── ong-custom/        (notre thème)
│   ├── plugins/                <- Plugins
│   │   ├── contact-form-7/
│   │   ├── yoast-seo/
│   │   └── ...
│   ├── uploads/                <- Médias (images, PDFs)
│   │   ├── 2024/
│   │   │   ├── 01/
│   │   │   └── 02/
│   │   └── ...
│   └── languages/              <- Traductions
├── .htaccess                   <- Règles Apache (permalinks)
└── wp-content-backup/          <- Backups
```

### Base de données WordPress

```sql
-- Tables principales (12 par défaut)

wp_posts            -- Articles, pages, médias, menus
wp_postmeta         -- Métadonnées des posts
wp_users            -- Utilisateurs
wp_usermeta         -- Métadonnées utilisateurs
wp_comments         -- Commentaires
wp_commentmeta      -- Métadonnées commentaires
wp_terms            -- Termes (catégories, tags)
wp_term_taxonomy    -- Taxonomies
wp_term_relationships -- Relations posts-termes
wp_options          -- Configuration (siteurl, home, etc.)
wp_links            -- Liens (rarement utilisé)
wp_termmeta         -- Métadonnées termes

-- Structure optimisée :
-- Tout est dans wp_posts (polymorphisme)
-- post_type = 'post', 'page', 'product', etc.
```

---

## [CODE] PARTIE 1 : INSTALLATION WORDPRESS

### ÉTAPE 1 : Prérequis LAMP

**Vérifier que LAMP est installé (Exercice 1 ou 5) :**

```bash
# Apache
apache2 -v
# Apache/2.4.52

# MySQL
mysql --version
# mysql  Ver 8.0.35

# PHP
php -v
# PHP 8.1.2
```

**Si pas installé, faire l'exercice 1 ou 5 d'abord.**

---

### ÉTAPE 2 : Créer base de données MySQL

```bash
sudo mysql -u root -p
```

```sql
-- ═══════════════════════════════════════════════════════════════
-- BASE DE DONNÉES WORDPRESS
-- ═══════════════════════════════════════════════════════════════

CREATE DATABASE ong_wordpress CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- Créer utilisateur dédié
CREATE USER 'wp_user'@'localhost' IDENTIFIED BY 'WordPress_SecurePass123!';

-- Donner tous les droits sur la base
GRANT ALL PRIVILEGES ON ong_wordpress.* TO 'wp_user'@'localhost';

-- Appliquer
FLUSH PRIVILEGES;

-- Vérifier
SHOW DATABASES;
SELECT User, Host FROM mysql.user WHERE User = 'wp_user';

EXIT;
```

**[OK] Base de données prête !**

---

### ÉTAPE 3 : Télécharger WordPress

```bash
# Se placer dans /tmp
cd /tmp

# Télécharger la dernière version
wget https://wordpress.org/latest.tar.gz

# Décompresser
tar -xzf latest.tar.gz

# Déplacer vers /var/www
sudo mv wordpress /var/www/ong-senegal

# Nettoyer
rm latest.tar.gz
```

---

### ÉTAPE 4 : Configuration permissions

```bash
# Propriétaire : www-data (utilisateur Apache)
sudo chown -R www-data:www-data /var/www/ong-senegal

# Permissions
sudo find /var/www/ong-senegal -type d -exec chmod 755 {} \;
sudo find /var/www/ong-senegal -type f -exec chmod 644 {} \;

# Permettre l'écriture pour wp-content (uploads, cache)
sudo chmod -R 775 /var/www/ong-senegal/wp-content
```

**Explications :**
- `755` (dossiers) : Propriétaire = lecture/écriture/exécution, Groupe+Autres = lecture/exécution
- `644` (fichiers) : Propriétaire = lecture/écriture, Groupe+Autres = lecture
- `775` (wp-content) : Permet à Apache d'écrire (uploads, cache plugins)

---

### ÉTAPE 5 : Virtual Host Apache

```bash
sudo nano /etc/apache2/sites-available/ong-senegal.conf
```

```apache
<VirtualHost *:80>
    ServerName ong-senegal.local
    ServerAlias www.ong-senegal.local
    ServerAdmin admin@ong-senegal.local
    
    DocumentRoot /var/www/ong-senegal
    
    <Directory /var/www/ong-senegal>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
        DirectoryIndex index.php
    </Directory>
    
    # Logs
    ErrorLog ${APACHE_LOG_DIR}/ong-error.log
    CustomLog ${APACHE_LOG_DIR}/ong-access.log combined
    
    # Sécurité
    <FilesMatch "^\.">
        Require all denied
    </FilesMatch>
    
    # Protéger wp-config.php
    <Files wp-config.php>
        Require all denied
    </Files>
</VirtualHost>
```

**Activer le site :**

```bash
# Activer mod_rewrite (CRUCIAL pour WordPress)
sudo a2enmod rewrite

# Activer le site
sudo a2ensite ong-senegal.conf

# Désactiver site par défaut
sudo a2dissite 000-default.conf

# Redémarrer Apache
sudo systemctl restart apache2
```

---

### ÉTAPE 6 : Ajouter au fichier hosts

**Sur ton PC (pas le serveur) :**

```bash
# Linux/Mac
sudo nano /etc/hosts

# Windows
# C:\Windows\System32\drivers\etc\hosts
```

**Ajouter :**

```
127.0.0.1    ong-senegal.local
127.0.0.1    www.ong-senegal.local
```

**Sauvegarder et quitter.**

---

### ÉTAPE 7 : Installation via navigateur

**Ouvrir :** `http://ong-senegal.local`

**[BRAVO] L'assistant d'installation WordPress apparaît !**

---

#### Écran 1 : Choix de la langue

```
Français [OK]
```

**Cliquer : Continuer**

---

#### Écran 2 : Informations de connexion

```
Nom de la base de données:     ong_wordpress
Identifiant:                   wp_user
Mot de passe:                  WordPress_SecurePass123!
Adresse de la base de données: localhost
Préfixe des tables:            wp_
```

**Cliquer : Envoyer**

**WordPress va tester la connexion.**

**[OK] Si succès : "C'est parti !"**

---

#### Écran 3 : Informations du site

```
Titre du site:           ONG Développement Communautaire Sénégal
Identifiant:             admin_ong
Mot de passe:            [GÉNÉRER MOT DE PASSE FORT - Noter !]
Votre adresse e-mail:    contact@ong-senegal.org
Visibilité moteurs:      [x] Demander aux moteurs de ne pas indexer ce site
                         (À décocher en production !)
```

**Cliquer : Installer WordPress**

**[TEMPS] Installation : 10-20 secondes**

---

#### Écran 4 : Succès !

```
[OK] Bravo ! WordPress a été installé.
```

**Cliquer : Se connecter**

---

### ÉTAPE 8 : Première connexion au tableau de bord

**URL :** `http://ong-senegal.local/wp-admin`

**Identifiants :**
- Identifiant : `admin_ong`
- Mot de passe : [celui généré]

**[BRAVO] BIENVENUE DANS WORDPRESS !**

---

## [DESIGN] PARTIE 2 : CONFIGURATION DE BASE

### ÉTAPE 1 : Paramètres généraux

**Tableau de bord -> Réglages -> Général**

```
Titre du site:           ONG Développement Communautaire Sénégal
Slogan:                  Ensemble pour un avenir meilleur
URL WordPress:           http://ong-senegal.local
URL du site:             http://ong-senegal.local
Adresse e-mail:          contact@ong-senegal.org
Rôle par défaut:         Abonné
Langue du site:          Français
Fuseau horaire:          Dakar (UTC+0)
Format de date:          d/m/Y (10/01/2024)
Format d'heure:          H:i (14:30)
```

**Enregistrer les modifications**

---

### ÉTAPE 2 : Permaliens (URLs)

**Réglages -> Permaliens**

**[ATTENTION] TRÈS IMPORTANT POUR LE SEO !**

```
Structure des permaliens:

[WHITE_CIRCLE] Simple                     (?p=123) <- MAL
[WHITE_CIRCLE] Date et titre             (/2024/01/article/) <- MAL
[WHITE_CIRCLE] Mois et titre             (/2024/01/article/) <- MAL
[WHITE_CIRCLE] Numérique                 (/archives/123/) <- MAL
[BLACK_CIRCLE] Titre de la publication  (/nom-article/) <- [OK] PARFAIT !
[WHITE_CIRCLE] Structure personnalisée
```

**Sélectionner : "Titre de la publication"**

**Enregistrer**

**[IDEE] Pourquoi ?**
- URLs parlantes : `/nos-projets/` vs `/?p=42`
- SEO optimal
- Partage social meilleur

---

### ÉTAPE 3 : Lecture (page d'accueil)

**Réglages -> Lecture**

```
Afficher sur la page d'accueil:

[BLACK_CIRCLE] Une page statique
   Page d'accueil: [À créer plus tard]
   Page des articles: Blog

Les pages du site affichent au plus: 10 articles
Les flux de syndication affichent: 10 éléments
```

---

### ÉTAPE 4 : Discussion (commentaires)

**Réglages -> Discussion**

```
Réglages par défaut des articles:
[ ] Autoriser les visiteurs à publier des commentaires

Avant publication d'un commentaire:
[x] L'auteur doit avoir un commentaire approuvé
[x] Le commentaire doit être approuvé manuellement

Blacklist de commentaires: [Liste de mots spam]

Avatars:
[BLACK_CIRCLE] Afficher les avatars
```

---

### ÉTAPE 5 : Médias (images)

**Réglages -> Médias**

```
Tailles d'image:

Miniature:
  Largeur max: 150
  Hauteur max: 150
  [x] Rogner l'image

Moyenne:
  Largeur max: 300
  Hauteur max: 300

Grande:
  Largeur max: 1024
  Hauteur max: 1024
```

---

## [NOTE] PARTIE 3 : CRÉATION DU CONTENU

### ÉTAPE 1 : Créer les pages principales

**Pages -> Ajouter**

#### Page 1 : Accueil

```
Titre: Accueil

Contenu:
[Bloc Couverture]
Titre: Ensemble pour un avenir meilleur
Sous-titre: ONG de développement communautaire au Sénégal
Bouton: Découvrir nos projets

[Bloc Colonnes - 3 colonnes]
Colonne 1:
  Icône: [HOUSE_BUILDINGS]
  Titre: Projets
  Texte: Plus de 50 projets communautaires réalisés
  
Colonne 2:
  Icône: [UTILISATEURS]
  Titre: Bénévoles
  Texte: 200+ bénévoles actifs sur le terrain
  
Colonne 3:
  Icône: [OBJECTIF]
  Titre: Impact
  Texte: 10 000 personnes touchées chaque année

[Bloc Bouton]
Voir tous nos projets
```

**Publier**

---

#### Page 2 : À propos

```
Titre: À propos de nous

Contenu:

Notre mission
Créée en 2015, l'ONG Développement Communautaire Sénégal œuvre pour...

Nos valeurs
- Solidarité
- Transparence
- Durabilité
- Inclusion

Notre équipe
[Bloc Colonnes avec photos et noms]
```

**Publier**

---

#### Page 3 : Projets

```
Titre: Nos projets

Contenu:
[Galerie de projets - sera remplie avec un plugin]
```

**Publier**

---

#### Page 4 : Contact

```
Titre: Contactez-nous

Contenu:
[Formulaire de contact - Contact Form 7]

Adresse:
ONG DCS
Rue 10, Plateau
Dakar, Sénégal

Email: contact@ong-senegal.org
Téléphone: +221 33 XXX XX XX
```

**Publier**

---

### ÉTAPE 2 : Créer le menu

**Apparence -> Menus**

```
Nom du menu: Menu principal

Ajouter au menu:
[x] Accueil
[x] À propos
[x] Projets
[x] Blog
[x] Contact

Réglages:
[x] Menu principal
[x] Menu pied de page
```

**Enregistrer**

---

### ÉTAPE 3 : Créer des articles de blog

**Articles -> Ajouter**

#### Article 1

```
Titre: Inauguration du puits à Thiès

Catégorie: Projets, Eau

Contenu:
Le 15 décembre 2023, nous avons inauguré un nouveau puits dans le village de...

[Images du projet]

Tags: eau, thiès, 2023
```

**Publier**

---

## [DESIGN] PARTIE 4 : INSTALLATION THÈME

### ÉTAPE 1 : Choisir un thème gratuit

**Apparence -> Thèmes -> Ajouter**

**Recommandations pour ONG :**

**1. Astra (*****)**
- Léger, rapide
- Page builder friendly
- 1+ million installations
- **-> Meilleur choix général**

**2. OceanWP (*****)**
- Demos magnifiques
- WooCommerce ready
- Import demo en 1 clic

**3. GeneratePress (*****)**
- Le plus rapide
- Ultra-léger (< 30KB)
- Accessibilité parfaite

**4. Kadence (*****)**
- Design moderne
- Header/Footer builder
- Gratuit très complet

---

### ÉTAPE 2 : Installer Astra

**Rechercher : "Astra"**

**Installer -> Activer**

---

### ÉTAPE 3 : Importer un modèle (Starter Template)

**Apparence -> Astra Options -> See Library**

**Chercher : "Charity" ou "Non-Profit"**

```
[Aperçu des modèles disponibles]

Choisir: "Charity Foundation"

Import:
[x] Pages
[x] Articles
[x] Médias
[x] Menus
[x] Widgets
```

**Importer**

**[TEMPS] Durée : 2-5 minutes**

**[BRAVO] Site complet importé !**

---

### ÉTAPE 4 : Personnaliser (Customizer)

**Apparence -> Personnaliser**

#### Identité du site

```
Logo: [Uploader logo ONG]
Icône du site: [Uploader favicon 512x512px]
```

---

#### Couleurs

```
Couleur principale: #27AE60 (Vert ONG)
Couleur secondaire: #2C3E50 (Bleu foncé)
Couleur de texte: #333333
Couleur liens: #27AE60
```

---

#### Typographie

```
Police de corps:
  Famille: Open Sans
  Taille: 16px
  
Police titres:
  Famille: Montserrat
  Poids: 700 (Bold)
```

---

#### En-tête

```
Structure: Logo à gauche, Menu à droite

Sticky header: Oui

Bouton CTA:
  Texte: Faire un don
  Lien: /faire-un-don/
  Style: Bouton primaire
```

---

#### Pied de page

```
Colonnes: 4

Colonne 1: À propos (widget texte)
Colonne 2: Liens rapides (widget menu)
Colonne 3: Derniers articles (widget articles)
Colonne 4: Contact (widget texte)

Copyright: © 2024 ONG DCS - Tous droits réservés
```

**Publier**

---

## [PLUGIN] PARTIE 5 : PLUGINS ESSENTIELS

### Liste des plugins à installer

**Sécurité -> Performance -> SEO -> Fonctionnalités**

---

### ÉTAPE 1 : Sécurité

#### 1. Wordfence Security

**Extensions -> Ajouter -> Rechercher "Wordfence"**

**Installer -> Activer**

**Configuration :**

```
Wordfence -> Options:

[x] Firewall Web Application
[x] Protection connexion
[x] Limiter tentatives: 20/heure
[x] Bloquer IPs malveillantes
[x] Scanner malware quotidien
```

---

#### 2. iThemes Security

**Alternative à Wordfence (choisir un seul)**

**Installation similaire.**

---

### ÉTAPE 2 : Performance

#### 1. WP Super Cache

**Extensions -> Ajouter -> "WP Super Cache"**

**Installer -> Activer**

**Configuration :**

```
Réglages -> WP Super Cache:

Cache:
[BLACK_CIRCLE] Activé
Mode: Mod_Rewrite

Paramètres avancés:
[x] Compresser les pages
[x] Cache pour utilisateurs connus
[x] Cache mobile
[x] Supprimer métadonnées
```

---

#### 2. Autoptimize

**Installation :**

**Extensions -> Ajouter -> "Autoptimize"**

**Configuration :**

```
Réglages -> Autoptimize:

JavaScript:
[x] Optimiser le code JavaScript
[x] Regrouper les scripts

CSS:
[x] Optimiser le code CSS
[x] Regrouper les CSS
[x] CSS Inline

HTML:
[x] Optimiser le code HTML
[x] Conserver les commentaires HTML: Non
```

---

#### 3. Smush (Optimisation images)

**Installation automatique.**

**Configuration :**

```
Réglages -> Smush:

[x] Compression automatique
[x] Lazy load images
[x] Redimensionnement automatique
  Largeur max: 1920px
```

---

### ÉTAPE 3 : SEO

#### 1. Rank Math SEO

*** Meilleur plugin SEO gratuit (surpasse Yoast)**

**Extensions -> Ajouter -> "Rank Math"**

**Assistant de configuration :**

```
Étape 1: Type de site
[BLACK_CIRCLE] Blog / Magazine

Étape 2: Connexion Google
[Connecter Search Console + Analytics]

Étape 3: Plan du site
[x] Générer sitemap XML
Inclure: Articles, Pages, Médias

Étape 4: Optimisations
[x] Balises Open Graph (Facebook)
[x] Twitter Cards
[x] Schema Markup (données structurées)

Étape 5: Rôle
Niveau accès SEO: Éditeur et supérieur
```

**Terminer**

**Configuration avancée :**

```
Rank Math -> Général:

Titres & Méta:
  Format titre: %title% | %sitename%
  Meta description: [Auto depuis extrait]

Social:
  Image par défaut: [Logo ONG]
  
Schema:
  Type organisation: ONG
  Logo: [Upload]
  Contact: contact@ong-senegal.org
```

---

### ÉTAPE 4 : Formulaires

#### Contact Form 7

**Extensions -> Ajouter -> "Contact Form 7"**

**Installer -> Activer**

**Créer formulaire :**

```
Contact -> Ajouter:

Nom: Contact principal

Code formulaire:
<label> Votre nom
    [text* your-name] </label>

<label> Votre e-mail
    [email* your-email] </label>

<label> Sujet
    [text your-subject] </label>

<label> Votre message
    [textarea your-message] </label>

[submit "Envoyer"]

E-mail destinataire: contact@ong-senegal.org
```

**Enregistrer**

**Copier le shortcode :** `[contact-form-7 id="123"]`

**Insérer dans page Contact.**

---

### ÉTAPE 5 : Multilingue

#### Polylang

**Extensions -> Ajouter -> "Polylang"**

**Configuration :**

```
Langues -> Ajouter:

Français:
  Langue: Français
  Code: fr
  Drapeau: [FR]
  
English:
  Langue: English
  Code: en
  Drapeau: [GB]

Langue par défaut: Français

Position sélecteur:
[x] Menu
[x] Barre admin
```

**Traduire les pages :**

```
Pages -> Éditer "Accueil":

Langues: [+] English
Titre: Home
[Traduire le contenu]

Répéter pour toutes les pages.
```

---

### ÉTAPE 6 : Newsletter

#### MailChimp for WordPress

**Extensions -> Ajouter -> "MC4WP"**

**Configuration :**

```
1. Créer compte gratuit MailChimp: https://mailchimp.com

2. Obtenir API Key:
   MailChimp -> Account -> Extras -> API keys

3. WordPress -> MailChimp:
   API Key: [Coller]
   
4. Créer formulaire:
   MC4WP -> Forms -> Add New
   
   Code:
   <label>Inscrivez-vous à notre newsletter</label>
   [mc4wp_email]
   [mc4wp_submit "S'inscrire"]
```

**Insérer dans sidebar/footer.**

---

### ÉTAPE 7 : E-commerce simple

#### WooCommerce

**Extensions -> Ajouter -> "WooCommerce"**

**Assistant configuration :**

```
Étape 1: Détails magasin
  Pays: Sénégal
  Adresse: [Adresse ONG]
  Devise: FCFA (XOF)
  
Étape 2: Type produits
  [x] Produits physiques

Étape 3: Thème
  Utiliser thème actuel: Oui
  
Étape 4: Paiement
  [x] Paiement à la livraison
  [x] Virement bancaire
  
Étape 5: Finaliser
```

**Ajouter produits :**

```
Produits -> Ajouter:

Nom: T-shirt ONG DCS
Prix: 5000 FCFA
Description: T-shirt 100% coton avec logo
Images: [Upload photos]
Catégorie: Merchandising
Stock: 50

Publier
```

---

## [SECURISE] PARTIE 6 : SÉCURITÉ WORDPRESS

### ÉTAPE 1 : Renforcer wp-config.php

```bash
sudo nano /var/www/ong-senegal/wp-config.php
```

**Ajouter AVANT "C'est tout" :**

```php
// ═══════════════════════════════════════════════════════════════
// SÉCURITÉ WORDPRESS
// ═══════════════════════════════════════════════════════════════

// Désactiver l'éditeur de fichiers
define('DISALLOW_FILE_EDIT', true);

// Désactiver l'installation de plugins/thèmes
define('DISALLOW_FILE_MODS', false); // true en production si pas de modifs

// Forcer SSL admin
define('FORCE_SSL_ADMIN', true);

// Limiter révisions
define('WP_POST_REVISIONS', 5);

// Vider corbeille auto (30 jours)
define('EMPTY_TRASH_DAYS', 30);

// Augmenter mémoire PHP
define('WP_MEMORY_LIMIT', '256M');

// Debug (désactiver en production)
define('WP_DEBUG', false);
define('WP_DEBUG_LOG', false);
define('WP_DEBUG_DISPLAY', false);
```

---

### ÉTAPE 2 : Changer préfixe tables (si pas fait)

**[ATTENTION] À faire AVANT l'installation idéalement**

**Si déjà installé, utiliser plugin :**

**Extensions -> "Change Table Prefix"**

**Changer `wp_` en `ong_` ou autre.**

---

### ÉTAPE 3 : Sécuriser .htaccess

```bash
sudo nano /var/www/ong-senegal/.htaccess
```

**Ajouter en haut :**

```apache
# ═══════════════════════════════════════════════════════════════
# SÉCURITÉ WORDPRESS
# ═══════════════════════════════════════════════════════════════

# Protéger wp-config.php
<Files wp-config.php>
    Require all denied
</Files>

# Protéger .htaccess
<Files .htaccess>
    Require all denied
</Files>

# Bloquer accès à readme.html, license.txt
<FilesMatch "^(readme|license|wp-config-sample)">
    Require all denied
</FilesMatch>

# Désactiver navigation dossiers
Options -Indexes

# Bloquer injection scripts
<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{QUERY_STRING} (<|%3C).*script.*(>|%3E) [NC,OR]
    RewriteCond %{QUERY_STRING} GLOBALS(=|[|%[0-9A-Z]{0,2}) [OR]
    RewriteCond %{QUERY_STRING} _REQUEST(=|[|%[0-9A-Z]{0,2})
    RewriteRule ^(.*)$ index.php [F,L]
</IfModule>

# Protéger contre hotlinking images
RewriteEngine on
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^http(s)?://(www\.)?ong-senegal.local [NC]
RewriteRule \.(jpg|jpeg|png|gif)$ - [F]

# Headers sécurité
<IfModule mod_headers.c>
    Header set X-XSS-Protection "1; mode=block"
    Header set X-Content-Type-Options "nosniff"
    Header set X-Frame-Options "SAMEORIGIN"
    Header set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>
```

---

### ÉTAPE 4 : Deux-facteurs (2FA)

**Extensions -> "WP 2FA"**

**Configuration :**

```
Utilisateurs -> Votre profil:

[x] Activer 2FA
Méthode: App Authenticator (Google Authenticator)

Scanner QR code avec app mobile
Entrer code de vérification
```

---

### ÉTAPE 5 : Limiter tentatives connexion

**Déjà couvert par Wordfence, mais si pas installé :**

**Extensions -> "Limit Login Attempts Reloaded"**

**Configuration :**

```
Réglages:

Tentatives autorisées: 4
Minutes avant reset: 20
Heures de verrouillage: 24

Notification email: Oui
```

---

### ÉTAPE 6 : Backups automatiques

**Extensions -> "UpdraftPlus"**

**Configuration :**

```
Réglages -> UpdraftPlus Backup:

Planning backup:
  Fichiers: Hebdomadaire
  Base de données: Quotidien
  
Rétention:
  7 backups fichiers
  30 backups BDD

Stockage distant:
  Google Drive / Dropbox [Connecter]

Inclure:
  [x] Plugins
  [x] Thèmes
  [x] Uploads
  [x] Base de données
```

**Faire backup manuel test :**

```
Sauvegarder maintenant -> Télécharger
```

---

## [RAPIDE] PARTIE 7 : OPTIMISATION PERFORMANCE

### ÉTAPE 1 : Tester vitesse actuelle

**Aller sur : https://gtmetrix.com**

**Entrer :** `http://ong-senegal.local`

**Note avant optimisation : ~C/D**

---

### ÉTAPE 2 : Optimiser base de données

**Extensions -> "WP-Optimize"**

**Optimisations -> Tout sélectionner:**

```
[x] Révisions articles
[x] Brouillons auto-sauvegardés
[x] Articles corbeille
[x] Commentaires spam
[x] Commentaires corbeille
[x] Transients expirés
[x] Tables overhead
```

**Lancer optimisation**

**Économie : ~30-50% taille BDD**

---

### ÉTAPE 3 : Lazy load images

**Déjà activé avec Smush, mais vérifier :**

```
Réglages -> Smush:

Lazy Load:
  [x] Images
  [x] Iframes (YouTube, etc.)
```

---

### ÉTAPE 4 : CDN (Cloudflare)

**Si domaine en production (exercice 5), activer Cloudflare.**

**Plugin recommandé :**

**Extensions -> "Cloudflare"**

**Configuration :**

```
API Key: [De Cloudflare]

Optimisations:
  [x] Auto Minify (JS, CSS, HTML)
  [x] Brotli compression
  [x] Polish images
  [x] Mirage (lazy load)
```

---

### ÉTAPE 5 : Désactiver plugins inutilisés

```
Extensions -> Installées:

Désactiver/Supprimer:
  [X] Hello Dolly
  [X] Akismet (si pas de commentaires)
  [X] Plugins démo
```

**Règle : 1 fonctionnalité = 1 plugin max**

---

### ÉTAPE 6 : Optimiser robots.txt

```bash
sudo nano /var/www/ong-senegal/robots.txt
```

```
User-agent: *
Disallow: /wp-admin/
Disallow: /wp-includes/
Disallow: /wp-content/plugins/
Disallow: /wp-content/themes/
Allow: /wp-content/uploads/

Sitemap: https://ong-senegal.com/sitemap.xml
```

---

### ÉTAPE 7 : Retester vitesse

**GTmetrix -> Nouvelle analyse**

**Note après optimisation : ~A/B**

**Amélioration : 40-60% temps chargement**

---

## [MOBILE] PARTIE 8 : RESPONSIVE ET MOBILE

### ÉTAPE 1 : Vérifier responsive

**Outils navigateur (F12) -> Toggle device toolbar**

**Tester :**
- iPhone SE (375px)
- iPad (768px)
- Desktop (1920px)

---

### ÉTAPE 2 : AMP (Accelerated Mobile Pages)

**Extensions -> "AMP"**

**Configuration minimale, Google gérera.**

---

### ÉTAPE 3 : Progressive Web App (PWA)

**Extensions -> "Super Progressive Web Apps"**

**Configuration :**

```
Nom app: ONG DCS
Nom court: ONG DCS
Description: ONG Développement Communautaire
Icône: [Upload 512x512]
Couleur thème: #27AE60
```

**Test : Bouton "Installer" apparaît sur mobile.**

---

## [RECHERCHE] PARTIE 9 : SEO AVANCÉ

### ÉTAPE 1 : Connecter Google Search Console

**1. Aller sur : https://search.google.com/search-console**

**2. Ajouter propriété : ong-senegal.com**

**3. Vérification : Balise HTML**

**Rank Math -> Général -> Webmaster Tools:**

```
Google Search Console:
  Code vérification: [Coller]
```

**4. Envoyer sitemap :**

```
URL sitemap: https://ong-senegal.com/sitemap_index.xml
```

---

### ÉTAPE 2 : Schema Markup

**Déjà configuré avec Rank Math, vérifier :**

```
Rank Math -> Schema Markup:

Homepage:
  Type: Organization
  Logo: [URL logo]
  Nom: ONG Développement Communautaire Sénégal
  Contact: contact@ong-senegal.org
  Réseaux sociaux: [Facebook, Twitter, etc.]

Articles:
  Type: Article
  Auteur: [Auto]
  Date publication: [Auto]
```

---

### ÉTAPE 3 : Optimiser chaque page

**Éditer page -> Rank Math (bandeau haut) :**

```
Focus Keyword: développement communautaire sénégal

Titre SEO: ONG Développement Communautaire au Sénégal | DCS
Meta Description: ONG engagée dans le développement communautaire au Sénégal. Projets d'eau, éducation, santé. Rejoignez-nous !

[Score Rank Math: 90+/100]
```

**Vérifications automatiques :**
- [x] Mot-clé dans titre
- [x] Mot-clé dans URL
- [x] Mot-clé dans méta
- [x] Mot-clé dans premier paragraphe
- [x] Longueur contenu (300+ mots)
- [x] Images avec alt text
- [x] Liens internes

---

## [MONDE] PARTIE 10 : PRODUCTION (SI DOMAINE RÉEL)

### ÉTAPE 1 : Migration local -> production

**Plugin : "All-in-One WP Migration"**

**Sur site local :**

```
Extensions -> All-in-One WP Migration:

Exporter:
  [x] Base de données
  [x] Médias
  [x] Thèmes
  [x] Plugins
  
Télécharger fichier .wpress
```

**Sur serveur production :**

```
1. Installer WordPress vierge
2. Installer plugin "All-in-One WP Migration"
3. Importer -> Uploader fichier .wpress
4. Remplacer URLs automatiquement
```

---

### ÉTAPE 2 : SSL en production

**Déjà couvert exercice 5.**

**Forcer HTTPS dans wp-config.php :**

```php
define('FORCE_SSL_ADMIN', true);

// Redirection HTTP -> HTTPS
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) {
    // HTTPS
} else {
    wp_redirect('https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'], 301);
    exit();
}
```

---

## [GRAPHIQUE] PARTIE 11 : ANALYTICS ET SUIVI

### ÉTAPE 1 : Google Analytics 4

**1. Créer propriété GA4**

**2. Obtenir Measurement ID : G-XXXXXXXXXX**

**3. WordPress :**

**Extensions -> "GA Google Analytics"**

**Configuration :**

```
Measurement ID: G-XXXXXXXXXX

Tracking:
  [x] Pages vues
  [x] Événements
  [x] E-commerce (si WooCommerce)
```

---

### ÉTAPE 2 : Heatmaps (Hotjar)

**1. Créer compte gratuit : https://www.hotjar.com**

**2. Obtenir Tracking Code**

**3. WordPress :**

**Apparence -> Personnaliser -> Code additionnel:**

```html
<script>
  (function(h,o,t,j,a,r){
    // Code Hotjar
  })(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');
</script>
```

---

## [COURS] PARTIE 12 : MAINTENANCE

### Checklist maintenance hebdomadaire

```
[ ] Vérifier mises à jour (core, plugins, thèmes)
[ ] Lancer scan sécurité (Wordfence)
[ ] Optimiser base de données (WP-Optimize)
[ ] Vérifier backups (UpdraftPlus)
[ ] Analyser logs erreurs
[ ] Vérifier vitesse (GTmetrix)
[ ] Modérer commentaires/spam
[ ] Vérifier formulaires contact
```

---

### Commandes utiles

```bash
# Vider cache WP Super Cache
sudo rm -rf /var/www/ong-senegal/wp-content/cache/*

# Régénérer .htaccess
WordPress -> Réglages -> Permaliens -> Enregistrer

# Réinitialiser permissions
sudo find /var/www/ong-senegal -type d -exec chmod 755 {} \;
sudo find /var/www/ong-senegal -type f -exec chmod 644 {} \;
sudo chown -R www-data:www-data /var/www/ong-senegal

# Exporter base de données
mysqldump -u wp_user -p ong_wordpress > backup.sql

# Importer base de données
mysql -u wp_user -p ong_wordpress < backup.sql
```

---

## [DOCS] RESSOURCES WORDPRESS

### Documentation officielle

- **WordPress Codex :** https://codex.wordpress.org
- **Developer Handbook :** https://developer.wordpress.org
- **Support Forums :** https://wordpress.org/support/

### Thèmes gratuits

- **WordPress.org Themes :** https://wordpress.org/themes/
- **Astra :** https://wpastra.com
- **GeneratePress :** https://generatepress.com
- **OceanWP :** https://oceanwp.org

### Plugins gratuits

- **WordPress.org Plugins :** https://wordpress.org/plugins/
- **WPBeginner :** https://www.wpbeginner.com

### Communautés

- **WPBeginner :** https://www.wpbeginner.com
- **WPCrafter (YouTube) :** https://youtube.com/wpcrafter
- **Reddit r/Wordpress :** https://reddit.com/r/Wordpress

---

## [COURS] CONCLUSION EXERCICE 6

**[BRAVO] FÉLICITATIONS ! Tu maîtrises WordPress de A à Z !**

**Compétences acquises :**
- [OK] Installation LAMP + WordPress
- [OK] Configuration optimale
- [OK] Création contenu (pages, articles, menus)
- [OK] Installation et personnalisation thème
- [OK] Gestion plugins essentiels
- [OK] Sécurité WordPress complète
- [OK] Optimisation performance
- [OK] SEO avancé avec Rank Math
- [OK] Responsive et mobile-first
- [OK] Analytics et tracking
- [OK] Maintenance professionnelle

**[PRO] Valeur professionnelle :**
- **Développeur WordPress Junior :** 25-40K€/an
- **Freelance WordPress :** 300-600€/jour
- **Agence web :** Compétence recherchée

**[GRAPHIQUE] Statistiques WordPress :**
- 42% des sites web mondiaux
- 64% de parts de marché CMS
- 500+ sites créés par jour
- Écosystème de 50 milliards $

**[RAPIDE] Prochaines étapes :**
1. Créer des thèmes WordPress custom
2. Développer plugins WordPress
3. WooCommerce avancé (e-commerce)
4. WordPress multisite (réseau)
5. Headless WordPress (API REST)

**Tu peux maintenant créer des sites WordPress professionnels pour des clients réels ! [FORCE]**

---

## [WRAPPED_PRESENT] BONUS : THÈME CUSTOM SIMPLE

Si tu veux créer un thème WordPress from scratch :

```bash
cd /var/www/ong-senegal/wp-content/themes
mkdir ong-custom
cd ong-custom
```

**style.css :**

```css
/*
Theme Name: ONG Custom
Theme URI: https://ong-senegal.com
Author: Votre Nom
Author URI: https://votre-site.com
Description: Thème custom pour ONG
Version: 1.0
License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Text Domain: ong-custom
*/

/* Vos styles ici */
```

**index.php :**

```php
<?php get_header(); ?>

<main>
    <?php if (have_posts()) : while (have_posts()) : the_post(); ?>
        <article>
            <h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
            <div><?php the_excerpt(); ?></div>
        </article>
    <?php endwhile; endif; ?>
</main>

<?php get_footer(); ?>
```

**functions.php :**

```php
<?php
// Enregistrer menus
register_nav_menus(array(
    'primary' => 'Menu Principal',
    'footer' => 'Menu Pied de page'
));

// Support images à la une
add_theme_support('post-thumbnails');

// Support titre automatique
add_theme_support('title-tag');
?>
```

**Et voilà - thème de base fonctionnel ! [DESIGN]**

# [ROUGE] EXERCICE 7 : LAMP + LARAVEL - APPLICATION PROFESSIONNELLE MVC

## [LISTE] ÉNONCÉ

### Contexte professionnel

Une **startup sénégalaise de services digitaux** te recrute comme développeur backend. Leur équipe utilise des outils dispersés (Excel, emails, WhatsApp) pour gérer leurs projets. Ils veulent une **application web interne moderne** pour :

**Fonctionnalités requises :**
- Gestion de projets et tâches (CRUD complet)
- Attribution de tâches aux membres
- Suivi d'avancement (To Do, In Progress, Done)
- Système de commentaires
- Notifications par email
- Dashboard avec statistiques
- API REST pour future app mobile
- Authentification sécurisée (login/register)
- Autorisation (rôles: Admin, Manager, Developer)

**Contraintes techniques :**
- **Architecture MVC** propre et maintenable
- **Code moderne PHP 8.1+**
- **Base de données relationnelle** (migrations, relations)
- **API RESTful** documentée
- **Tests unitaires** et d'intégration
- **Sécurité** (CSRF, XSS, SQL Injection)
- **Performances** optimales

**Stack retenue :** **Laravel 10** (framework PHP le plus populaire au monde)

---

## [OBJECTIF] POURQUOI LARAVEL ?

### [OK] Avantages massifs

**1. Framework PHP n°1 mondial**
- **78% des développeurs PHP** l'utilisent
- **1,5 million de sites** en production
- Utilisé par : Disney, Lenovo, New York Times, 9GAG
- Écosystème gigantesque

**2. Productivité exceptionnelle**
- **Artisan CLI** : Génération code automatique
- **Eloquent ORM** : Requêtes SQL en PHP
- **Blade Templates** : Moteur de template puissant
- **Migrations** : Versionning de BDD
- **Seeders** : Données de test automatiques
- **Queue System** : Jobs asynchrones
- **Task Scheduling** : Cron intégré

**Exemple concret :**
```bash
# Créer un modèle + migration + controller en 1 commande
php artisan make:model Task -mcr

# = 4 fichiers générés automatiquement :
# - app/Models/Task.php (modèle)
# - database/migrations/xxxx_create_tasks_table.php
# - app/Http/Controllers/TaskController.php
# - routes/web.php (ajouté automatiquement)
```

**3. Architecture MVC stricte**
- **Séparation claire** : Modèles, Vues, Contrôleurs
- **Code organisé** et maintenable
- **Facile à tester**
- **Scalable** (millions d'utilisateurs possibles)

**4. Sécurité intégrée**
- **CSRF protection** automatique
- **XSS prevention** (Blade escape)
- **SQL Injection** impossible (Eloquent)
- **Hash passwords** (Bcrypt par défaut)
- **Encryption** (AES-256)
- **Authentication** scaffolding

**5. Écosystème riche**
- **Laravel Sanctum** : API tokens
- **Laravel Passport** : OAuth2 server
- **Laravel Socialite** : Login social (Google, Facebook)
- **Laravel Telescope** : Debugging avancé
- **Laravel Horizon** : Queue monitoring
- **Laravel Nova** : Admin panel

**6. Documentation exceptionnelle**
- **99/100** sur documentation quality
- Tutorials vidéo (Laracasts - 1500+ vidéos)
- Communauté active (forums, Discord, Reddit)

**7. Performance**
- **Caching** multi-drivers (Redis, Memcached)
- **Database Query Cache**
- **Route caching**
- **View caching**
- **OPcache ready**

**8. Testing natif**
- **PHPUnit** intégré
- **Feature tests** (HTTP requests)
- **Unit tests**
- **Database factories**
- **Mock/Spy** faciles

### [X] Inconvénients

**1. Courbe d'apprentissage**
- Concepts MVC à maîtriser
- Conventions Laravel spécifiques
- Eloquent ORM puissant mais complexe
- -> 2-3 semaines pour maîtrise basique

**2. Overhead (surcharge)**
- Framework lourd (~80MB avec vendor)
- Plus lent que PHP natif
- Nécessite Composer
- -> Acceptable pour 99% des projets

**3. Versions qui évoluent**
- Nouvelle version majeure/an
- Breaking changes possibles
- Migration parfois coûteuse
- -> LTS (Long Term Support) disponible

**4. Hosting spécifique**
- Nécessite SSH + Composer
- Hébergement mutualisé difficile
- VPS recommandé
- -> Coût légèrement supérieur

**5. Overkill pour petits projets**
- Blog simple = WordPress meilleur
- Landing page = HTML statique
- -> Laravel pour apps complexes

---

## [GRAPHIQUE] QUAND UTILISER LARAVEL ?

### [OK] Cas d'usage idéaux

**1. Applications web complexes**
- **ERP, CRM, SaaS**
- Dashboards administratifs
- Plateformes multi-tenants
- **-> 80% des use cases**

**2. API REST professionnelles**
- Backend pour apps mobiles
- Microservices
- Intégrations tierces
- **-> Architecture découplée**

**3. E-commerce custom**
- Boutiques spécifiques
- Marketplace
- Abonnements/SaaS
- **-> Alternative à Magento**

**4. Plateformes collaboratives**
- Gestion de projets
- Outils internes entreprise
- Social networks
- **-> Temps réel avec Laravel Echo**

**5. Portails entreprise**
- Intranets
- Gestion RH
- Systèmes de ticketing
- **-> Autorisation complexe (Policies)**

**6. Applications avec logique métier**
- Calculs complexes
- Workflows
- Business rules
- **-> PHP orienté objet moderne**

### [X] Éviter Laravel pour

**1. Sites statiques/vitrines**
- Blogs simples
- Landing pages
- Portfolios basiques
- -> WordPress ou générateur statique

**2. Performance extrême**
- Millions requêtes/seconde
- Latence < 10ms critique
- -> Go, Rust, Node.js

**3. Temps réel intensif**
- Chat en temps réel
- Collaboration live (Google Docs)
- Gaming
- -> Node.js + WebSockets natifs

**4. Pas de serveur SSH**
- Hébergement mutualisé
- FTP seulement
- -> PHP natif ou WordPress

---

## [CONSTRUCTION] ARCHITECTURE LARAVEL

### Vue d'ensemble MVC

```
┌─────────────────────────────────────────────────────┐
│                  NAVIGATEUR                          │
│  • HTML/CSS/JavaScript                               │
│  • Vue.js / React (optionnel)                        │
└────────────┬────────────────────────────────────────┘
             │ HTTP Request
             v
┌─────────────────────────────────────────────────────┐
│                   ROUTES                             │
│  routes/web.php -> Route::get('/tasks', ...)          │
│  routes/api.php -> Route::apiResource('tasks', ...)   │
└────────────┬────────────────────────────────────────┘
             │
             v
┌─────────────────────────────────────────────────────┐
│                 MIDDLEWARE                           │
│  • Authentification                                  │
│  • Autorisation                                      │
│  • CSRF protection                                   │
│  • Rate limiting                                     │
└────────────┬────────────────────────────────────────┘
             │
             v
┌─────────────────────────────────────────────────────┐
│                 CONTROLLERS                          │
│  app/Http/Controllers/TaskController.php             │
│  • index() -> Liste                                   │
│  • create() -> Formulaire création                    │
│  • store() -> Enregistrer                             │
│  • show() -> Détails                                  │
│  • edit() -> Formulaire édition                       │
│  • update() -> Mettre à jour                          │
│  • destroy() -> Supprimer                             │
└────────────┬────────────────────────────────────────┘
             │
    ┌────────┴────────┐
    v                 v
┌─────────┐      ┌─────────────┐
│ MODELS  │      │  VIEWS      │
│ Eloquent│[BLACK_LEFT-POINTING_POINTER]─────┤  Blade      │
└────┬────┘      └─────────────┘
     │
     v Query Builder / Eloquent ORM
┌─────────────────────────────────────────────────────┐
│                   MySQL 8.0                          │
│  • Migrations (versioning schéma)                    │
│  • Relations (hasMany, belongsTo, etc.)              │
└─────────────────────────────────────────────────────┘
```

### Structure fichiers Laravel

```
task-manager/
├── app/
│   ├── Http/
│   │   ├── Controllers/        <- CONTRÔLEURS
│   │   │   ├── TaskController.php
│   │   │   ├── ProjectController.php
│   │   │   └── DashboardController.php
│   │   ├── Middleware/         <- Filtres HTTP
│   │   └── Requests/           <- Validation formulaires
│   ├── Models/                 <- MODÈLES (Eloquent)
│   │   ├── User.php
│   │   ├── Task.php
│   │   └── Project.php
│   └── Providers/              <- Service providers
├── database/
│   ├── migrations/             <- Schéma BDD versionné
│   │   ├── 2024_01_10_create_users_table.php
│   │   ├── 2024_01_10_create_projects_table.php
│   │   └── 2024_01_10_create_tasks_table.php
│   ├── seeders/                <- Données de test
│   │   └── DatabaseSeeder.php
│   └── factories/              <- Génération fake data
│       └── TaskFactory.php
├── resources/
│   ├── views/                  <- VUES (Blade templates)
│   │   ├── layouts/
│   │   │   └── app.blade.php  (Layout principal)
│   │   ├── tasks/
│   │   │   ├── index.blade.php
│   │   │   ├── create.blade.php
│   │   │   ├── edit.blade.php
│   │   │   └── show.blade.php
│   │   └── dashboard.blade.php
│   ├── css/
│   └── js/
├── routes/
│   ├── web.php                 <- Routes interface web
│   └── api.php                 <- Routes API REST
├── public/                     <- Dossier public (DocumentRoot)
│   ├── index.php              <- Point d'entrée unique
│   ├── css/
│   ├── js/
│   └── images/
├── storage/                    <- Stockage (logs, cache, uploads)
├── tests/                      <- Tests automatisés
│   ├── Feature/
│   └── Unit/
├── vendor/                     <- Dépendances Composer
├── .env                        <- Configuration (DB, mail, etc.)
├── artisan                     <- CLI Laravel
├── composer.json               <- Dépendances PHP
└── package.json                <- Dépendances JS (optionnel)
```

### Pattern MVC en action

```php
// ═══════════════════════════════════════════════════════════════
// ROUTE (routes/web.php)
// ═══════════════════════════════════════════════════════════════
Route::get('/tasks', [TaskController::class, 'index']);

// ═══════════════════════════════════════════════════════════════
// CONTROLLER (app/Http/Controllers/TaskController.php)
// ═══════════════════════════════════════════════════════════════
public function index() {
    $tasks = Task::with('user', 'project')->get();  // MODEL
    return view('tasks.index', compact('tasks'));    // VIEW
}

// ═══════════════════════════════════════════════════════════════
// MODEL (app/Models/Task.php)
// ═══════════════════════════════════════════════════════════════
class Task extends Model {
    public function user() {
        return $this->belongsTo(User::class);
    }
    
    public function project() {
        return $this->belongsTo(Project::class);
    }
}

// ═══════════════════════════════════════════════════════════════
// VIEW (resources/views/tasks/index.blade.php)
// ═══════════════════════════════════════════════════════════════
@foreach($tasks as $task)
    <div class="task">
        <h3>{{ $task->title }}</h3>
        <p>Assignée à : {{ $task->user->name }}</p>
    </div>
@endforeach
```

---

## [CODE] PARTIE 1 : INSTALLATION LARAVEL

### ÉTAPE 1 : Prérequis système

**Vérifier LAMP (Exercice 1 ou 5) :**

```bash
# PHP 8.1+ avec extensions requises
php -v
# PHP 8.1.x

php -m | grep -E 'pdo|mbstring|tokenizer|xml|ctype|json|bcmath|fileinfo'
# Toutes doivent apparaître
```

**Si extensions manquantes :**

```bash
sudo apt install -y php8.1-cli php8.1-fpm php8.1-mysql php8.1-mbstring \
  php8.1-xml php8.1-curl php8.1-zip php8.1-gd php8.1-intl \
  php8.1-bcmath php8.1-tokenizer
```

---

### ÉTAPE 2 : Installer Composer

**Composer = Gestionnaire de dépendances PHP (npm pour PHP)**

```bash
# Télécharger
curl -sS https://getcomposer.org/installer | php

# Installer globalement
sudo mv composer.phar /usr/local/bin/composer

# Vérifier
composer --version
# Composer version 2.6.x
```

---

### ÉTAPE 3 : Créer projet Laravel

```bash
# Se placer dans /var/www
cd /var/www

# Créer nouveau projet Laravel
sudo composer create-project laravel/laravel task-manager

# [TEMPS] Durée : 2-5 minutes (télécharge ~80MB)

# Permissions
sudo chown -R www-data:www-data task-manager
sudo chmod -R 775 task-manager/storage
sudo chmod -R 775 task-manager/bootstrap/cache
```

**Structure créée :**

```
/var/www/task-manager/
├── app/
├── database/
├── public/        <- DocumentRoot Apache
├── routes/
├── .env           <- Configuration
└── artisan        <- CLI Laravel
```

---

### ÉTAPE 4 : Configurer base de données

```bash
# Se connecter à MySQL
sudo mysql -u root -p
```

```sql
-- ═══════════════════════════════════════════════════════════════
-- BASE DE DONNÉES LARAVEL
-- ═══════════════════════════════════════════════════════════════

CREATE DATABASE task_manager CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

CREATE USER 'laravel_user'@'localhost' IDENTIFIED BY 'Laravel_SecurePass123!';

GRANT ALL PRIVILEGES ON task_manager.* TO 'laravel_user'@'localhost';

FLUSH PRIVILEGES;

EXIT;
```

---

### ÉTAPE 5 : Configurer .env

```bash
cd /var/www/task-manager
sudo nano .env
```

**Modifier les lignes :**

```ini
APP_NAME="Task Manager"
APP_ENV=local
APP_DEBUG=true
APP_URL=http://task-manager.local

# Base de données
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=task_manager
DB_USERNAME=laravel_user
DB_PASSWORD=Laravel_SecurePass123!

# Mail (plus tard)
MAIL_MAILER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="noreply@task-manager.local"
MAIL_FROM_NAME="${APP_NAME}"
```

**Sauvegarder (Ctrl+O, Entrée, Ctrl+X)**

---

### ÉTAPE 6 : Générer clé application

```bash
# Générer APP_KEY (crucial pour encryption)
php artisan key:generate

# Résultat dans .env :
# APP_KEY=base64:xxxxxxxxxxxxx
```

---

### ÉTAPE 7 : Virtual Host Apache

```bash
sudo nano /etc/apache2/sites-available/task-manager.conf
```

```apache
<VirtualHost *:80>
    ServerName task-manager.local
    ServerAlias www.task-manager.local
    ServerAdmin admin@task-manager.local
    
    # [ATTENTION] IMPORTANT : DocumentRoot = /public
    DocumentRoot /var/www/task-manager/public
    
    <Directory /var/www/task-manager/public>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
        DirectoryIndex index.php
    </Directory>
    
    # Logs
    ErrorLog ${APACHE_LOG_DIR}/task-manager-error.log
    CustomLog ${APACHE_LOG_DIR}/task-manager-access.log combined
</VirtualHost>
```

**Activer :**

```bash
# Activer le site
sudo a2ensite task-manager.conf

# Activer mod_rewrite (CRUCIAL)
sudo a2enmod rewrite

# Redémarrer Apache
sudo systemctl restart apache2
```

---

### ÉTAPE 8 : Hosts file

**Sur ton PC :**

```bash
# Linux/Mac
sudo nano /etc/hosts

# Windows : C:\Windows\System32\drivers\etc\hosts
```

**Ajouter :**

```
127.0.0.1    task-manager.local
127.0.0.1    www.task-manager.local
```

---

### ÉTAPE 9 : Tester l'installation

**Ouvrir navigateur :** `http://task-manager.local`

**[BRAVO] Page d'accueil Laravel doit s'afficher !**

```
Laravel

The PHP Framework for Web Artisans
```

---

## [ARCHIVE] PARTIE 2 : SCHÉMA DE BASE DE DONNÉES (MIGRATIONS)

### ÉTAPE 1 : Comprendre les migrations

**Migrations = Versioning du schéma de BDD**

```php
// Équivalent Git mais pour la base de données
php artisan migrate         // git commit
php artisan migrate:rollback // git revert
php artisan migrate:fresh    // git reset --hard
```

**Avantages :**
- [OK] Historique des changements
- [OK] Collaboration équipe (pas de conflit SQL)
- [OK] Reproductible (dev -> staging -> prod)
- [OK] Testable (recréer BDD de test)

---

### ÉTAPE 2 : Créer migrations

```bash
cd /var/www/task-manager

# Migration pour projects
php artisan make:migration create_projects_table

# Migration pour tasks
php artisan make:migration create_tasks_table

# Migration pour comments
php artisan make:migration create_comments_table
```

**Fichiers créés dans `database/migrations/` :**

```
2024_01_10_120000_create_projects_table.php
2024_01_10_120100_create_tasks_table.php
2024_01_10_120200_create_comments_table.php
```

---

### ÉTAPE 3 : Modifier users (déjà existante)

```bash
nano database/migrations/2014_10_12_000000_create_users_table.php
```

**Ajouter colonnes role et status :**

```php
<?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('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            
            // AJOUTS
            $table->enum('role', ['admin', 'manager', 'developer'])
                  ->default('developer');
            $table->boolean('is_active')->default(true);
            $table->string('avatar')->nullable();
            
            $table->rememberToken();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('users');
    }
};
```

---

### ÉTAPE 4 : Migration projects

```bash
nano database/migrations/2024_01_10_120000_create_projects_table.php
```

```php
<?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('projects', function (Blueprint $table) {
            $table->id();
            
            $table->string('name');
            $table->text('description')->nullable();
            $table->enum('status', ['active', 'on_hold', 'completed', 'cancelled'])
                  ->default('active');
            
            // Relation : un projet a un propriétaire (User)
            $table->foreignId('owner_id')
                  ->constrained('users')
                  ->onDelete('cascade');
            
            $table->date('start_date')->nullable();
            $table->date('end_date')->nullable();
            $table->string('color', 7)->default('#3490dc'); // Hex color
            
            $table->timestamps();
            
            // Index pour performance
            $table->index('status');
            $table->index('owner_id');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('projects');
    }
};
```

**Explications :**

```php
$table->id();                  // BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
$table->string('name');        // VARCHAR(255)
$table->text('description');   // TEXT
$table->enum('status', [...])  // ENUM('active', 'on_hold', ...)
$table->foreignId('owner_id')  // BIGINT UNSIGNED (clé étrangère)
  ->constrained('users')       // FOREIGN KEY REFERENCES users(id)
  ->onDelete('cascade');       // ON DELETE CASCADE
$table->timestamps();          // created_at, updated_at TIMESTAMP
```

---

### ÉTAPE 5 : Migration tasks

```bash
nano database/migrations/2024_01_10_120100_create_tasks_table.php
```

```php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('tasks', function (Blueprint $table) {
            $table->id();
            
            $table->string('title');
            $table->text('description')->nullable();
            
            $table->enum('status', ['todo', 'in_progress', 'review', 'done'])
                  ->default('todo');
            
            $table->enum('priority', ['low', 'medium', 'high', 'urgent'])
                  ->default('medium');
            
            // Relations
            $table->foreignId('project_id')
                  ->constrained()
                  ->onDelete('cascade');
            
            $table->foreignId('assigned_to')
                  ->nullable()
                  ->constrained('users')
                  ->onDelete('set null');
            
            $table->foreignId('created_by')
                  ->constrained('users')
                  ->onDelete('cascade');
            
            // Dates
            $table->date('due_date')->nullable();
            $table->timestamp('completed_at')->nullable();
            
            // Estimations (en heures)
            $table->integer('estimated_hours')->nullable();
            $table->integer('actual_hours')->nullable();
            
            $table->timestamps();
            
            // Index
            $table->index(['project_id', 'status']);
            $table->index('assigned_to');
            $table->index('due_date');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('tasks');
    }
};
```

---

### ÉTAPE 6 : Migration comments

```bash
nano database/migrations/2024_01_10_120200_create_comments_table.php
```

```php
<?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('comments', function (Blueprint $table) {
            $table->id();
            
            $table->text('content');
            
            // Polymorphique : peut commenter tâche ou projet
            $table->morphs('commentable');
            
            // Auteur du commentaire
            $table->foreignId('user_id')
                  ->constrained()
                  ->onDelete('cascade');
            
            $table->timestamps();
            
            $table->index(['commentable_type', 'commentable_id']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('comments');
    }
};
```

**Explication polymorphique :**

```php
$table->morphs('commentable');

// Crée 2 colonnes :
// - commentable_type (VARCHAR) -> "App\Models\Task" ou "App\Models\Project"
// - commentable_id (BIGINT) -> ID de la tâche ou du projet

// Permet de commenter plusieurs types d'entités avec 1 seule table
```

---

### ÉTAPE 7 : Exécuter migrations

```bash
# Lancer toutes les migrations
php artisan migrate

# Résultat :
# Migrating: 2014_10_12_000000_create_users_table
# Migrated:  2014_10_12_000000_create_users_table (XX.XXms)
# Migrating: 2024_01_10_120000_create_projects_table
# Migrated:  2024_01_10_120000_create_projects_table (XX.XXms)
# ...
```

**Vérifier dans MySQL :**

```bash
sudo mysql -u laravel_user -p task_manager
```

```sql
SHOW TABLES;

-- Résultat :
-- +------------------------+
-- | Tables_in_task_manager |
-- +------------------------+
-- | comments               |
-- | failed_jobs            |
-- | migrations             |
-- | password_reset_tokens  |
-- | personal_access_tokens |
-- | projects               |
-- | tasks                  |
-- | users                  |
-- +------------------------+

DESCRIBE tasks;

EXIT;
```

**[OK] Base de données créée avec relations !**

---

## [DESIGN] PARTIE 3 : MODÈLES ELOQUENT

### ÉTAPE 1 : Créer les modèles

```bash
# Project
php artisan make:model Project

# Task (déjà créé avec migration, mais on peut recréer)
php artisan make:model Task

# Comment
php artisan make:model Comment
```

**Fichiers créés dans `app/Models/` :**

---

### ÉTAPE 2 : Modèle User (modifier existant)

```bash
nano app/Models/User.php
```

```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    protected $fillable = [
        'name',
        'email',
        'password',
        'role',
        'is_active',
        'avatar',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected $casts = [
        'email_verified_at' => 'datetime',
        'password' => 'hashed',
        'is_active' => 'boolean',
    ];

    // ═══════════════════════════════════════════════════════════
    // RELATIONS
    // ═══════════════════════════════════════════════════════════
    
    /**
     * Projets dont je suis propriétaire
     */
    public function ownedProjects()
    {
        return $this->hasMany(Project::class, 'owner_id');
    }
    
    /**
     * Tâches qui me sont assignées
     */
    public function assignedTasks()
    {
        return $this->hasMany(Task::class, 'assigned_to');
    }
    
    /**
     * Tâches que j'ai créées
     */
    public function createdTasks()
    {
        return $this->hasMany(Task::class, 'created_by');
    }
    
    /**
     * Mes commentaires
     */
    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
    
    // ═══════════════════════════════════════════════════════════
    // ACCESSORS & MUTATORS
    // ═══════════════════════════════════════════════════════════
    
    /**
     * Formater le rôle
     */
    public function getRoleNameAttribute()
    {
        return match($this->role) {
            'admin' => 'Administrateur',
            'manager' => 'Manager',
            'developer' => 'Développeur',
            default => $this->role,
        };
    }
    
    /**
     * Vérifier si admin
     */
    public function isAdmin()
    {
        return $this->role === 'admin';
    }
    
    /**
     * Vérifier si manager
     */
    public function isManager()
    {
        return in_array($this->role, ['admin', 'manager']);
    }
}
```

---

### ÉTAPE 3 : Modèle Project

```bash
nano app/Models/Project.php
```

```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Project extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
        'description',
        'status',
        'owner_id',
        'start_date',
        'end_date',
        'color',
    ];

    protected $casts = [
        'start_date' => 'date',
        'end_date' => 'date',
    ];

    // ═══════════════════════════════════════════════════════════
    // RELATIONS
    // ═══════════════════════════════════════════════════════════
    
    /**
     * Propriétaire du projet
     */
    public function owner()
    {
        return $this->belongsTo(User::class, 'owner_id');
    }
    
    /**
     * Tâches du projet
     */
    public function tasks()
    {
        return $this->hasMany(Task::class);
    }
    
    /**
     * Commentaires du projet (polymorphique)
     */
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
    
    // ═══════════════════════════════════════════════════════════
    // SCOPES (filtres réutilisables)
    // ═══════════════════════════════════════════════════════════
    
    /**
     * Scope : Projets actifs uniquement
     */
    public function scopeActive($query)
    {
        return $query->where('status', 'active');
    }
    
    /**
     * Scope : Projets d'un utilisateur
     */
    public function scopeOwnedBy($query, $userId)
    {
        return $query->where('owner_id', $userId);
    }
    
    // ═══════════════════════════════════════════════════════════
    // ACCESSORS
    // ═══════════════════════════════════════════════════════════
    
    /**
     * Pourcentage de complétion
     */
    public function getCompletionPercentageAttribute()
    {
        $totalTasks = $this->tasks()->count();
        
        if ($totalTasks === 0) {
            return 0;
        }
        
        $completedTasks = $this->tasks()->where('status', 'done')->count();
        
        return round(($completedTasks / $totalTasks) * 100);
    }
    
    /**
     * Statut formaté
     */
    public function getStatusLabelAttribute()
    {
        return match($this->status) {
            'active' => 'Actif',
            'on_hold' => 'En pause',
            'completed' => 'Terminé',
            'cancelled' => 'Annulé',
            default => $this->status,
        };
    }
}
```

---

### ÉTAPE 4 : Modèle Task

```bash
nano app/Models/Task.php
```

```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Task extends Model
{
    use HasFactory;

    protected $fillable = [
        'title',
        'description',
        'status',
        'priority',
        'project_id',
        'assigned_to',
        'created_by',
        'due_date',
        'completed_at',
        'estimated_hours',
        'actual_hours',
    ];

    protected $casts = [
        'due_date' => 'date',
        'completed_at' => 'datetime',
        'estimated_hours' => 'integer',
        'actual_hours' => 'integer',
    ];

    // ═══════════════════════════════════════════════════════════
    // RELATIONS
    // ═══════════════════════════════════════════════════════════
    
    public function project()
    {
        return $this->belongsTo(Project::class);
    }
    
    public function assignedUser()
    {
        return $this->belongsTo(User::class, 'assigned_to');
    }
    
    public function creator()
    {
        return $this->belongsTo(User::class, 'created_by');
    }
    
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
    
    // ═══════════════════════════════════════════════════════════
    // SCOPES
    // ═══════════════════════════════════════════════════════════
    
    public function scopeAssignedTo($query, $userId)
    {
        return $query->where('assigned_to', $userId);
    }
    
    public function scopeByStatus($query, $status)
    {
        return $query->where('status', $status);
    }
    
    public function scopeOverdue($query)
    {
        return $query->where('due_date', '<', now())
                     ->whereNotIn('status', ['done']);
    }
    
    public function scopeHighPriority($query)
    {
        return $query->whereIn('priority', ['high', 'urgent']);
    }
    
    // ═══════════════════════════════════════════════════════════
    // ACCESSORS & MUTATORS
    // ═══════════════════════════════════════════════════════════
    
    public function getIsOverdueAttribute()
    {
        if ($this->status === 'done' || !$this->due_date) {
            return false;
        }
        
        return $this->due_date->isPast();
    }
    
    public function getPriorityLabelAttribute()
    {
        return match($this->priority) {
            'low' => 'Basse',
            'medium' => 'Moyenne',
            'high' => 'Haute',
            'urgent' => 'Urgente',
            default => $this->priority,
        };
    }
    
    public function getStatusBadgeAttribute()
    {
        return match($this->status) {
            'todo' => 'badge-secondary',
            'in_progress' => 'badge-primary',
            'review' => 'badge-warning',
            'done' => 'badge-success',
            default => 'badge-light',
        };
    }
}
```

---

### ÉTAPE 5 : Modèle Comment

```bash
nano app/Models/Comment.php
```

```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    use HasFactory;

    protected $fillable = [
        'content',
        'commentable_type',
        'commentable_id',
        'user_id',
    ];

    // ═══════════════════════════════════════════════════════════
    // RELATIONS
    // ═══════════════════════════════════════════════════════════
    
    /**
     * Élément commenté (Task ou Project)
     */
    public function commentable()
    {
        return $this->morphTo();
    }
    
    /**
     * Auteur du commentaire
     */
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}
```

---

## [OBJECTIF] PARTIE 4 : SEEDERS (DONNÉES DE TEST)

### ÉTAPE 1 : Créer Seeders

```bash
php artisan make:seeder UserSeeder
php artisan make:seeder ProjectSeeder
php artisan make:seeder TaskSeeder
```

---

### ÉTAPE 2 : UserSeeder

```bash
nano database/seeders/UserSeeder.php
```

```php
<?php

namespace Database\Seeders;

use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;

class UserSeeder extends Seeder
{
    public function run(): void
    {
        // Admin
        User::create([
            'name' => 'Admin User',
            'email' => 'admin@task-manager.com',
            'password' => Hash::make('password'),
            'role' => 'admin',
            'is_active' => true,
        ]);
        
        // Manager
        User::create([
            'name' => 'Fatou Sall',
            'email' => 'fatou@task-manager.com',
            'password' => Hash::make('password'),
            'role' => 'manager',
            'is_active' => true,
        ]);
        
        // Developers
        User::create([
            'name' => 'Amadou Diop',
            'email' => 'amadou@task-manager.com',
            'password' => Hash::make('password'),
            'role' => 'developer',
            'is_active' => true,
        ]);
        
        User::create([
            'name' => 'Moussa Kane',
            'email' => 'moussa@task-manager.com',
            'password' => Hash::make('password'),
            'role' => 'developer',
            'is_active' => true,
        ]);
    }
}
```

---

### ÉTAPE 3 : ProjectSeeder

```bash
nano database/seeders/ProjectSeeder.php
```

```php
<?php

namespace Database\Seeders;

use App\Models\Project;
use Illuminate\Database\Seeder;

class ProjectSeeder extends Seeder
{
    public function run(): void
    {
        Project::create([
            'name' => 'Refonte site web',
            'description' => 'Modernisation complète du site corporate',
            'status' => 'active',
            'owner_id' => 2, // Fatou (Manager)
            'start_date' => now(),
            'end_date' => now()->addMonths(3),
            'color' => '#3490dc',
        ]);
        
        Project::create([
            'name' => 'Application mobile',
            'description' => 'Développement app iOS/Android',
            'status' => 'active',
            'owner_id' => 2,
            'start_date' => now(),
            'end_date' => now()->addMonths(6),
            'color' => '#38c172',
        ]);
        
        Project::create([
            'name' => 'Migration serveurs',
            'description' => 'Migration infrastructure vers cloud',
            'status' => 'on_hold',
            'owner_id' => 1, // Admin
            'start_date' => now()->addMonth(),
            'color' => '#f6993f',
        ]);
    }
}
```

---

### ÉTAPE 4 : TaskSeeder

```bash
nano database/seeders/TaskSeeder.php
```

```php
<?php

namespace Database\Seeders;

use App\Models\Task;
use Illuminate\Database\Seeder;

class TaskSeeder extends Seeder
{
    public function run(): void
    {
        // Projet 1 : Refonte site web
        Task::create([
            'title' => 'Créer maquettes Figma',
            'description' => 'Maquettes pour les 5 pages principales',
            'status' => 'done',
            'priority' => 'high',
            'project_id' => 1,
            'assigned_to' => 3, // Amadou
            'created_by' => 2,  // Fatou
            'due_date' => now()->subDays(5),
            'completed_at' => now()->subDays(2),
            'estimated_hours' => 16,
            'actual_hours' => 14,
        ]);
        
        Task::create([
            'title' => 'Intégration HTML/CSS',
            'description' => 'Intégration responsive des maquettes',
            'status' => 'in_progress',
            'priority' => 'high',
            'project_id' => 1,
            'assigned_to' => 3,
            'created_by' => 2,
            'due_date' => now()->addDays(7),
            'estimated_hours' => 24,
        ]);
        
        Task::create([
            'title' => 'Backend API REST',
            'description' => 'Développer API pour formulaires',
            'status' => 'todo',
            'priority' => 'medium',
            'project_id' => 1,
            'assigned_to' => 4, // Moussa
            'created_by' => 2,
            'due_date' => now()->addDays(14),
            'estimated_hours' => 32,
        ]);
        
        // Projet 2 : App mobile
        Task::create([
            'title' => 'Setup projet React Native',
            'description' => 'Initialiser projet et dépendances',
            'status' => 'done',
            'priority' => 'high',
            'project_id' => 2,
            'assigned_to' => 4,
            'created_by' => 2,
            'due_date' => now()->subDays(3),
            'completed_at' => now()->subDay(),
            'estimated_hours' => 8,
            'actual_hours' => 6,
        ]);
        
        Task::create([
            'title' => 'Écrans authentification',
            'description' => 'Login, Register, Forgot Password',
            'status' => 'in_progress',
            'priority' => 'high',
            'project_id' => 2,
            'assigned_to' => 4,
            'created_by' => 2,
            'due_date' => now()->addDays(10),
            'estimated_hours' => 20,
        ]);
        
        // Tâche en retard
        Task::create([
            'title' => 'Tests unitaires',
            'description' => 'Écrire tests pour module auth',
            'status' => 'todo',
            'priority' => 'urgent',
            'project_id' => 2,
            'assigned_to' => 3,
            'created_by' => 2,
            'due_date' => now()->subDays(2), // EN RETARD !
            'estimated_hours' => 12,
        ]);
    }
}
```

---

### ÉTAPE 5 : DatabaseSeeder (orchestrateur)

```bash
nano database/seeders/DatabaseSeeder.php
```

```php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        $this->call([
            UserSeeder::class,
            ProjectSeeder::class,
            TaskSeeder::class,
        ]);
    }
}
```

---

### ÉTAPE 6 : Exécuter Seeders

```bash
# Lancer tous les seeders
php artisan db:seed

# OU migration + seed en 1 commande
php artisan migrate:fresh --seed
```

**Résultat :**

```
Seeding: Database\Seeders\UserSeeder
Seeded:  Database\Seeders\UserSeeder (XX.XXms)
Seeding: Database\Seeders\ProjectSeeder
Seeded:  Database\Seeders\ProjectSeeder (XX.XXms)
Seeding: Database\Seeders\TaskSeeder
Seeded:  Database\Seeders\TaskSeeder (XX.XXms)
```

**[OK] Base de données peuplée avec données réalistes !**

---

## [DESIGN] PARTIE 5 : AUTHENTIFICATION

### ÉTAPE 1 : Installer Laravel Breeze

**Laravel Breeze = Scaffolding authentification simple**

```bash
cd /var/www/task-manager

# Installer Breeze
composer require laravel/breeze --dev

# Installer scaffolding Blade (pas React/Vue)
php artisan breeze:install blade

# Installer dépendances npm et compiler assets
npm install
npm run build

# Exécuter migrations (si pas déjà fait)
php artisan migrate
```

**Breeze crée automatiquement :**
- Routes auth (login, register, logout)
- Controllers
- Views (login.blade.php, register.blade.php, etc.)
- Middleware

---

### ÉTAPE 2 : Tester l'authentification

**Ouvrir :** `http://task-manager.local/login`

**Page de login apparaît ! [BRAVO]**

**S'inscrire ou se connecter :**

```
Email: admin@task-manager.com
Password: password
```

**[OK] Dashboard Breeze s'affiche !**

---

### ÉTAPE 3 : Personnaliser layout

```bash
nano resources/views/layouts/app.blade.php
```

**Modifier le titre et la navigation :**

```blade
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="csrf-token" content="{{ csrf_token() }}">

    <title>{{ config('app.name', 'Task Manager') }}</title>

    <!-- Fonts -->
    <link rel="preconnect" href="https://fonts.bunny.net">
    <link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />

    <!-- Scripts -->
    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans antialiased">
    <div class="min-h-screen bg-gray-100">
        @include('layouts.navigation')

        <!-- Page Heading -->
        @if (isset($header))
            <header class="bg-white shadow">
                <div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
                    {{ $header }}
                </div>
            </header>
        @endif

        <!-- Page Content -->
        <main>
            {{ $slot }}
        </main>
    </div>
</body>
</html>
```

---

Je continue avec les Controllers, Routes et Views dans la prochaine section...

## [VIDEO_GAME] PARTIE 6 : CONTROLLERS & ROUTES

### ÉTAPE 1 : Créer Controllers

```bash
# Dashboard
php artisan make:controller DashboardController

# Projects (Resource = CRUD complet auto)
php artisan make:controller ProjectController --resource

# Tasks
php artisan make:controller TaskController --resource

# Comments
php artisan make:controller CommentController
```

**Fichiers créés dans `app/Http/Controllers/` :**

---

### ÉTAPE 2 : DashboardController

```bash
nano app/Http/Controllers/DashboardController.php
```

```php
<?php

namespace App\Http\Controllers;

use App\Models\Project;
use App\Models\Task;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class DashboardController extends Controller
{
    public function index()
    {
        $user = Auth::user();
        
        // Statistiques
        $stats = [
            'total_projects' => Project::count(),
            'my_projects' => Project::ownedBy($user->id)->count(),
            'total_tasks' => Task::count(),
            'my_tasks' => Task::assignedTo($user->id)->count(),
            'pending_tasks' => Task::assignedTo($user->id)
                                   ->whereIn('status', ['todo', 'in_progress'])
                                   ->count(),
            'overdue_tasks' => Task::assignedTo($user->id)
                                   ->overdue()
                                   ->count(),
        ];
        
        // Tâches récentes assignées
        $recentTasks = Task::assignedTo($user->id)
                           ->with(['project', 'creator'])
                           ->latest()
                           ->take(5)
                           ->get();
        
        // Projets actifs
        $activeProjects = Project::active()
                                 ->with('owner')
                                 ->latest()
                                 ->take(5)
                                 ->get();
        
        return view('dashboard', compact('stats', 'recentTasks', 'activeProjects'));
    }
}
```

---

### ÉTAPE 3 : ProjectController

```bash
nano app/Http/Controllers/ProjectController.php
```

```php
<?php

namespace App\Http\Controllers;

use App\Models\Project;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class ProjectController extends Controller
{
    /**
     * Liste des projets
     */
    public function index()
    {
        $projects = Project::with('owner')
                           ->latest()
                           ->paginate(10);
        
        return view('projects.index', compact('projects'));
    }

    /**
     * Formulaire création
     */
    public function create()
    {
        return view('projects.create');
    }

    /**
     * Enregistrer nouveau projet
     */
    public function store(Request $request)
    {
        $validated = $request->validate([
            'name' => 'required|string|max:255',
            'description' => 'nullable|string',
            'status' => 'required|in:active,on_hold,completed,cancelled',
            'start_date' => 'nullable|date',
            'end_date' => 'nullable|date|after_or_equal:start_date',
            'color' => 'nullable|string|max:7',
        ]);
        
        $project = Project::create([
            ...$validated,
            'owner_id' => Auth::id(),
        ]);
        
        return redirect()
            ->route('projects.show', $project)
            ->with('success', 'Projet créé avec succès !');
    }

    /**
     * Afficher un projet
     */
    public function show(Project $project)
    {
        $project->load(['owner', 'tasks.assignedUser']);
        
        // Regrouper tâches par statut
        $tasksByStatus = $project->tasks->groupBy('status');
        
        return view('projects.show', compact('project', 'tasksByStatus'));
    }

    /**
     * Formulaire édition
     */
    public function edit(Project $project)
    {
        // Vérifier autorisation
        if ($project->owner_id !== Auth::id() && !Auth::user()->isAdmin()) {
            abort(403, 'Action non autorisée.');
        }
        
        return view('projects.edit', compact('project'));
    }

    /**
     * Mettre à jour projet
     */
    public function update(Request $request, Project $project)
    {
        // Vérifier autorisation
        if ($project->owner_id !== Auth::id() && !Auth::user()->isAdmin()) {
            abort(403);
        }
        
        $validated = $request->validate([
            'name' => 'required|string|max:255',
            'description' => 'nullable|string',
            'status' => 'required|in:active,on_hold,completed,cancelled',
            'start_date' => 'nullable|date',
            'end_date' => 'nullable|date|after_or_equal:start_date',
            'color' => 'nullable|string|max:7',
        ]);
        
        $project->update($validated);
        
        return redirect()
            ->route('projects.show', $project)
            ->with('success', 'Projet modifié avec succès !');
    }

    /**
     * Supprimer projet
     */
    public function destroy(Project $project)
    {
        // Seul admin ou owner peut supprimer
        if ($project->owner_id !== Auth::id() && !Auth::user()->isAdmin()) {
            abort(403);
        }
        
        $project->delete();
        
        return redirect()
            ->route('projects.index')
            ->with('success', 'Projet supprimé avec succès !');
    }
}
```

---

### ÉTAPE 4 : Routes web

```bash
nano routes/web.php
```

```php
<?php

use App\Http\Controllers\DashboardController;
use App\Http\Controllers\ProjectController;
use App\Http\Controllers\TaskController;
use App\Http\Controllers\CommentController;
use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Routes publiques
|--------------------------------------------------------------------------
*/

Route::get('/', function () {
    return redirect()->route('login');
});

/*
|--------------------------------------------------------------------------
| Routes authentifiées
|--------------------------------------------------------------------------
*/

Route::middleware(['auth', 'verified'])->group(function () {
    
    // Dashboard
    Route::get('/dashboard', [DashboardController::class, 'index'])
         ->name('dashboard');
    
    // Projects (CRUD complet)
    Route::resource('projects', ProjectController::class);
    
    // Tasks (CRUD complet)
    Route::resource('tasks', TaskController::class);
    
    // Quick actions
    Route::patch('/tasks/{task}/status', [TaskController::class, 'updateStatus'])
         ->name('tasks.update-status');
    
    // Comments
    Route::post('/tasks/{task}/comments', [CommentController::class, 'store'])
         ->name('comments.store');
    
    Route::delete('/comments/{comment}', [CommentController::class, 'destroy'])
         ->name('comments.destroy');
});

/*
|--------------------------------------------------------------------------
| Routes profil (Breeze)
|--------------------------------------------------------------------------
*/

Route::middleware('auth')->group(function () {
    Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
    Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
    Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});

require __DIR__.'/auth.php';
```

---

### ÉTAPE 5 : Routes API (optionnel)

```bash
nano routes/api.php
```

```php
<?php

use App\Http\Controllers\Api\ProjectController;
use App\Http\Controllers\Api\TaskController;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
*/

Route::middleware('auth:sanctum')->group(function () {
    
    // Projects API
    Route::apiResource('projects', ProjectController::class);
    
    // Tasks API
    Route::apiResource('tasks', TaskController::class);
    
    // Stats
    Route::get('/stats', function () {
        return response()->json([
            'projects_count' => \App\Models\Project::count(),
            'tasks_count' => \App\Models\Task::count(),
            'users_count' => \App\Models\User::count(),
        ]);
    });
});
```

---

## [DESIGN] PARTIE 7 : VIEWS BLADE (TEMPLATES)

### ÉTAPE 1 : Layout principal amélioré

```bash
nano resources/views/layouts/app.blade.php
```

**Ajouter styles et navigation :**

```blade
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>{{ config('app.name') }} - @yield('title', 'Dashboard')</title>
    @vite(['resources/css/app.css', 'resources/js/app.js'])
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
</head>
<body>
    <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
        <div class="container-fluid">
            <a class="navbar-brand" href="{{ route('dashboard') }}">
                <i class="bi bi-kanban"></i> Task Manager
            </a>
            <div class="collapse navbar-collapse">
                <ul class="navbar-nav me-auto">
                    <li class="nav-item">
                        <a class="nav-link" href="{{ route('dashboard') }}">Dashboard</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{{ route('projects.index') }}">Projets</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{{ route('tasks.index') }}">Tâches</a>
                    </li>
                </ul>
                <ul class="navbar-nav">
                    <li class="nav-item dropdown">
                        <a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown">
                            {{ Auth::user()->name }}
                        </a>
                        <ul class="dropdown-menu">
                            <li><a class="dropdown-item" href="{{ route('profile.edit') }}">Profil</a></li>
                            <li><hr class="dropdown-divider"></li>
                            <li>
                                <form method="POST" action="{{ route('logout') }}">
                                    @csrf
                                    <button class="dropdown-item" type="submit">Déconnexion</button>
                                </form>
                            </li>
                        </ul>
                    </li>
                </ul>
            </div>
        </div>
    </nav>

    <main class="container mt-4">
        @if(session('success'))
            <div class="alert alert-success alert-dismissible fade show">
                {{ session('success') }}
                <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
            </div>
        @endif
        
        @if($errors->any())
            <div class="alert alert-danger alert-dismissible fade show">
                <ul class="mb-0">
                    @foreach($errors->all() as $error)
                        <li>{{ $error }}</li>
                    @endforeach
                </ul>
                <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
            </div>
        @endif
        
        @yield('content')
    </main>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
    @stack('scripts')
</body>
</html>
```

---

### ÉTAPE 2 : Dashboard

```bash
nano resources/views/dashboard.blade.php
```

```blade
@extends('layouts.app')
@section('title', 'Dashboard')

@section('content')
<div class="row">
    <div class="col-12">
        <h1 class="mb-4">
            <i class="bi bi-speedometer2"></i> Dashboard
            <small class="text-muted">Bienvenue {{ Auth::user()->name }} !</small>
        </h1>
    </div>
</div>

{{-- Statistiques --}}
<div class="row mb-4">
    <div class="col-md-3">
        <div class="card text-white bg-primary">
            <div class="card-body">
                <h5 class="card-title"><i class="bi bi-folder"></i> Mes Projets</h5>
                <h2 class="mb-0">{{ $stats['my_projects'] }}</h2>
            </div>
        </div>
    </div>
    <div class="col-md-3">
        <div class="card text-white bg-success">
            <div class="card-body">
                <h5 class="card-title"><i class="bi bi-check-circle"></i> Mes Tâches</h5>
                <h2 class="mb-0">{{ $stats['my_tasks'] }}</h2>
            </div>
        </div>
    </div>
    <div class="col-md-3">
        <div class="card text-white bg-warning">
            <div class="card-body">
                <h5 class="card-title"><i class="bi bi-clock"></i> En cours</h5>
                <h2 class="mb-0">{{ $stats['pending_tasks'] }}</h2>
            </div>
        </div>
    </div>
    <div class="col-md-3">
        <div class="card text-white bg-danger">
            <div class="card-body">
                <h5 class="card-title"><i class="bi bi-exclamation-triangle"></i> En retard</h5>
                <h2 class="mb-0">{{ $stats['overdue_tasks'] }}</h2>
            </div>
        </div>
    </div>
</div>

{{-- Tâches récentes --}}
<div class="row">
    <div class="col-md-6">
        <div class="card">
            <div class="card-header">
                <h5 class="mb-0"><i class="bi bi-list-task"></i> Mes tâches récentes</h5>
            </div>
            <div class="card-body p-0">
                <div class="list-group list-group-flush">
                    @forelse($recentTasks as $task)
                        <a href="{{ route('tasks.show', $task) }}" class="list-group-item list-group-item-action">
                            <div class="d-flex justify-content-between align-items-center">
                                <div>
                                    <h6 class="mb-1">{{ $task->title }}</h6>
                                    <small class="text-muted">
                                        {{ $task->project->name }} • 
                                        <span class="badge bg-{{ $task->status_badge }}">{{ $task->status }}</span>
                                    </small>
                                </div>
                                @if($task->is_overdue)
                                    <span class="badge bg-danger">En retard</span>
                                @endif
                            </div>
                        </a>
                    @empty
                        <div class="list-group-item text-center text-muted">
                            Aucune tâche assignée
                        </div>
                    @endforelse
                </div>
            </div>
        </div>
    </div>

    {{-- Projets actifs --}}
    <div class="col-md-6">
        <div class="card">
            <div class="card-header">
                <h5 class="mb-0"><i class="bi bi-folder"></i> Projets actifs</h5>
            </div>
            <div class="card-body p-0">
                <div class="list-group list-group-flush">
                    @forelse($activeProjects as $project)
                        <a href="{{ route('projects.show', $project) }}" class="list-group-item list-group-item-action">
                            <div class="d-flex justify-content-between align-items-center">
                                <div>
                                    <h6 class="mb-1">
                                        <span class="badge" style="background-color: {{ $project->color }}">{{ $project->name }}</span>
                                    </h6>
                                    <small class="text-muted">Par {{ $project->owner->name }}</small>
                                </div>
                                <div>
                                    <div class="progress" style="width: 100px;">
                                        <div class="progress-bar" style="width: {{ $project->completion_percentage }}%">
                                            {{ $project->completion_percentage }}%
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </a>
                    @empty
                        <div class="list-group-item text-center text-muted">
                            Aucun projet actif
                        </div>
                    @endforelse
                </div>
            </div>
        </div>
    </div>
</div>
@endsection
```

---

### ÉTAPE 3 : Liste projets

```bash
mkdir -p resources/views/projects
nano resources/views/projects/index.blade.php
```

```blade
@extends('layouts.app')
@section('title', 'Projets')

@section('content')
<div class="d-flex justify-content-between align-items-center mb-4">
    <h1><i class="bi bi-folder"></i> Projets</h1>
    <a href="{{ route('projects.create') }}" class="btn btn-primary">
        <i class="bi bi-plus-lg"></i> Nouveau projet
    </a>
</div>

<div class="row">
    @forelse($projects as $project)
        <div class="col-md-6 col-lg-4 mb-4">
            <div class="card h-100">
                <div class="card-header" style="background-color: {{ $project->color }}; color: white;">
                    <h5 class="mb-0">{{ $project->name }}</h5>
                </div>
                <div class="card-body">
                    <p class="card-text">{{ Str::limit($project->description, 100) }}</p>
                    
                    <div class="mb-2">
                        <small class="text-muted">
                            <i class="bi bi-person"></i> {{ $project->owner->name }}
                        </small>
                    </div>
                    
                    <div class="mb-2">
                        <span class="badge bg-secondary">{{ $project->status_label }}</span>
                        <span class="badge bg-info">{{ $project->tasks_count }} tâches</span>
                    </div>
                    
                    <div class="progress mb-2">
                        <div class="progress-bar bg-success" style="width: {{ $project->completion_percentage }}%">
                            {{ $project->completion_percentage }}%
                        </div>
                    </div>
                </div>
                <div class="card-footer">
                    <a href="{{ route('projects.show', $project) }}" class="btn btn-sm btn-primary">
                        <i class="bi bi-eye"></i> Voir
                    </a>
                    @if($project->owner_id === Auth::id() || Auth::user()->isAdmin())
                        <a href="{{ route('projects.edit', $project) }}" class="btn btn-sm btn-warning">
                            <i class="bi bi-pencil"></i> Modifier
                        </a>
                    @endif
                </div>
            </div>
        </div>
    @empty
        <div class="col-12">
            <div class="alert alert-info text-center">
                <i class="bi bi-info-circle"></i> Aucun projet. Créez-en un !
            </div>
        </div>
    @endforelse
</div>

<div class="d-flex justify-content-center">
    {{ $projects->links() }}
</div>
@endsection
```

---

## [OK] PARTIE 8 : TESTS AUTOMATISÉS

### ÉTAPE 1 : Configuration PHPUnit

```bash
# Copier .env pour tests
cp .env .env.testing
```

**Modifier `.env.testing` :**

```ini
DB_CONNECTION=mysql
DB_DATABASE=task_manager_test
```

**Créer base de test :**

```bash
sudo mysql -u root -p
```

```sql
CREATE DATABASE task_manager_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
GRANT ALL ON task_manager_test.* TO 'laravel_user'@'localhost';
EXIT;
```

---

### ÉTAPE 2 : Test Feature - Projects

```bash
php artisan make:test ProjectTest
```

```bash
nano tests/Feature/ProjectTest.php
```

```php
<?php

namespace Tests\Feature;

use App\Models\User;
use App\Models\Project;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class ProjectTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function authenticated_users_can_view_projects()
    {
        $user = User::factory()->create();
        
        $response = $this->actingAs($user)->get('/projects');
        
        $response->assertStatus(200);
    }

    /** @test */
    public function users_can_create_projects()
    {
        $user = User::factory()->create();
        
        $projectData = [
            'name' => 'Test Project',
            'description' => 'Test Description',
            'status' => 'active',
        ];
        
        $response = $this->actingAs($user)->post('/projects', $projectData);
        
        $response->assertRedirect();
        $this->assertDatabaseHas('projects', ['name' => 'Test Project']);
    }

    /** @test */
    public function only_owner_or_admin_can_edit_project()
    {
        $owner = User::factory()->create();
        $otherUser = User::factory()->create(['role' => 'developer']);
        $project = Project::factory()->create(['owner_id' => $owner->id]);
        
        // Propriétaire peut éditer
        $response = $this->actingAs($owner)->get("/projects/{$project->id}/edit");
        $response->assertStatus(200);
        
        // Autre utilisateur ne peut pas
        $response = $this->actingAs($otherUser)->get("/projects/{$project->id}/edit");
        $response->assertStatus(403);
    }
}
```

**Lancer tests :**

```bash
php artisan test

# OU spécifique
php artisan test --filter=ProjectTest
```

---

## [RAPIDE] PARTIE 9 : API REST

### Créer API Controllers

```bash
mkdir -p app/Http/Controllers/Api
php artisan make:controller Api/TaskController --api
```

```bash
nano app/Http/Controllers/Api/TaskController.php
```

```php
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Task;
use Illuminate\Http\Request;

class TaskController extends Controller
{
    public function index()
    {
        $tasks = Task::with(['project', 'assignedUser'])->get();
        
        return response()->json($tasks);
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'project_id' => 'required|exists:projects,id',
            'status' => 'in:todo,in_progress,review,done',
        ]);
        
        $task = Task::create($validated);
        
        return response()->json($task, 201);
    }

    public function show(Task $task)
    {
        $task->load(['project', 'assignedUser', 'comments.user']);
        
        return response()->json($task);
    }

    public function update(Request $request, Task $task)
    {
        $validated = $request->validate([
            'title' => 'string|max:255',
            'status' => 'in:todo,in_progress,review,done',
        ]);
        
        $task->update($validated);
        
        return response()->json($task);
    }

    public function destroy(Task $task)
    {
        $task->delete();
        
        return response()->json(null, 204);
    }
}
```

---

## [RAPIDE] PARTIE 10 : OPTIMISATION PERFORMANCES

### ÉTAPE 1 : Cache configuration

```bash
# Cache routes
php artisan route:cache

# Cache config
php artisan config:cache

# Cache views
php artisan view:cache

# Optimiser autoload Composer
composer install --optimize-autoloader --no-dev
```

---

### ÉTAPE 2 : Eager Loading (éviter N+1)

```php
// [X] BAD : N+1 queries
$tasks = Task::all();
foreach($tasks as $task) {
    echo $task->project->name; // Requête par tâche !
}

// [OK] GOOD : 2 queries seulement
$tasks = Task::with('project')->get();
foreach($tasks as $task) {
    echo $task->project->name;
}
```

---

### ÉTAPE 3 : Queue pour emails

```bash
# Configurer queue
php artisan queue:table
php artisan migrate

# Créer job
php artisan make:job SendTaskAssignedEmail
```

---

## [COURS] CONCLUSION EXERCICE LARAVEL

**[BRAVO] FÉLICITATIONS ! Application Laravel complète créée !**

**Compétences acquises :**
- [OK] Architecture MVC professionnelle
- [OK] Eloquent ORM (relations, scopes)
- [OK] Migrations & Seeders
- [OK] Authentication (Laravel Breeze)
- [OK] Authorization (Policies)
- [OK] Blade templates
- [OK] API REST
- [OK] Tests automatisés (PHPUnit)
- [OK] Performance optimization

**[PRO] Valeur marché :**
- Développeur Laravel Junior : 35-50K€/an
- Développeur Laravel Senior : 60-90K€/an
- Freelance Laravel : 400-800€/jour

**[GRAPHIQUE] Laravel en chiffres :**
- 78% des développeurs PHP l'utilisent
- Framework PHP n°1 mondial
- 1,5 million de sites
- Communauté de 600 000+ développeurs

**Tu maîtrises maintenant le framework PHP professionnel de référence ! [RAPIDE]**