Maîtriser PostgreSQL
Thème :
Intermédiaire
1 vues ·7/9 leçons

CRUD avec PDO PostgreSQL

code complet du CRUD convertis avec PDO pour PostgreSQL :

1. Configuration Database (config/database.php)

<?php
declare(strict_types=1);

// Configuration PostgreSQL
const DB_HOST = "localhost";
const DB_PORT = "5432";
const DB_NAME = "shop_db";
const DB_USER = "postgres";
const DB_PASS = "postgres";

/**
 * Établit une connexion PDO à PostgreSQL
 *
 * @return PDO Connexion PDO
 * @throws PDOException En cas d'erreur de connexion
 */
function db_connect(): PDO {
    static $pdo = null;

    if ($pdo !== null) {
        return $pdo;
    }

    try {
        $dsn = sprintf(
            "pgsql:host=%s;port=%s;dbname=%s",
            DB_HOST,
            DB_PORT,
            DB_NAME
        );

        $pdo = new PDO($dsn, DB_USER, DB_PASS);

        // Configuration PDO
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
        $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

        // Encodage UTF-8
        $pdo->exec("SET NAMES utf8");
        $pdo->exec("SET search_path TO public");

        return $pdo;
    } catch (PDOException $e) {
        error_log("Erreur de connexion PostgreSQL: " . $e->getMessage());
        throw new PDOException("Impossible de se connecter à la base de données");
    }
}

/**
 * Ferme la connexion PDO
 */
function db_close(): void {
    global $pdo;
    $pdo = null;
}

2. Fonctions Utilitaires (includes/functions.php)

<?php
declare(strict_types=1);

require_once __DIR__ . '/../config/database.php';

/**
 * Échappe les caractères HTML pour éviter XSS
 */
function h(string $text): string {
    return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
}

/**
 * Redirige vers une URL
 */
function redirect(string $url): void {
    header("Location: $url");
    exit;
}

/**
 * Définit un message flash en session
 */
function set_flash(string $type, string $message): void {
    if (session_status() === PHP_SESSION_NONE) {
        session_start();
    }
    $_SESSION['flash'] = ['type' => $type, 'message' => $message];
}

/**
 * Récupère et supprime le message flash
 */
function get_flash(): ?array {
    if (session_status() === PHP_SESSION_NONE) {
        session_start();
    }

    if (isset($_SESSION['flash'])) {
        $flash = $_SESSION['flash'];
        unset($_SESSION['flash']);
        return $flash;
    }

    return null;
}

/**
 * Affiche le message flash en HTML
 */
function display_flash(): void {
    $flash = get_flash();
    if ($flash) {
        $class = $flash['type'] === 'success' ? 'alert-success' : 'alert-danger';
        echo "<div class='alert {$class}'>" . h($flash['message']) . "</div>";
    }
}

/**
 * Valide un entier positif
 */
function validate_positive_int(mixed $value): ?int {
    $filtered = filter_var($value, FILTER_VALIDATE_INT);
    return ($filtered !== false && $filtered > 0) ? $filtered : null;
}

/**
 * Valide un prix (nombre décimal positif)
 */
function validate_price(mixed $value): ?float {
    $filtered = filter_var($value, FILTER_VALIDATE_FLOAT);
    return ($filtered !== false && $filtered >= 0) ? $filtered : null;
}

/**
 * Valide une chaîne non vide
 */
function validate_string(mixed $value, int $max_length = 255): ?string {
    if (!is_string($value)) {
        return null;
    }

    $trimmed = trim($value);

    if ($trimmed === '' || mb_strlen($trimmed) > $max_length) {
        return null;
    }

    return $trimmed;
}

/**
 * Formate un prix pour l'affichage
 */
function format_price(float $price): string {
    return number_format($price, 2, ',', ' ') . ' €';
}

/**
 * Formate une date PostgreSQL pour l'affichage
 */
function format_date(string $date): string {
    $timestamp = strtotime($date);
    return date('d/m/Y H:i', $timestamp);
}

/**
 * Pagination : calcule le nombre de pages
 */
function calculate_pages(int $total_items, int $items_per_page): int {
    return (int) ceil($total_items / $items_per_page);
}

/**
 * Pagination : calcule l'offset SQL
 */
function calculate_offset(int $page, int $items_per_page): int {
    return ($page - 1) * $items_per_page;
}

3. Requêtes SQL avec PDO (includes/queries.php)

<?php
declare(strict_types=1);

require_once __DIR__ . '/functions.php';

// ==================== CATEGORIES ====================

/**
 * Récupère toutes les catégories avec pagination
 */
function get_all_categories(PDO $pdo, int $limit = 20, int $offset = 0): array {
    try {
        $query = "
            SELECT id, name, created_at, updated_at
            FROM categories
            ORDER BY name ASC
            LIMIT :limit OFFSET :offset
        ";

        $stmt = $pdo->prepare($query);
        $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
        $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
        $stmt->execute();

        return $stmt->fetchAll();
    } catch (PDOException $e) {
        error_log("Erreur requête categories: " . $e->getMessage());
        return [];
    }
}

/**
 * Compte le nombre total de catégories
 */
function count_categories(PDO $pdo): int {
    try {
        $stmt = $pdo->query("SELECT COUNT(*) as count FROM categories");
        $result = $stmt->fetch();
        return (int) $result['count'];
    } catch (PDOException $e) {
        error_log("Erreur count categories: " . $e->getMessage());
        return 0;
    }
}

/**
 * Récupère une catégorie par son ID
 */
function get_category_by_id(PDO $pdo, int $id): ?array {
    try {
        $stmt = $pdo->prepare("SELECT * FROM categories WHERE id = :id");
        $stmt->bindValue(':id', $id, PDO::PARAM_INT);
        $stmt->execute();

        $result = $stmt->fetch();
        return $result ?: null;
    } catch (PDOException $e) {
        error_log("Erreur get_category_by_id: " . $e->getMessage());
        return null;
    }
}

/**
 * Crée une nouvelle catégorie
 */
function create_category(PDO $pdo, string $name): int|false {
    try {
        $stmt = $pdo->prepare("
            INSERT INTO categories (name)
            VALUES (:name)
            RETURNING id
        ");

        $stmt->bindValue(':name', $name);
        $stmt->execute();

        $result = $stmt->fetch();
        return $result ? (int) $result['id'] : false;
    } catch (PDOException $e) {
        error_log("Erreur create_category: " . $e->getMessage());
        return false;
    }
}

/**
 * Met à jour une catégorie
 */
function update_category(PDO $pdo, int $id, string $name): bool {
    try {
        $stmt = $pdo->prepare("
            UPDATE categories
            SET name = :name
            WHERE id = :id
        ");

        $stmt->bindValue(':name', $name);
        $stmt->bindValue(':id', $id, PDO::PARAM_INT);

        return $stmt->execute();
    } catch (PDOException $e) {
        error_log("Erreur update_category: " . $e->getMessage());
        return false;
    }
}

/**
 * Supprime une catégorie (seulement si aucun produit associé)
 */
function delete_category(PDO $pdo, int $id): bool {
    try {
        // Vérifier s'il y a des produits
        $check_stmt = $pdo->prepare("SELECT COUNT(*) as count FROM products WHERE category_id = :id");
        $check_stmt->bindValue(':id', $id, PDO::PARAM_INT);
        $check_stmt->execute();

        $row = $check_stmt->fetch();
        if ((int) $row['count'] > 0) {
            return false; // Impossible de supprimer, il y a des produits
        }

        $stmt = $pdo->prepare("DELETE FROM categories WHERE id = :id");
        $stmt->bindValue(':id', $id, PDO::PARAM_INT);

        return $stmt->execute();
    } catch (PDOException $e) {
        error_log("Erreur delete_category: " . $e->getMessage());
        return false;
    }
}

// ==================== PRODUCTS ====================

/**
 * Récupère tous les produits avec leurs catégories (JOINTURE OPTIMISÉE)
 */
function get_all_products(PDO $pdo, ?int $category_id = null, int $limit = 20, int $offset = 0): array {
    try {
        if ($category_id !== null) {
            $query = "
                SELECT
                    p.id, p.name, p.description, p.price, p.quantity,
                    p.created_at, p.updated_at, p.category_id,
                    c.name AS category_name
                FROM products p
                INNER JOIN categories c ON p.category_id = c.id
                WHERE p.category_id = :category_id
                ORDER BY p.name ASC
                LIMIT :limit OFFSET :offset
            ";

            $stmt = $pdo->prepare($query);
            $stmt->bindValue(':category_id', $category_id, PDO::PARAM_INT);
        } else {
            $query = "
                SELECT
                    p.id, p.name, p.description, p.price, p.quantity,
                    p.created_at, p.updated_at, p.category_id,
                    c.name AS category_name
                FROM products p
                INNER JOIN categories c ON p.category_id = c.id
                ORDER BY p.name ASC
                LIMIT :limit OFFSET :offset
            ";

            $stmt = $pdo->prepare($query);
        }

        $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
        $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
        $stmt->execute();

        return $stmt->fetchAll();
    } catch (PDOException $e) {
        error_log("Erreur get_all_products: " . $e->getMessage());
        return [];
    }
}

/**
 * Compte le nombre total de produits
 */
function count_products(PDO $pdo, ?int $category_id = null): int {
    try {
        if ($category_id !== null) {
            $stmt = $pdo->prepare("SELECT COUNT(*) as count FROM products WHERE category_id = :category_id");
            $stmt->bindValue(':category_id', $category_id, PDO::PARAM_INT);
        } else {
            $stmt = $pdo->query("SELECT COUNT(*) as count FROM products");
        }

        $stmt->execute();
        $result = $stmt->fetch();
        return (int) $result['count'];
    } catch (PDOException $e) {
        error_log("Erreur count_products: " . $e->getMessage());
        return 0;
    }
}

/**
 * Récupère un produit par son ID avec sa catégorie
 */
function get_product_by_id(PDO $pdo, int $id): ?array {
    try {
        $query = "
            SELECT
                p.*, c.name AS category_name
            FROM products p
            INNER JOIN categories c ON p.category_id = c.id
            WHERE p.id = :id
        ";

        $stmt = $pdo->prepare($query);
        $stmt->bindValue(':id', $id, PDO::PARAM_INT);
        $stmt->execute();

        $result = $stmt->fetch();
        return $result ?: null;
    } catch (PDOException $e) {
        error_log("Erreur get_product_by_id: " . $e->getMessage());
        return null;
    }
}

/**
 * Crée un nouveau produit
 */
function create_product(
    PDO $pdo,
    string $name,
    ?string $description,
    float $price,
    int $quantity,
    int $category_id
): int|false {
    try {
        $stmt = $pdo->prepare("
            INSERT INTO products (name, description, price, quantity, category_id)
            VALUES (:name, :description, :price, :quantity, :category_id)
            RETURNING id
        ");

        $stmt->bindValue(':name', $name);
        $stmt->bindValue(':description', $description);
        $stmt->bindValue(':price', $price);
        $stmt->bindValue(':quantity', $quantity, PDO::PARAM_INT);
        $stmt->bindValue(':category_id', $category_id, PDO::PARAM_INT);

        $stmt->execute();

        $result = $stmt->fetch();
        return $result ? (int) $result['id'] : false;
    } catch (PDOException $e) {
        error_log("Erreur create_product: " . $e->getMessage());
        return false;
    }
}

/**
 * Met à jour un produit
 */
function update_product(
    PDO $pdo,
    int $id,
    string $name,
    ?string $description,
    float $price,
    int $quantity,
    int $category_id
): bool {
    try {
        $stmt = $pdo->prepare("
            UPDATE products
            SET name = :name,
                description = :description,
                price = :price,
                quantity = :quantity,
                category_id = :category_id
            WHERE id = :id
        ");

        $stmt->bindValue(':name', $name);
        $stmt->bindValue(':description', $description);
        $stmt->bindValue(':price', $price);
        $stmt->bindValue(':quantity', $quantity, PDO::PARAM_INT);
        $stmt->bindValue(':category_id', $category_id, PDO::PARAM_INT);
        $stmt->bindValue(':id', $id, PDO::PARAM_INT);

        return $stmt->execute();
    } catch (PDOException $e) {
        error_log("Erreur update_product: " . $e->getMessage());
        return false;
    }
}

/**
 * Supprime un produit
 */
function delete_product(PDO $pdo, int $id): bool {
    try {
        $stmt = $pdo->prepare("DELETE FROM products WHERE id = :id");
        $stmt->bindValue(':id', $id, PDO::PARAM_INT);

        return $stmt->execute();
    } catch (PDOException $e) {
        error_log("Erreur delete_product: " . $e->getMessage());
        return false;
    }
}

/**
 * Recherche de produits par nom (OPTIMISÉ avec ILIKE)
 */
function search_products(PDO $pdo, string $search_term, int $limit = 20, int $offset = 0): array {
    try {
        $query = "
            SELECT
                p.id, p.name, p.description, p.price, p.quantity,
                p.created_at, p.updated_at, p.category_id,
                c.name AS category_name
            FROM products p
            INNER JOIN categories c ON p.category_id = c.id
            WHERE p.name ILIKE :search OR p.description ILIKE :search
            ORDER BY p.name ASC
            LIMIT :limit OFFSET :offset
        ";

        $stmt = $pdo->prepare($query);
        $search_pattern = '%' . $search_term . '%';
        $stmt->bindValue(':search', $search_pattern);
        $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
        $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);

        $stmt->execute();

        return $stmt->fetchAll();
    } catch (PDOException $e) {
        error_log("Erreur search_products: " . $e->getMessage());
        return [];
    }
}

/**
 * Met à jour la quantité d'un produit (pour gestion de stock)
 */
function update_product_quantity(PDO $pdo, int $id, int $quantity_change): bool {
    try {
        $pdo->beginTransaction();

        // Récupérer la quantité actuelle avec verrou
        $select_stmt = $pdo->prepare("
            SELECT quantity FROM products WHERE id = :id FOR UPDATE
        ");
        $select_stmt->bindValue(':id', $id, PDO::PARAM_INT);
        $select_stmt->execute();

        $row = $select_stmt->fetch();
        if (!$row) {
            $pdo->rollBack();
            return false;
        }

        $new_quantity = $row['quantity'] + $quantity_change;

        // Vérifier que la quantité ne devient pas négative
        if ($new_quantity < 0) {
            $pdo->rollBack();
            return false;
        }

        // Mettre à jour
        $update_stmt = $pdo->prepare("
            UPDATE products SET quantity = :quantity WHERE id = :id
        ");
        $update_stmt->bindValue(':quantity', $new_quantity, PDO::PARAM_INT);
        $update_stmt->bindValue(':id', $id, PDO::PARAM_INT);

        if (!$update_stmt->execute()) {
            $pdo->rollBack();
            return false;
        }

        $pdo->commit();
        return true;
    } catch (PDOException $e) {
        error_log("Erreur update_product_quantity: " . $e->getMessage());
        $pdo->rollBack();
        return false;
    }
}

/**
 * Récupère les statistiques par catégorie (Vue optimisée)
 */
function get_category_statistics(PDO $pdo): array {
    try {
        $stmt = $pdo->query("SELECT * FROM product_stats ORDER BY category_name");
        return $stmt->fetchAll();
    } catch (PDOException $e) {
        error_log("Erreur get_category_statistics: " . $e->getMessage());
        return [];
    }
}

4. Page d’Accueil (public/index.php)

<?php
declare(strict_types=1);

require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../includes/functions.php';
require_once __DIR__ . '/../includes/queries.php';

session_start();

try {
    $pdo = db_connect();
} catch (PDOException $e) {
    die("Erreur de connexion à la base de données");
}

// Récupérer les statistiques globales
$total_categories = count_categories($pdo);
$total_products = count_products($pdo);
$category_stats = get_category_statistics($pdo);

// Calculer la valeur totale du stock
$total_stock_value = 0;
$total_items = 0;
foreach ($category_stats as $stat) {
    $total_stock_value += $stat['total_quantity'] * $stat['avg_price'];
    $total_items += $stat['total_quantity'];
}

// Récupérer les produits récents
$recent_products = get_all_products($pdo, null, 5, 0);
?>
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Tableau de Bord - Gestion de Stock</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; }
        .container { max-width: 1400px; margin: 0 auto; }
        header { text-align: center; color: white; margin-bottom: 40px; }
        header h1 { font-size: 48px; margin-bottom: 10px; text-shadow: 2px 2px 4px rgba(0,0,0,0.2); }
        header p { font-size: 18px; opacity: 0.9; }
        .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-bottom: 40px; }
        .stat-card { background: white; padding: 30px; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); text-align: center; transition: transform 0.3s; }
        .stat-card:hover { transform: translateY(-5px); }
        .stat-card .icon { font-size: 48px; margin-bottom: 15px; }
        .stat-card .value { font-size: 36px; font-weight: bold; color: #333; margin-bottom: 10px; }
        .stat-card .label { font-size: 14px; color: #666; text-transform: uppercase; letter-spacing: 1px; }
        .nav-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 40px; }
        .nav-card { background: white; padding: 40px; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); text-align: center; transition: all 0.3s; text-decoration: none; display: block; }
        .nav-card:hover { transform: translateY(-5px); box-shadow: 0 8px 25px rgba(0,0,0,0.15); }
        .nav-card .icon { font-size: 64px; margin-bottom: 20px; }
        .nav-card h2 { color: #333; margin-bottom: 10px; font-size: 24px; }
        .nav-card p { color: #666; font-size: 14px; }
        .content-section { background: white; padding: 30px; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); margin-bottom: 20px; }
        .content-section h2 { color: #333; margin-bottom: 20px; border-bottom: 2px solid #667eea; padding-bottom: 10px; }
        table { width: 100%; border-collapse: collapse; }
        th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
        th { background: #f8f9fa; font-weight: 600; }
        tr:hover { background: #f8f9fa; }
        .badge { display: inline-block; padding: 4px 8px; border-radius: 3px; font-size: 12px; font-weight: 600; }
        .badge-primary { background: #007bff; color: white; }
        .price { font-weight: bold; color: #28a745; }
    </style>
</head>
<body>
    <div class="container">
        <header>
            <h1>🛒 Gestion de Stock</h1>
            <p>Système de gestion PostgreSQL avec PHP 8.4 et PDO</p>
        </header>

        <?php display_flash(); ?>

        <!-- Statistiques globales -->
        <div class="stats-grid">
            <div class="stat-card">
                <div class="icon">📦</div>
                <div class="value"><?= number_format($total_items) ?></div>
                <div class="label">Articles en Stock</div>
            </div>

            <div class="stat-card">
                <div class="icon">📁</div>
                <div class="value"><?= $total_categories ?></div>
                <div class="label">Catégories</div>
            </div>

            <div class="stat-card">
                <div class="icon">🏷️</div>
                <div class="value"><?= $total_products ?></div>
                <div class="label">Produits</div>
            </div>

            <div class="stat-card">
                <div class="icon">💰</div>
                <div class="value"><?= format_price($total_stock_value) ?></div>
                <div class="label">Valeur du Stock</div>
            </div>
        </div>

        <!-- Navigation principale -->
        <div class="nav-grid">
            <a href="categories/index.php" class="nav-card">
                <div class="icon">📁</div>
                <h2>Gérer les Catégories</h2>
                <p>Organiser et classer vos produits</p>
            </a>

            <a href="products/index.php" class="nav-card">
                <div class="icon">🏷️</div>
                <h2>Gérer les Produits</h2>
                <p>Ajouter, modifier et supprimer des produits</p>
            </a>
        </div>

        <!-- Statistiques par catégorie -->
        <div class="content-section">
            <h2>📊 Statistiques par Catégorie</h2>
            <table>
                <thead>
                    <tr>
                        <th>Catégorie</th>
                        <th>Produits</th>
                        <th>Stock Total</th>
                        <th>Prix Moyen</th>
                        <th>Prix Min</th>
                        <th>Prix Max</th>
                    </tr>
                </thead>
                <tbody>
                    <?php if (empty($category_stats)): ?>
                        <tr>
                            <td colspan="6" style="text-align: center; color: #666;">Aucune donnée disponible</td>
                        </tr>
                    <?php else: ?>
                        <?php foreach ($category_stats as $stat): ?>
                            <tr>
                                <td><strong><?= h($stat['category_name']) ?></strong></td>
                                <td><?= h($stat['product_count']) ?></td>
                                <td><?= number_format($stat['total_quantity']) ?></td>
                                <td class="price"><?= format_price($stat['avg_price']) ?></td>
                                <td class="price"><?= format_price($stat['min_price']) ?></td>
                                <td class="price"><?= format_price($stat['max_price']) ?></td>
                            </tr>
                        <?php endforeach; ?>
                    <?php endif; ?>
                </tbody>
            </table>
        </div>

        <!-- Produits récents -->
        <div class="content-section">
            <h2>🆕 Produits Récents</h2>
            <table>
                <thead>
                    <tr>
                        <th>Nom</th>
                        <th>Catégorie</th>
                        <th>Prix</th>
                        <th>Stock</th>
                        <th>Ajouté le</th>
                    </tr>
                </thead>
                <tbody>
                    <?php if (empty($recent_products)): ?>
                        <tr>
                            <td colspan="5" style="text-align: center; color: #666;">Aucun produit</td>
                        </tr>
                    <?php else: ?>
                        <?php foreach ($recent_products as $product): ?>
                            <tr>
                                <td><strong><?= h($product['name']) ?></strong></td>
                                <td><span class="badge badge-primary"><?= h($product['category_name']) ?></span></td>
                                <td class="price"><?= format_price($product['price']) ?></td>
                                <td><?= h($product['quantity']) ?></td>
                                <td><?= format_date($product['created_at']) ?></td>
                            </tr>
                        <?php endforeach; ?>
                    <?php endif; ?>
                </tbody>
            </table>
        </div>
    </div>
</body>
</html>

5. Catégories - Index (public/categories/index.php)

<?php
declare(strict_types=1);

require_once __DIR__ . '/../../config/database.php';
require_once __DIR__ . '/../../includes/functions.php';
require_once __DIR__ . '/../../includes/queries.php';

session_start();

try {
    $pdo = db_connect();
} catch (PDOException $e) {
    die("Erreur de connexion à la base de données");
}

// Pagination
$page = validate_positive_int($_GET['page'] ?? 1) ?? 1;
$items_per_page = 10;
$offset = calculate_offset($page, $items_per_page);

// Récupérer les catégories
$categories = get_all_categories($pdo, $items_per_page, $offset);
$total_categories = count_categories($pdo);
$total_pages = calculate_pages($total_categories, $items_per_page);

// Récupérer les statistiques
$stats = get_category_statistics($pdo);
?>
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Gestion des Catégories</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f5f5f5; padding: 20px; }
        .container { max-width: 1200px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        h1 { color: #333; margin-bottom: 30px; }
        .nav { margin-bottom: 20px; }
        .nav a { color: #007bff; text-decoration: none; margin-right: 20px; }
        .nav a:hover { text-decoration: underline; }
        .btn { display: inline-block; padding: 10px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 5px; border: none; cursor: pointer; }
        .btn:hover { background: #0056b3; }
        .btn-danger { background: #dc3545; }
        .btn-danger:hover { background: #c82333; }
        .btn-success { background: #28a745; }
        .btn-success:hover { background: #218838; }
        .alert { padding: 15px; margin-bottom: 20px; border-radius: 5px; }
        .alert-success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
        .alert-danger { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
        table { width: 100%; border-collapse: collapse; margin-top: 20px; }
        th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
        th { background: #f8f9fa; font-weight: 600; }
        tr:hover { background: #f8f9fa; }
        .actions a { margin-right: 10px; }
        .pagination { margin-top: 20px; text-align: center; }
        .pagination a, .pagination span { display: inline-block; padding: 8px 12px; margin: 0 5px; border: 1px solid #ddd; border-radius: 3px; text-decoration: none; color: #007bff; }
        .pagination span { background: #007bff; color: white; }
        .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 30px; }
        .stat-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 8px; }
        .stat-card h3 { font-size: 14px; margin-bottom: 10px; opacity: 0.9; }
        .stat-card .value { font-size: 28px; font-weight: bold; }
    </style>
</head>
<body>
    <div class="container">
        <div class="nav">
            <a href="../index.php">Accueil</a>
            <a href="index.php">Catégories</a>
            <a href="../products/index.php">Produits</a>
        </div>

        <h1>Gestion des Catégories</h1>

        <?php display_flash(); ?>

        <!-- Statistiques -->
        <div class="stats">
            <div class="stat-card">
                <h3>Total Catégories</h3>
                <div class="value"><?= $total_categories ?></div>
            </div>
        </div>

        <a href="create.php" class="btn btn-success">+ Nouvelle Catégorie</a>

        <table>
            <thead>
                <tr>
                    <th>ID</th>
                    <th>Nom</th>
                    <th>Produits</th>
                    <th>Créée le</th>
                    <th>Modifiée le</th>
                    <th>Actions</th>
                </tr>
            </thead>
            <tbody>
                <?php if (empty($categories)): ?>
                    <tr>
                        <td colspan="6" style="text-align: center; color: #666;">Aucune catégorie trouvée</td>
                    </tr>
                <?php else: ?>
                    <?php foreach ($categories as $category): ?>
                        <?php
                        $category_stats = null;
                        foreach ($stats as $stat) {
                            if ($stat['category_id'] == $category['id']) {
                                $category_stats = $stat;
                                break;
                            }
                        }
                        ?>
                        <tr>
                            <td><?= h($category['id']) ?></td>
                            <td><strong><?= h($category['name']) ?></strong></td>
                            <td>
                                <?php if ($category_stats): ?>
                                    <?= h($category_stats['product_count']) ?> produit(s)
                                <?php else: ?>
                                    0 produit
                                <?php endif; ?>
                            </td>
                            <td><?= format_date($category['created_at']) ?></td>
                            <td><?= format_date($category['updated_at']) ?></td>
                            <td class="actions">
                                <a href="edit.php?id=<?= $category['id'] ?>" class="btn">Modifier</a>
                                <a href="delete.php?id=<?= $category['id'] ?>"
                                   class="btn btn-danger"
                                   onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette catégorie ?')">
                                   Supprimer
                                </a>
                            </td>
                        </tr>
                    <?php endforeach; ?>
                <?php endif; ?>
            </tbody>
        </table>

        <!-- Pagination -->
        <?php if ($total_pages > 1): ?>
            <div class="pagination">
                <?php if ($page > 1): ?>
                    <a href="?page=<?= $page - 1 ?>">« Précédent</a>
                <?php endif; ?>

                <?php for ($i = 1; $i <= $total_pages; $i++): ?>
                    <?php if ($i == $page): ?>
                        <span><?= $i ?></span>
                    <?php else: ?>
                        <a href="?page=<?= $i ?>"><?= $i ?></a>
                    <?php endif; ?>
                <?php endfor; ?>

                <?php if ($page < $total_pages): ?>
                    <a href="?page=<?= $page + 1 ?>">Suivant »</a>
                <?php endif; ?>
            </div>
        <?php endif; ?>
    </div>
</body>
</html>

6. Catégories - Create (public/categories/create.php)

<?php
declare(strict_types=1);

require_once __DIR__ . '/../../config/database.php';
require_once __DIR__ . '/../../includes/functions.php';
require_once __DIR__ . '/../../includes/queries.php';

session_start();

try {
    $pdo = db_connect();
} catch (PDOException $e) {
    die("Erreur de connexion à la base de données");
}

$errors = [];
$name = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $name = validate_string($_POST['name'] ?? '', 100);

    if ($name === null) {
        $errors[] = "Le nom de la catégorie est requis (max 100 caractères)";
    }

    if (empty($errors)) {
        $category_id = create_category($pdo, $name);

        if ($category_id) {
            set_flash('success', "Catégorie créée avec succès (ID: {$category_id})");
            redirect('index.php');
        } else {
            $errors[] = "Erreur lors de la création de la catégorie";
        }
    }
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Nouvelle Catégorie</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f5f5f5; padding: 20px; }
        .container { max-width: 600px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        h1 { color: #333; margin-bottom: 30px; }
        .form-group { margin-bottom: 20px; }
        label { display: block; margin-bottom: 5px; font-weight: 600; color: #555; }
        input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; }
        input:focus { outline: none; border-color: #007bff; }
        .btn { display: inline-block; padding: 10px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 5px; border: none; cursor: pointer; font-size: 14px; }
        .btn:hover { background: #0056b3; }
        .btn-secondary { background: #6c757d; }
        .btn-secondary:hover { background: #5a6268; }
        .alert-danger { background: #f8d7da; color: #721c24; padding: 15px; border-radius: 5px; margin-bottom: 20px; border: 1px solid #f5c6cb; }
        .back-link { display: inline-block; margin-bottom: 20px; color: #007bff; text-decoration: none; }
        .back-link:hover { text-decoration: underline; }
    </style>
</head>
<body>
    <div class="container">
        <a href="index.php" class="back-link">← Retour à la liste</a>

        <h1>Nouvelle Catégorie</h1>

        <?php if (!empty($errors)): ?>
            <div class="alert-danger">
                <ul>
                    <?php foreach ($errors as $error): ?>
                        <li><?= h($error) ?></li>
                    <?php endforeach; ?>
                </ul>
            </div>
        <?php endif; ?>

        <form method="POST" action="">
            <div class="form-group">
                <label for="name">Nom de la catégorie *</label>
                <input type="text"
                       id="name"
                       name="name"
                       value="<?= h($name) ?>"
                       required
                       maxlength="100"
                       placeholder="Ex: Électronique">
            </div>

            <button type="submit" class="btn">Créer la Catégorie</button>
            <a href="index.php" class="btn btn-secondary">Annuler</a>
        </form>
    </div>
</body>
</html>

7. Catégories - Edit (public/categories/edit.php)

<?php
declare(strict_types=1);

require_once __DIR__ . '/../../config/database.php';
require_once __DIR__ . '/../../includes/functions.php';
require_once __DIR__ . '/../../includes/queries.php';

session_start();

try {
    $pdo = db_connect();
} catch (PDOException $e) {
    die("Erreur de connexion à la base de données");
}

$id = validate_positive_int($_GET['id'] ?? 0);
if (!$id) {
    set_flash('danger', "ID de catégorie invalide");
    redirect('index.php');
}

$category = get_category_by_id($pdo, $id);
if (!$category) {
    set_flash('danger', "Catégorie introuvable");
    redirect('index.php');
}

$errors = [];
$name = $category['name'];

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $name = validate_string($_POST['name'] ?? '', 100);

    if ($name === null) {
        $errors[] = "Le nom de la catégorie est requis (max 100 caractères)";
    }

    if (empty($errors)) {
        $success = update_category($pdo, $id, $name);

        if ($success) {
            set_flash('success', "Catégorie modifiée avec succès");
            redirect('index.php');
        } else {
            $errors[] = "Erreur lors de la modification de la catégorie";
        }
    }
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Modifier Catégorie</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f5f5f5; padding: 20px; }
        .container { max-width: 600px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        h1 { color: #333; margin-bottom: 30px; }
        .form-group { margin-bottom: 20px; }
        label { display: block; margin-bottom: 5px; font-weight: 600; color: #555; }
        input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; }
        input:focus { outline: none; border-color: #007bff; }
        .btn { display: inline-block; padding: 10px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 5px; border: none; cursor: pointer; font-size: 14px; }
        .btn:hover { background: #0056b3; }
        .btn-secondary { background: #6c757d; }
        .btn-secondary:hover { background: #5a6268; }
        .alert-danger { background: #f8d7da; color: #721c24; padding: 15px; border-radius: 5px; margin-bottom: 20px; border: 1px solid #f5c6cb; }
        .back-link { display: inline-block; margin-bottom: 20px; color: #007bff; text-decoration: none; }
        .back-link:hover { text-decoration: underline; }
        .info-box { background: #e7f3ff; padding: 15px; border-left: 4px solid #007bff; margin-bottom: 20px; }
    </style>
</head>
<body>
    <div class="container">
        <a href="index.php" class="back-link">← Retour à la liste</a>

        <h1>Modifier Catégorie</h1>

        <div class="info-box">
            <strong>ID:</strong> <?= h($category['id']) ?><br>
            <strong>Créée le:</strong> <?= format_date($category['created_at']) ?><br>
            <strong>Modifiée le:</strong> <?= format_date($category['updated_at']) ?>
        </div>

        <?php if (!empty($errors)): ?>
            <div class="alert-danger">
                <ul>
                    <?php foreach ($errors as $error): ?>
                        <li><?= h($error) ?></li>
                    <?php endforeach; ?>
                </ul>
            </div>
        <?php endif; ?>

        <form method="POST" action="">
            <div class="form-group">
                <label for="name">Nom de la catégorie *</label>
                <input type="text"
                       id="name"
                       name="name"
                       value="<?= h($name) ?>"
                       required
                       maxlength="100">
            </div>

            <button type="submit" class="btn">Enregistrer les modifications</button>
            <a href="index.php" class="btn btn-secondary">Annuler</a>
        </form>
    </div>
</body>
</html>

8. Catégories - Delete (public/categories/delete.php)

<?php
declare(strict_types=1);

require_once __DIR__ . '/../../config/database.php';
require_once __DIR__ . '/../../includes/functions.php';
require_once __DIR__ . '/../../includes/queries.php';

session_start();

try {
    $pdo = db_connect();
} catch (PDOException $e) {
    die("Erreur de connexion à la base de données");
}

$id = validate_positive_int($_GET['id'] ?? 0);
if (!$id) {
    set_flash('danger', "ID de catégorie invalide");
    redirect('index.php');
}

$category = get_category_by_id($pdo, $id);
if (!$category) {
    set_flash('danger', "Catégorie introuvable");
    redirect('index.php');
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $success = delete_category($pdo, $id);

    if ($success) {
        set_flash('success', "Catégorie supprimée avec succès");
    } else {
        set_flash('danger', "Impossible de supprimer cette catégorie car elle contient des produits");
    }

    redirect('index.php');
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Supprimer Catégorie</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f5f5f5; padding: 20px; }
        .container { max-width: 600px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        h1 { color: #dc3545; margin-bottom: 30px; }
        .warning-box { background: #fff3cd; padding: 20px; border-left: 4px solid #ffc107; margin-bottom: 20px; }
        .category-info { background: #f8f9fa; padding: 15px; border-radius: 5px; margin-bottom: 20px; }
        .btn { display: inline-block; padding: 10px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 5px; border: none; cursor: pointer; font-size: 14px; margin-right: 10px; }
        .btn-danger { background: #dc3545; }
        .btn-danger:hover { background: #c82333; }
        .btn-secondary { background: #6c757d; }
        .btn-secondary:hover { background: #5a6268; }
        .back-link { display: inline-block; margin-bottom: 20px; color: #007bff; text-decoration: none; }
        .back-link:hover { text-decoration: underline; }
    </style>
</head>
<body>
    <div class="container">
        <a href="index.php" class="back-link">← Retour à la liste</a>

        <h1>⚠️ Supprimer la Catégorie</h1>

        <div class="warning-box">
            <strong>Attention !</strong> Cette action est irréversible. Vous êtes sur le point de supprimer la catégorie suivante :
        </div>

        <div class="category-info">
            <p><strong>ID :</strong> <?= h($category['id']) ?></p>
            <p><strong>Nom :</strong> <?= h($category['name']) ?></p>
            <p><strong>Créée le :</strong> <?= format_date($category['created_at']) ?></p>
        </div>

        <form method="POST" action="">
            <button type="submit" class="btn btn-danger">Confirmer la suppression</button>
            <a href="index.php" class="btn btn-secondary">Annuler</a>
        </form>
    </div>
</body>
</html>

9. Produits - Index (public/products/index.php)

<?php
declare(strict_types=1);

require_once __DIR__ . '/../../config/database.php';
require_once __DIR__ . '/../../includes/functions.php';
require_once __DIR__ . '/../../includes/queries.php';

session_start();

try {
    $pdo = db_connect();
} catch (PDOException $e) {
    die("Erreur de connexion à la base de données");
}

// Filtres
$category_filter = validate_positive_int($_GET['category'] ?? 0);
$search = validate_string($_GET['search'] ?? '', 100);

// Pagination
$page = validate_positive_int($_GET['page'] ?? 1) ?? 1;
$items_per_page = 10;
$offset = calculate_offset($page, $items_per_page);

// Récupérer les produits
if ($search) {
    $products = search_products($pdo, $search, $items_per_page, $offset);
    $total_products = count($products);
} else {
    $products = get_all_products($pdo, $category_filter ?: null, $items_per_page, $offset);
    $total_products = count_products($pdo, $category_filter ?: null);
}

$total_pages = calculate_pages($total_products, $items_per_page);

// Récupérer toutes les catégories pour le filtre
$all_categories = get_all_categories($pdo, 100, 0);
?>
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Gestion des Produits</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f5f5f5; padding: 20px; }
        .container { max-width: 1400px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        h1 { color: #333; margin-bottom: 30px; }
        .nav { margin-bottom: 20px; }
        .nav a { color: #007bff; text-decoration: none; margin-right: 20px; }
        .nav a:hover { text-decoration: underline; }
        .filters { display: flex; gap: 15px; margin-bottom: 20px; flex-wrap: wrap; align-items: flex-end; }
        .filter-group { flex: 1; min-width: 200px; }
        .filter-group label { display: block; margin-bottom: 5px; font-weight: 600; color: #555; }
        .filter-group select, .filter-group input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; }
        .btn { display: inline-block; padding: 10px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 5px; border: none; cursor: pointer; white-space: nowrap; }
        .btn:hover { background: #0056b3; }
        .btn-danger { background: #dc3545; }
        .btn-danger:hover { background: #c82333; }
        .btn-success { background: #28a745; }
        .btn-success:hover { background: #218838; }
        .btn-secondary { background: #6c757d; }
        .btn-secondary:hover { background: #5a6268; }
        .btn-sm { padding: 6px 12px; font-size: 12px; }
        .alert { padding: 15px; margin-bottom: 20px; border-radius: 5px; }
        .alert-success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
        .alert-danger { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
        table { width: 100%; border-collapse: collapse; margin-top: 20px; }
        th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
        th { background: #f8f9fa; font-weight: 600; }
        tr:hover { background: #f8f9fa; }
        .actions { white-space: nowrap; }
        .actions a { margin-right: 5px; }
        .pagination { margin-top: 20px; text-align: center; }
        .pagination a, .pagination span { display: inline-block; padding: 8px 12px; margin: 0 5px; border: 1px solid #ddd; border-radius: 3px; text-decoration: none; color: #007bff; }
        .pagination span { background: #007bff; color: white; }
        .badge { display: inline-block; padding: 4px 8px; border-radius: 3px; font-size: 12px; font-weight: 600; }
        .badge-primary { background: #007bff; color: white; }
        .badge-warning { background: #ffc107; color: #000; }
        .badge-success { background: #28a745; color: white; }
        .price { font-weight: bold; color: #28a745; }
    </style>
</head>
<body>
    <div class="container">
        <div class="nav">
            <a href="../index.php">Accueil</a>
            <a href="../categories/index.php">Catégories</a>
            <a href="index.php">Produits</a>
        </div>

        <h1>Gestion des Produits</h1>

        <?php display_flash(); ?>

        <!-- Filtres et recherche -->
        <form method="GET" class="filters">
            <div class="filter-group">
                <label for="category">Catégorie</label>
                <select name="category" id="category">
                    <option value="">Toutes les catégories</option>
                    <?php foreach ($all_categories as $cat): ?>
                        <option value="<?= $cat['id'] ?>" <?= $category_filter == $cat['id'] ? 'selected' : '' ?>>
                            <?= h($cat['name']) ?>
                        </option>
                    <?php endforeach; ?>
                </select>
            </div>

            <div class="filter-group">
                <label for="search">Rechercher</label>
                <input type="text"
                       name="search"
                       id="search"
                       placeholder="Nom ou description..."
                       value="<?= h($search) ?>">
            </div>

            <button type="submit" class="btn">Filtrer</button>
            <a href="index.php" class="btn btn-secondary">Réinitialiser</a>
        </form>

        <a href="create.php" class="btn btn-success">+ Nouveau Produit</a>

        <table>
            <thead>
                <tr>
                    <th>ID</th>
                    <th>Nom</th>
                    <th>Catégorie</th>
                    <th>Prix</th>
                    <th>Stock</th>
                    <th>Créé le</th>
                    <th>Actions</th>
                </tr>
            </thead>
            <tbody>
                <?php if (empty($products)): ?>
                    <tr>
                        <td colspan="7" style="text-align: center; color: #666;">Aucun produit trouvé</td>
                    </tr>
                <?php else: ?>
                    <?php foreach ($products as $product): ?>
                        <tr>
                            <td><?= h($product['id']) ?></td>
                            <td>
                                <strong><?= h($product['name']) ?></strong>
                                <?php if ($product['description']): ?>
                                    <br><small style="color: #666;"><?= h(mb_substr($product['description'], 0, 50)) ?>...</small>
                                <?php endif; ?>
                            </td>
                            <td>
                                <span class="badge badge-primary"><?= h($product['category_name']) ?></span>
                            </td>
                            <td class="price"><?= format_price($product['price']) ?></td>
                            <td>
                                <?php if ($product['quantity'] <= 5): ?>
                                    <span class="badge badge-warning"><?= h($product['quantity']) ?> ⚠️</span>
                                <?php else: ?>
                                    <span class="badge badge-success"><?= h($product['quantity']) ?></span>
                                <?php endif; ?>
                            </td>
                            <td><?= format_date($product['created_at']) ?></td>
                            <td class="actions">
                                <a href="edit.php?id=<?= $product['id'] ?>" class="btn btn-sm">Modifier</a>
                                <a href="delete.php?id=<?= $product['id'] ?>"
                                   class="btn btn-sm btn-danger"
                                   onclick="return confirm('Supprimer ce produit ?')">
                                   Supprimer
                                </a>
                            </td>
                        </tr>
                    <?php endforeach; ?>
                <?php endif; ?>
            </tbody>
        </table>

        <!-- Pagination -->
        <?php if ($total_pages > 1): ?>
            <div class="pagination">
                <?php
                $query_params = [];
                if ($category_filter) $query_params['category'] = $category_filter;
                if ($search) $query_params['search'] = $search;
                $base_url = '?' . http_build_query($query_params);
                ?>

                <?php if ($page > 1): ?>
                    <a href="<?= $base_url ?>&page=<?= $page - 1 ?>">« Précédent</a>
                <?php endif; ?>

                <?php for ($i = 1; $i <= $total_pages; $i++): ?>
                    <?php if ($i == $page): ?>
                        <span><?= $i ?></span>
                    <?php else: ?>
                        <a href="<?= $base_url ?>&page=<?= $i ?>"><?= $i ?></a>
                    <?php endif; ?>
                <?php endfor; ?>

                <?php if ($page < $total_pages): ?>
                    <a href="<?= $base_url ?>&page=<?= $page + 1 ?>">Suivant »</a>
                <?php endif; ?>
            </div>
        <?php endif; ?>
    </div>
</body>
</html>

10. Produits - Create (public/products/create.php)

<?php
declare(strict_types=1);

require_once __DIR__ . '/../../config/database.php';
require_once __DIR__ . '/../../includes/functions.php';
require_once __DIR__ . '/../../includes/queries.php';

session_start();

try {
    $pdo = db_connect();
} catch (PDOException $e) {
    die("Erreur de connexion à la base de données");
}

$errors = [];
$name = '';
$description = '';
$price = '';
$quantity = '';
$category_id = '';

// Récupérer toutes les catégories
$categories = get_all_categories($pdo, 100, 0);

if (empty($categories)) {
    set_flash('danger', "Vous devez d'abord créer au moins une catégorie");
    redirect('../categories/create.php');
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // Validation
    $name = validate_string($_POST['name'] ?? '', 200);
    $description = trim($_POST['description'] ?? '');
    $description = $description === '' ? null : $description;
    $price = validate_price($_POST['price'] ?? 0);
    $quantity = validate_positive_int($_POST['quantity'] ?? 0);
    if ($quantity === null) $quantity = 0;
    $category_id = validate_positive_int($_POST['category_id'] ?? 0);

    if ($name === null) {
        $errors[] = "Le nom du produit est requis (max 200 caractères)";
    }

    if ($price === null) {
        $errors[] = "Le prix doit être un nombre positif ou zéro";
    }

    if ($quantity < 0) {
        $errors[] = "La quantité ne peut pas être négative";
    }

    if (!$category_id) {
        $errors[] = "Vous devez sélectionner une catégorie";
    }

    // Si pas d'erreurs, créer le produit
    if (empty($errors)) {
        $product_id = create_product($pdo, $name, $description, $price, $quantity, $category_id);

        if ($product_id) {
            set_flash('success', "Produit créé avec succès (ID: {$product_id})");
            redirect('index.php');
        } else {
            $errors[] = "Erreur lors de la création du produit";
        }
    }
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Nouveau Produit</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f5f5f5; padding: 20px; }
        .container { max-width: 800px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        h1 { color: #333; margin-bottom: 30px; }
        .form-group { margin-bottom: 20px; }
        label { display: block; margin-bottom: 5px; font-weight: 600; color: #555; }
        input, textarea, select { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; font-family: inherit; }
        input:focus, textarea:focus, select:focus { outline: none; border-color: #007bff; }
        textarea { min-height: 100px; resize: vertical; }
        .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
        .btn { display: inline-block; padding: 10px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 5px; border: none; cursor: pointer; font-size: 14px; }
        .btn:hover { background: #0056b3; }
        .btn-secondary { background: #6c757d; }
        .btn-secondary:hover { background: #5a6268; }
        .alert-danger { background: #f8d7da; color: #721c24; padding: 15px; border-radius: 5px; margin-bottom: 20px; border: 1px solid #f5c6cb; }
        .back-link { display: inline-block; margin-bottom: 20px; color: #007bff; text-decoration: none; }
        .back-link:hover { text-decoration: underline; }
    </style>
</head>
<body>
    <div class="container">
        <a href="index.php" class="back-link">← Retour à la liste</a>

        <h1>Nouveau Produit</h1>

        <?php if (!empty($errors)): ?>
            <div class="alert-danger">
                <ul>
                    <?php foreach ($errors as $error): ?>
                        <li><?= h($error) ?></li>
                    <?php endforeach; ?>
                </ul>
            </div>
        <?php endif; ?>

        <form method="POST" action="">
            <div class="form-group">
                <label for="name">Nom du produit *</label>
                <input type="text"
                       id="name"
                       name="name"
                       value="<?= h($name) ?>"
                       required
                       maxlength="200"
                       placeholder="Ex: Ordinateur portable Dell XPS 13">
            </div>

            <div class="form-group">
                <label for="description">Description</label>
                <textarea id="description"
                          name="description"
                          placeholder="Description détaillée du produit..."><?= h($description ?? '') ?></textarea>
            </div>

            <div class="form-row">
                <div class="form-group">
                    <label for="price">Prix (€) *</label>
                    <input type="number"
                           id="price"
                           name="price"
                           value="<?= h($price) ?>"
                           required
                           step="0.01"
                           min="0"
                           placeholder="0.00">
                </div>

                <div class="form-group">
                    <label for="quantity">Quantité en stock *</label>
                    <input type="number"
                           id="quantity"
                           name="quantity"
                           value="<?= h($quantity) ?>"
                           required
                           min="0"
                           placeholder="0">
                </div>
            </div>

            <div class="form-group">
                <label for="category_id">Catégorie *</label>
                <select id="category_id" name="category_id" required>
                    <option value="">-- Sélectionnez une catégorie --</option>
                    <?php foreach ($categories as $cat): ?>
                        <option value="<?= $cat['id'] ?>" <?= $category_id == $cat['id'] ? 'selected' : '' ?>>
                            <?= h($cat['name']) ?>
                        </option>
                    <?php endforeach; ?>
                </select>
            </div>

            <button type="submit" class="btn">Créer le Produit</button>
            <a href="index.php" class="btn btn-secondary">Annuler</a>
        </form>
    </div>
</body>
</html>

11. Produits - Edit (public/products/edit.php)

<?php
declare(strict_types=1);

require_once __DIR__ . '/../../config/database.php';
require_once __DIR__ . '/../../includes/functions.php';
require_once __DIR__ . '/../../includes/queries.php';

session_start();

try {
    $pdo = db_connect();
} catch (PDOException $e) {
    die("Erreur de connexion à la base de données");
}

// Récupérer l'ID
$id = validate_positive_int($_GET['id'] ?? 0);
if (!$id) {
    set_flash('danger', "ID de produit invalide");
    redirect('index.php');
}

// Récupérer le produit
$product = get_product_by_id($pdo, $id);
if (!$product) {
    set_flash('danger', "Produit introuvable");
    redirect('index.php');
}

$errors = [];
$name = $product['name'];
$description = $product['description'] ?? '';
$price = $product['price'];
$quantity = $product['quantity'];
$category_id = $product['category_id'];

// Récupérer toutes les catégories
$categories = get_all_categories($pdo, 100, 0);

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // Validation
    $name = validate_string($_POST['name'] ?? '', 200);
    $description = trim($_POST['description'] ?? '');
    $description = $description === '' ? null : $description;
    $price = validate_price($_POST['price'] ?? 0);
    $quantity = validate_positive_int($_POST['quantity'] ?? 0);
    if ($quantity === null) $quantity = 0;
    $category_id = validate_positive_int($_POST['category_id'] ?? 0);

    if ($name === null) {
        $errors[] = "Le nom du produit est requis (max 200 caractères)";
    }

    if ($price === null) {
        $errors[] = "Le prix doit être un nombre positif ou zéro";
    }

    if ($quantity < 0) {
        $errors[] = "La quantité ne peut pas être négative";
    }

    if (!$category_id) {
        $errors[] = "Vous devez sélectionner une catégorie";
    }

    // Si pas d'erreurs, mettre à jour
    if (empty($errors)) {
        $success = update_product($pdo, $id, $name, $description, $price, $quantity, $category_id);

        if ($success) {
            set_flash('success', "Produit modifié avec succès");
            redirect('index.php');
        } else {
            $errors[] = "Erreur lors de la modification du produit";
        }
    }
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Modifier Produit</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f5f5f5; padding: 20px; }
        .container { max-width: 800px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        h1 { color: #333; margin-bottom: 30px; }
        .form-group { margin-bottom: 20px; }
        label { display: block; margin-bottom: 5px; font-weight: 600; color: #555; }
        input, textarea, select { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; font-family: inherit; }
        input:focus, textarea:focus, select:focus { outline: none; border-color: #007bff; }
        textarea { min-height: 100px; resize: vertical; }
        .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
        .btn { display: inline-block; padding: 10px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 5px; border: none; cursor: pointer; font-size: 14px; }
        .btn:hover { background: #0056b3; }
        .btn-secondary { background: #6c757d; }
        .btn-secondary:hover { background: #5a6268; }
        .alert-danger { background: #f8d7da; color: #721c24; padding: 15px; border-radius: 5px; margin-bottom: 20px; border: 1px solid #f5c6cb; }
        .back-link { display: inline-block; margin-bottom: 20px; color: #007bff; text-decoration: none; }
        .back-link:hover { text-decoration: underline; }
        .info-box { background: #e7f3ff; padding: 15px; border-left: 4px solid #007bff; margin-bottom: 20px; }
    </style>
</head>
<body>
    <div class="container">
        <a href="index.php" class="back-link">← Retour à la liste</a>

        <h1>Modifier Produit</h1>

        <div class="info-box">
            <strong>ID:</strong> <?= h($product['id']) ?><br>
            <strong>Catégorie actuelle:</strong> <?= h($product['category_name']) ?><br>
            <strong>Créé le:</strong> <?= format_date($product['created_at']) ?><br>
            <strong>Modifié le:</strong> <?= format_date($product['updated_at']) ?>
        </div>

        <?php if (!empty($errors)): ?>
            <div class="alert-danger">
                <ul>
                    <?php foreach ($errors as $error): ?>
                        <li><?= h($error) ?></li>
                    <?php endforeach; ?>
                </ul>
            </div>
        <?php endif; ?>

        <form method="POST" action="">
            <div class="form-group">
                <label for="name">Nom du produit *</label>
                <input type="text"
                       id="name"
                       name="name"
                       value="<?= h($name) ?>"
                       required
                       maxlength="200">
            </div>

            <div class="form-group">
                <label for="description">Description</label>
                <textarea id="description" name="description"><?= h($description) ?></textarea>
            </div>

            <div class="form-row">
                <div class="form-group">
                    <label for="price">Prix (€) *</label>
                    <input type="number"
                           id="price"
                           name="price"
                           value="<?= h($price) ?>"
                           required
                           step="0.01"
                           min="0">
                </div>

                <div class="form-group">
                    <label for="quantity">Quantité en stock *</label>
                    <input type="number"
                           id="quantity"
                           name="quantity"
                           value="<?= h($quantity) ?>"
                           required
                           min="0">
                </div>
            </div>

            <div class="form-group">
                <label for="category_id">Catégorie *</label>
                <select id="category_id" name="category_id" required>
                    <option value="">-- Sélectionnez une catégorie --</option>
                    <?php foreach ($categories as $cat): ?>
                        <option value="<?= $cat['id'] ?>" <?= $category_id == $cat['id'] ? 'selected' : '' ?>>
                            <?= h($cat['name']) ?>
                        </option>
                    <?php endforeach; ?>
                </select>
            </div>

            <button type="submit" class="btn">Enregistrer les modifications</button>
            <a href="index.php" class="btn btn-secondary">Annuler</a>
        </form>
    </div>
</body>
</html>

12. Produits - Delete (public/products/delete.php)

<?php
declare(strict_types=1);

require_once __DIR__ . '/../../config/database.php';
require_once __DIR__ . '/../../includes/functions.php';
require_once __DIR__ . '/../../includes/queries.php';

session_start();

try {
    $pdo = db_connect();
} catch (PDOException $e) {
    die("Erreur de connexion à la base de données");
}

// Récupérer l'ID
$id = validate_positive_int($_GET['id'] ?? 0);
if (!$id) {
    set_flash('danger', "ID de produit invalide");
    redirect('index.php');
}

// Récupérer le produit
$product = get_product_by_id($pdo, $id);
if (!$product) {
    set_flash('danger', "Produit introuvable");
    redirect('index.php');
}

// Vérifier si la méthode est POST (confirmation)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $success = delete_product($pdo, $id);

    if ($success) {
        set_flash('success', "Produit supprimé avec succès");
    } else {
        set_flash('danger', "Erreur lors de la suppression du produit");
    }

    redirect('index.php');
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Supprimer Produit</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f5f5f5; padding: 20px; }
        .container { max-width: 700px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        h1 { color: #dc3545; margin-bottom: 30px; }
        .warning-box { background: #fff3cd; padding: 20px; border-left: 4px solid #ffc107; margin-bottom: 20px; }
        .product-info { background: #f8f9fa; padding: 20px; border-radius: 5px; margin-bottom: 20px; }
        .product-info p { margin-bottom: 10px; }
        .product-info .price { font-size: 24px; font-weight: bold; color: #28a745; }
        .btn { display: inline-block; padding: 10px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 5px; border: none; cursor: pointer; font-size: 14px; margin-right: 10px; }
        .btn-danger { background: #dc3545; }
        .btn-danger:hover { background: #c82333; }
        .btn-secondary { background: #6c757d; }
        .btn-secondary:hover { background: #5a6268; }
        .back-link { display: inline-block; margin-bottom: 20px; color: #007bff; text-decoration: none; }
        .back-link:hover { text-decoration: underline; }
    </style>
</head>
<body>
    <div class="container">
        <a href="index.php" class="back-link">← Retour à la liste</a>

        <h1>⚠️ Supprimer le Produit</h1>

        <div class="warning-box">
            <strong>Attention !</strong> Cette action est irréversible. Vous êtes sur le point de supprimer le produit suivant :
        </div>

        <div class="product-info">
            <p><strong>ID :</strong> <?= h($product['id']) ?></p>
            <p><strong>Nom :</strong> <?= h($product['name']) ?></p>
            <p><strong>Catégorie :</strong> <?= h($product['category_name']) ?></p>
            <p><strong>Description :</strong> <?= h($product['description'] ?: 'Aucune') ?></p>
            <p class="price"><?= format_price($product['price']) ?></p>
            <p><strong>Stock :</strong> <?= h($product['quantity']) ?> unité(s)</p>
            <p><strong>Créé le :</strong> <?= format_date($product['created_at']) ?></p>
        </div>

        <form method="POST" action="">
            <button type="submit" class="btn btn-danger">Confirmer la suppression</button>
            <a href="index.php" class="btn btn-secondary">Annuler</a>
        </form>
    </div>
</body>
</html>

📊 Résumé des Changements PDO

✅ Avantages de PDO vs pg_*

Aspect pg_* PDO
Sécurité pg_query_params() Requêtes préparées natives
Portabilité PostgreSQL uniquement MySQL, SQLite, Oracle, etc.
Gestion d’erreurs Manuelle Exceptions PDOException
Performance Bonne Excellente avec prepared statements
Fetch modes pg_fetch_assoc() PDO::FETCH_ASSOC
Transactions pg_query() beginTransaction() / commit()
Binding :1, :2 :name ou ?

🔐 Sécurité PDO

// ❌ Avant (Injection SQL possible)
$result = pg_query($conn, "SELECT * FROM products WHERE id = " . $_GET['id']);

// ✅ Après (Sécurisé)
$stmt = $pdo->prepare("SELECT * FROM products WHERE id = :id");
$stmt->bindValue(':id', $_GET['id'], PDO::PARAM_INT);
$stmt->execute();

🎯 Points clés PDO

  1. Connexion statique : Une seule instance PDO en mémoire
  2. Exceptions : Gestion d’erreurs via try/catch
  3. Binding : :name ou ? pour les paramètres
  4. Fetch modes : PDO::FETCH_ASSOC par défaut
  5. Transactions : beginTransaction(), commit(), rollBack()

🚀 Utilisation

Structure complète :

project/
├── config/
│   └── database.php          ← Connexion PDO
├── includes/
│   ├── functions.php         ← Fonctions utilitaires
│   └── queries.php           ← Requêtes PDO
├── public/
│   ├── index.php             ← Dashboard
│   ├── categories/
│   │   ├── index.php
│   │   ├── create.php
│   │   ├── edit.php
│   │   └── delete.php
│   └── products/
│       ├── index.php
│       ├── create.php
│       ├── edit.php
│       └── delete.php
└── sql/
    └── schema.sql            ← Schéma PostgreSQL

Démarrage :

  1. Créer la BD : Exécuter sql/schema.sql
  2. Configurer : Éditer config/database.php
  3. Accéder : http://localhost/project/public/index.php

✨ Améliorations apportées

✅ PDO au lieu de pg_*
✅ Type hints avec declare(strict_types=1)
✅ Exceptions PDOException capturées
✅ Binding de paramètres sécurisé
✅ Transactions avec beginTransaction()
✅ Fetch mode uniforme PDO::FETCH_ASSOC
✅ Code plus portable (compatible autres BD)
✅ Gestion d’erreurs robuste

Le CRUD complet avec PDO est maintenant prêt ! 🎉

🐛 Correction de l’Erreur de Type

Le problème vient du fait que $product['price'] est retourné en string par PDO au lieu de float.

Solution 1 : Corriger la fonction format_price() (Recommandé)

Modifiez la fonction dans includes/functions.php :

/**
 * Formate un prix pour l'affichage
 */
function format_price(float|string $price): string {
    // Convertir en float si c'est une string
    $price = (float) $price;
    return number_format($price, 2, ',', ' ') . ' €';
}

Solution 2 : Configurer PDO pour les types numériques

Modifiez config/database.php pour que PDO retourne les nombres en tant que nombres :

<?php
declare(strict_types=1);

// Configuration PostgreSQL
const DB_HOST = "localhost";
const DB_PORT = "5432";
const DB_NAME = "shop_db";
const DB_USER = "postgres";
const DB_PASS = "postgres";

/**
 * Établit une connexion PDO à PostgreSQL
 */
function db_connect(): PDO {
    static $pdo = null;

    if ($pdo !== null) {
        return $pdo;
    }

    try {
        $dsn = sprintf(
            "pgsql:host=%s;port=%s;dbname=%s",
            DB_HOST,
            DB_PORT,
            DB_NAME
        );

        $pdo = new PDO($dsn, DB_USER, DB_PASS);

        // Configuration PDO
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
        $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

        // ✅ IMPORTANT : Convertir les nombres en types PHP
        $pdo->setAttribute(PDO::PGSQL_ATTR_DISABLE_NATIVE_PREPARED_STATEMENT, false);

        // Encodage UTF-8
        $pdo->exec("SET NAMES utf8");
        $pdo->exec("SET search_path TO public");

        return $pdo;
    } catch (PDOException $e) {
        error_log("Erreur de connexion PostgreSQL: " . $e->getMessage());
        throw new PDOException("Impossible de se connecter à la base de données");
    }
}

/**
 * Ferme la connexion PDO
 */
function db_close(): void {
    global $pdo;
    $pdo = null;
}

Solution 3 : Convertir les types dans les requêtes (Meilleure approche)

Modifiez includes/queries.php pour convertir les types au niveau de la requête :

/**
 * Récupère tous les produits avec leurs catégories (JOINTURE OPTIMISÉE)
 */
function get_all_products(PDO $pdo, ?int $category_id = null, int $limit = 20, int $offset = 0): array {
    try {
        if ($category_id !== null) {
            $query = "
                SELECT
                    p.id,
                    p.name,
                    p.description,
                    CAST(p.price AS FLOAT) as price,
                    p.quantity,
                    p.created_at,
                    p.updated_at,
                    p.category_id,
                    c.name AS category_name
                FROM products p
                INNER JOIN categories c ON p.category_id = c.id
                WHERE p.category_id = :category_id
                ORDER BY p.name ASC
                LIMIT :limit OFFSET :offset
            ";

            $stmt = $pdo->prepare($query);
            $stmt->bindValue(':category_id', $category_id, PDO::PARAM_INT);
        } else {
            $query = "
                SELECT
                    p.id,
                    p.name,
                    p.description,
                    CAST(p.price AS FLOAT) as price,
                    p.quantity,
                    p.created_at,
                    p.updated_at,
                    p.category_id,
                    c.name AS category_name
                FROM products p
                INNER JOIN categories c ON p.category_id = c.id
                ORDER BY p.name ASC
                LIMIT :limit OFFSET :offset
            ";

            $stmt = $pdo->prepare($query);
        }

        $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
        $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
        $stmt->execute();

        return $stmt->fetchAll();
    } catch (PDOException $e) {
        error_log("Erreur get_all_products: " . $e->getMessage());
        return [];
    }
}

/**
 * Récupère un produit par son ID avec sa catégorie
 */
function get_product_by_id(PDO $pdo, int $id): ?array {
    try {
        $query = "
            SELECT
                p.id,
                p.name,
                p.description,
                CAST(p.price AS FLOAT) as price,
                p.quantity,
                p.created_at,
                p.updated_at,
                p.category_id,
                c.name AS category_name
            FROM products p
            INNER JOIN categories c ON p.category_id = c.id
            WHERE p.id = :id
        ";

        $stmt = $pdo->prepare($query);
        $stmt->bindValue(':id', $id, PDO::PARAM_INT);
        $stmt->execute();

        $result = $stmt->fetch();
        return $result ?: null;
    } catch (PDOException $e) {
        error_log("Erreur get_product_by_id: " . $e->getMessage());
        return null;
    }
}

/**
 * Recherche de produits par nom (OPTIMISÉ avec ILIKE)
 */
function search_products(PDO $pdo, string $search_term, int $limit = 20, int $offset = 0): array {
    try {
        $query = "
            SELECT
                p.id,
                p.name,
                p.description,
                CAST(p.price AS FLOAT) as price,
                p.quantity,
                p.created_at,
                p.updated_at,
                p.category_id,
                c.name AS category_name
            FROM products p
            INNER JOIN categories c ON p.category_id = c.id
            WHERE p.name ILIKE :search OR p.description ILIKE :search
            ORDER BY p.name ASC
            LIMIT :limit OFFSET :offset
        ";

        $stmt = $pdo->prepare($query);
        $search_pattern = '%' . $search_term . '%';
        $stmt->bindValue(':search', $search_pattern);
        $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
        $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);

        $stmt->execute();

        return $stmt->fetchAll();
    } catch (PDOException $e) {
        error_log("Erreur search_products: " . $e->getMessage());
        return [];
    }
}

/**
 * Récupère les statistiques par catégorie (Vue optimisée)
 */
function get_category_statistics(PDO $pdo): array {
    try {
        $query = "
            SELECT
                category_id,
                category_name,
                product_count,
                total_quantity,
                CAST(avg_price AS FLOAT) as avg_price,
                CAST(min_price AS FLOAT) as min_price,
                CAST(max_price AS FLOAT) as max_price
            FROM product_stats
            ORDER BY category_name
        ";
        $stmt = $pdo->query($query);
        return $stmt->fetchAll();
    } catch (PDOException $e) {
        error_log("Erreur get_category_statistics: " . $e->getMessage());
        return [];
    }
}

Solution 4 : Créer une classe de conversion (Plus robuste)

Créez un fichier includes/types.php :

<?php
declare(strict_types=1);

/**
 * Classe pour gérer les conversions de types
 */
class TypeConverter {
    /**
     * Convertit un tableau de résultats en types corrects
     */
    public static function convertProduct(array $product): array {
        return [
            'id' => (int) $product['id'],
            'name' => (string) $product['name'],
            'description' => $product['description'] ? (string) $product['description'] : null,
            'price' => (float) $product['price'],
            'quantity' => (int) $product['quantity'],
            'category_id' => (int) $product['category_id'],
            'category_name' => (string) $product['category_name'],
            'created_at' => (string) $product['created_at'],
            'updated_at' => (string) $product['updated_at'],
        ];
    }

    /**
     * Convertit un tableau de produits
     */
    public static function convertProducts(array $products): array {
        return array_map([self::class, 'convertProduct'], $products);
    }

    /**
     * Convertit une catégorie
     */
    public static function convertCategory(array $category): array {
        return [
            'id' => (int) $category['id'],
            'name' => (string) $category['name'],
            'created_at' => (string) $category['created_at'],
            'updated_at' => (string) $category['updated_at'],
        ];
    }

    /**
     * Convertit les statistiques
     */
    public static function convertStats(array $stats): array {
        return [
            'category_id' => (int) $stats['category_id'],
            'category_name' => (string) $stats['category_name'],
            'product_count' => (int) $stats['product_count'],
            'total_quantity' => (int) $stats['total_quantity'],
            'avg_price' => (float) $stats['avg_price'],
            'min_price' => (float) $stats['min_price'],
            'max_price' => (float) $stats['max_price'],
        ];
    }
}

Puis utilisez-le dans includes/queries.php :

<?php
declare(strict_types=1);

require_once __DIR__ . '/functions.php';
require_once __DIR__ . '/types.php';

/**
 * Récupère tous les produits avec leurs catégories
 */
function get_all_products(PDO $pdo, ?int $category_id = null, int $limit = 20, int $offset = 0): array {
    try {
        if ($category_id !== null) {
            $query = "
                SELECT
                    p.id, p.name, p.description, p.price, p.quantity,
                    p.created_at, p.updated_at, p.category_id,
                    c.name AS category_name
                FROM products p
                INNER JOIN categories c ON p.category_id = c.id
                WHERE p.category_id = :category_id
                ORDER BY p.name ASC
                LIMIT :limit OFFSET :offset
            ";

            $stmt = $pdo->prepare($query);
            $stmt->bindValue(':category_id', $category_id, PDO::PARAM_INT);
        } else {
            $query = "
                SELECT
                    p.id, p.name, p.description, p.price, p.quantity,
                    p.created_at, p.updated_at, p.category_id,
                    c.name AS category_name
                FROM products p
                INNER JOIN categories c ON p.category_id = c.id
                ORDER BY p.name ASC
                LIMIT :limit OFFSET :offset
            ";

            $stmt = $pdo->prepare($query);
        }

        $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
        $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
        $stmt->execute();

        $products = $stmt->fetchAll();
        return array_map([TypeConverter::class, 'convertProduct'], $products);
    } catch (PDOException $e) {
        error_log("Erreur get_all_products: " . $e->getMessage());
        return [];
    }
}

✅ Recommandation : Utiliser la Solution 1 (Plus simple)

Modifiez simplement includes/functions.php :

/**
 * Formate un prix pour l'affichage
 */
function format_price(float|string $price): string {
    $price = (float) $price;
    return number_format($price, 2, ',', ' ') . ' €';
}

/**
 * Échappe les caractères HTML pour éviter XSS
 */
function h(string|int|float $text): string {
    return htmlspecialchars((string) $text, ENT_QUOTES, 'UTF-8');
}

Et décommentez la fonction h() dans votre fichier !


🎯 Résumé

Solution Avantages Inconvénients
1. Modifier format_price() ✅ Simple, rapide ⚠️ Conversion à chaque appel
2. Config PDO ✅ Automatique ⚠️ Peut affecter d’autres types
3. CAST SQL ✅ Au niveau DB ⚠️ À faire dans chaque requête
4. TypeConverter ✅ Robuste, réutilisable ⚠️ Plus de code

Je recommande la Solution 1 + décommenter h() pour votre cas ! 🚀

Prêt à passer à la suite ?

Vous avez terminé cette leçon sur CRUD avec PDO PostgreSQL. Continuez votre apprentissage avec la prochaine leçon ou explorez d'autres formations.