Architecture simple

Le projet Contrats & Co à plat, commenté fichier par fichier : le point de départ honnête

Voici la première architecture du projet Contrats & Co, celle de la tag ARCHITECTURE_SIMPLE. Quatre dossiers, modèle, accesseur, action, admin, et les vues à la racine. Contrats, soumissions et membres fonctionnent, chaque fichier a une seule responsabilité, les requêtes SQL vivent en constantes et les modèles filtrent ce qu'on leur donne. Une redirection suit chaque formulaire. C'est une structure qui marche et qui commence à peser : sept pages à la racine, cinq actions dans un seul dossier, deux entêtes. La suite, l'architecture MVC, ne fait que déplacer ces pièces.

projet-web-contrat/ (tag ARCHITECTURE_SIMPLE) ├── index.php le catalogue, filtrable par domaine ├── contrat.php un contrat et ses soumissions ├── proposer.php proposer un contrat (formulaire public) ├── merci.php la confirmation après une proposition ├── inscription.php devenir membre ├── connexion.php se connecter ├── profil.php le profil, seule page protégée ├── entete.php le haut de chaque page, ouvre la session ├── pied-page.php le bas de chaque page ├── .htaccess DirectoryIndex, rien d'autre ├── base-de-donnees.sql contrat, soumission, membre ├── modele/ les objets métier ├── accesseur/ la connexion PDO et les DAO ├── action/ cinq actions à plat, qui redirigent └── admin/ le volet admin, avec ses propres entête et pied de page

La racine - les pages publiques

Les vues vivent à la racine : chaque page inclut l'entête et le pied de page, demande ses données à un DAO et affiche. Sept pages, c'est déjà beaucoup pour une racine.

index.php

Le catalogue : lit le paramètre de domaine, demande la liste au DAO, affiche les cartes.

<?php
require_once 'accesseur/ContratDAO.php';

// Dictionnaire de traduction (Slug -> Valeur en BDD)
$traductionsDomaines = [
    'developpement-web' => 'Développement Web',
    'mobile' => 'Mobile',
    'cybersecurite' => 'Cybersécurité',
    'logiciels-pro' => 'Logiciels Pro'
];

$domaineFiltreSlug = null;
$domaineFiltreBDD = null;

if (isset($_GET['domaine']) && !empty($_GET['domaine'])) {
    $domaineFiltreSlug = $_GET['domaine'];
    
    // Si le slug existe dans notre dictionnaire, on filtre
    if (isset($traductionsDomaines[$domaineFiltreSlug])) {
        $domaineFiltreBDD = $traductionsDomaines[$domaineFiltreSlug];
        $listeContrats = ContratDAO::listerContratsParDomaineActifs($domaineFiltreBDD);
    } else {
        // Fallback si URL invalide
        $listeContrats = ContratDAO::listerContratsActifs();
    }
} else {
    $listeContrats = ContratDAO::listerContratsActifs();
}

include 'entete.php'; 
?>
    
    <section id="contenu">
        <aside id="panneau-lateral">
            <h3>Filtrer par domaine</h3>
            <ul id="liste-categories">
                <li class="categorie-item"><a href="index.php" style="color: inherit; text-decoration: <?= !$domaineFiltreSlug ? 'underline' : 'none' ?>;">Tous les contrats</a></li>
                <li class="categorie-item"><a href="index.php?domaine=developpement-web" style="color: inherit; text-decoration: <?= ($domaineFiltreSlug === 'developpement-web') ? 'underline' : 'none' ?>;">Développement Web</a></li>
                <li class="categorie-item"><a href="index.php?domaine=mobile" style="color: inherit; text-decoration: <?= ($domaineFiltreSlug === 'mobile') ? 'underline' : 'none' ?>;">Applications Mobile</a></li>
                <li class="categorie-item"><a href="index.php?domaine=cybersecurite" style="color: inherit; text-decoration: <?= ($domaineFiltreSlug === 'cybersecurite') ? 'underline' : 'none' ?>;">Cybersécurité</a></li>
                <li class="categorie-item"><a href="index.php?domaine=logiciels-pro" style="color: inherit; text-decoration: <?= ($domaineFiltreSlug === 'logiciels-pro') ? 'underline' : 'none' ?>;">Logiciels Pro</a></li>
            </ul>
        </aside>
        
        <main id="affichage-principal">
            <div id="navigation-historique"><a href="index.php">Accueil</a> > Tous les contrats > <span><?= $domaineFiltreBDD ? htmlspecialchars($domaineFiltreBDD) : 'Nouveautés' ?></span></div>
            <div id="liste-contrats">
                
                <?php if(empty($listeContrats)): ?>
                    <p style="padding: 20px;">Aucun contrat disponible dans ce domaine pour le moment.</p>
                <?php else: ?>
                    <?php foreach($listeContrats as $contrat): ?>
                    <article class="contrat" onclick="window.location.href='contrat.php?id=<?= $contrat->id ?>';">
                        <a href="contrat.php?id=<?= $contrat->id ?>" class="contrat-lien">
                            <div class="contrat-client"><?= $contrat->icone ?></div>
                            <div class="contrat-reference"><?= $contrat->reference ?> | <?= $contrat->typeService ?></div>
                            <h4><?= $contrat->titre ?></h4>
                            <div class="contrat-evaluation"><?= $contrat->etoilesCalculees ?> <span>(<?= $contrat->nombreEvaluations ?> évaluations)</span></div>
                            <div class="contrat-prix">
                                <span class="contrat-prix-symbole">$</span><?= $contrat->prixDollars ?><span class="contrat-prix-cents"><?= $contrat->prixCents ?></span>
                            </div>
                            <button class="action-contrat-ajout">Ajouter au panier</button>
                        </a>
                    </article>
                    <?php endforeach; ?>
                <?php endif; ?>

            </div>
        </main>
    </section>

<?php include 'pied-page.php'; ?>

contrat.php

Le détail d'un contrat et le formulaire de soumission ; sans identifiant valide, retour à l'accueil.

<?php 
require_once 'accesseur/ContratDAO.php';
require_once 'accesseur/SoumissionDAO.php';

// Vérification de la présence du paramètre id
if (!isset($_GET['id']) || empty($_GET['id'])) {
    header('Location: index.php');
    exit;
}

$idContrat = $_GET['id'];

// Récupération via le DAO
$contrat = ContratDAO::detaillerContrat($idContrat);

if (!$contrat) {
    header('Location: index.php');
    exit;
}

include 'entete.php'; 
?>
    
    <section id="contenu">
        <aside id="panneau-lateral">
            <h3>Filtrer par domaine</h3>
            <ul id="liste-categories">
                <li class="categorie-item"><a href="index.php" style="color: inherit; text-decoration: none;">Tous les contrats</a></li>
                <li class="categorie-item"><a href="index.php?domaine=developpement-web" style="color: inherit; text-decoration: none;">Développement Web</a></li>
                <li class="categorie-item"><a href="index.php?domaine=mobile" style="color: inherit; text-decoration: none;">Applications Mobile</a></li>
                <li class="categorie-item"><a href="index.php?domaine=cybersecurite" style="color: inherit; text-decoration: none;">Cybersécurité</a></li>
                <li class="categorie-item"><a href="index.php?domaine=logiciels-pro" style="color: inherit; text-decoration: none;">Logiciels Pro</a></li>
            </ul>
        </aside>
        
        <main id="affichage-principal">
            <div id="navigation-historique"><a href="index.php">Accueil</a> > Tous les contrats > <?= $contrat->typeService ?> > <span><?= $contrat->titre ?></span></div>
            
            <article id="affichage-contrat-detail">
                <div class="contrat-client"><?= $contrat->icone ?></div>
                
                <div class="contrat-informations-techniques">
                    <div class="contrat-reference"><?= $contrat->reference ?> | <?= $contrat->typeService ?></div>
                    <h2 class="contrat-titre"><?= $contrat->titre ?></h2>
                    
                    <div class="contrat-evaluation">
                        <?= $contrat->etoilesCalculees ?> <span>(<?= $contrat->nombreEvaluations ?> évaluations vérifiées)</span>
                    </div>
                    
                    <div class="contrat-zone-transaction">
                        <div class="contrat-prix">
                            <span class="contrat-prix-symbole">$</span><?= $contrat->prixDollars ?><span class="contrat-prix-cents"><?= $contrat->prixCents ?></span>
                        </div>
                        <p class="contrat-disponibilite">Prestataire disponible immédiatement</p>
                        <button class="action-contrat-paiement">Procéder au paiement</button>
                        <button class="action-contrat-ajout">Ajouter au panier</button>
                    </div>
                    
                    <div class="contrat-description">
                        <h3>Détails de la prestation</h3>
                        <p>Cette section sera dynamisée plus tard. Pour l'instant, elle simule une description générique liée au contrat.</p>
                        <p>Livrables inclus :</p>
                        <ul>
                            <li>Design adaptatif (Responsive Web Design)</li>
                            <li>Optimisation SEO de base</li>
                            <li>Formation de 2h à la remise</li>
                            <li>Garantie de corrections de 30 jours</li>
                        </ul>
                    </div>

                    <div style="margin-top: 40px; padding-top: 20px; border-top: 1px solid var(--gris-border);">
                        <h3>Faire une offre sur ce contrat</h3>
                        
                        <?php if(isset($_GET['succes'])): ?>
                            <div style="background-color: #d4edda; color: #155724; padding: 15px; border-radius: 4px; margin-bottom: 20px;">
                                ✅ Votre offre a été transmise avec succès !
                            </div>
                        <?php endif; ?>
                        
                        <form action="action/bidder-contrat.php" method="POST" style="background: #f9f9f9; padding: 20px; border-radius: 4px;">
                            <input type="hidden" name="id_contrat" value="<?= $contrat->id ?>">
                            
                            <div style="margin-bottom: 15px; display: flex; gap: 15px;">
                                <div style="flex: 1;">
                                    <label style="display: block; font-weight: bold; margin-bottom: 5px;">Votre Courriel</label>
                                    <input type="email" name="courriel" required style="width: 100%; padding: 10px; border: 1px solid #ccc;">
                                </div>
                                <div style="flex: 1;">
                                    <label style="display: block; font-weight: bold; margin-bottom: 5px;">Montant de votre offre ($)</label>
                                    <input type="number" name="montant_offre" step="0.01" required style="width: 100%; padding: 10px; border: 1px solid #ccc;">
                                </div>
                            </div>
                            
                            <div style="margin-bottom: 15px;">
                                <label style="display: block; font-weight: bold; margin-bottom: 5px;">Message pour le client</label>
                                <textarea name="message" rows="3" required style="width: 100%; padding: 10px; border: 1px solid #ccc;"></textarea>
                            </div>
                            
                            <button type="submit" style="background-color: var(--bleu-rona); color: white; border: none; padding: 10px 20px; font-weight: bold; cursor: pointer;">Soumettre mon offre</button>
                        </form>
                        
                        <h3 style="margin-top: 40px;">Offres actuelles (<?= count(SoumissionDAO::listerParContrat($contrat->id)) ?>)</h3>
                        <?php 
                        $offres = SoumissionDAO::listerParContrat($contrat->id);
                        if(empty($offres)): 
                        ?>
                            <p style="color: #777; font-style: italic;">Soyez le premier à faire une offre sur ce contrat !</p>
                        <?php else: ?>
                            <div style="display: flex; flex-direction: column; gap: 15px;">
                                <?php foreach($offres as $offre): ?>
                                    <div style="border: 1px solid #eee; padding: 15px; border-radius: 4px; border-left: 4px solid var(--jaune-accent);">
                                        <div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
                                            <strong>Offre de : <?= explode('@', $offre->courriel)[0] ?>***</strong>
                                            <span style="font-size: 1.2rem; font-weight: bold; color: var(--vert-succes);"><?= number_format($offre->montantOffre, 2) ?> $</span>
                                        </div>
                                        <p style="margin: 0; color: #555;"><?= nl2br($offre->message) ?></p>
                                        <small style="color: #999; display: block; margin-top: 10px;"><?= $offre->dateSoumission ?></small>
                                    </div>
                                <?php endforeach; ?>
                            </div>
                        <?php endif; ?>
                    </div>
                </div>
            </article>

        </main>
    </section>

<?php include 'pied-page.php'; ?>

proposer.php

Le formulaire public de proposition ; le statut en_attente est forcé par un champ caché.

<?php include 'entete.php'; ?>
    
    <section id="contenu">
        <div id="entete-catalogue">
            <h2>Proposer un mandat</h2>
            <p>Décrivez votre besoin. Un administrateur validera votre demande avant de la publier.</p>
        </div>
        
        <div style="max-width: 600px; margin: 40px auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.05);">
            <form action="action/proposer-contrat.php" method="POST">
                <input type="hidden" name="statut" value="en_attente">
                <input type="hidden" name="reference" value="WEB-USER-<?= time() ?>">
                <input type="hidden" name="icone" value="👤">
                
                <div style="margin-bottom: 20px;">
                    <label style="display: block; font-weight: bold; margin-bottom: 8px; color: #2c3e50;">Titre de votre projet</label>
                    <input type="text" name="titre" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;" placeholder="Ex: Refonte de mon site e-commerce">
                </div>
                
                <div style="margin-bottom: 20px;">
                    <label style="display: block; font-weight: bold; margin-bottom: 8px; color: #2c3e50;">Domaine d'expertise requis</label>
                    <select name="type_service" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
                        <option value="Développement Web">Développement Web</option>
                        <option value="Mobile">Mobile</option>
                        <option value="Cybersécurité">Cybersécurité</option>
                    </select>
                </div>
                
                <div style="margin-bottom: 20px;">
                    <label style="display: block; font-weight: bold; margin-bottom: 8px; color: #2c3e50;">Budget estimé ($)</label>
                    <input type="number" name="prix" step="0.01" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;" placeholder="Ex: 1500.00">
                </div>
                
                <div style="text-align: right;">
                    <button type="submit" style="background-color: #3498db; color: white; border: none; padding: 12px 24px; border-radius: 4px; font-weight: bold; cursor: pointer;">Soumettre ma demande</button>
                </div>
            </form>
        </div>
    </section>

<?php include 'pied-page.php'; ?>

merci.php

La page de remerciement vers laquelle l'action redirige après une proposition.

<?php include 'entete.php'; ?>
    
    <section id="contenu">
        <div style="text-align: center; margin: 80px auto; max-width: 600px;">
            <h1 style="font-size: 4em; margin-bottom: 20px;">🎉</h1>
            <h2 style="color: #2c3e50; margin-bottom: 15px;">Demande envoyée avec succès !</h2>
            <p style="color: #7f8c8d; margin-bottom: 30px; font-size: 1.2em;">Votre proposition de mandat a bien été enregistrée. Elle est actuellement <strong>en attente de validation</strong> par l'un de nos administrateurs.</p>
            <a href="index.php" style="display: inline-block; background-color: #3498db; color: white; text-decoration: none; padding: 12px 24px; border-radius: 4px; font-weight: bold;">Retour au catalogue</a>
        </div>
    </section>

<?php include 'pied-page.php'; ?>

inscription.php

Le formulaire d'inscription d'un membre.

<?php include 'entete.php'; ?>
    
    <section id="contenu">
        <div id="entete-catalogue">
            <h2>Devenir membre</h2>
            <p>Créez votre compte pour suivre vos soumissions et vos mandats.</p>
        </div>
        
        <div style="max-width: 600px; margin: 40px auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.05);">
            <?php if (isset($_GET['erreur']) && $_GET['erreur'] === 'courriel'): ?>
                <p id="message-erreur-inscription" style="background: #fdecea; color: #c0392b; padding: 12px; border-radius: 4px; margin-bottom: 20px;">Ce courriel est déjà associé à un compte. <a href="connexion.php">Connectez-vous</a> plutôt.</p>
            <?php endif; ?>
            <form action="action/inscrire-membre.php" method="POST">
                <div style="display: flex; gap: 20px;">
                    <div style="flex: 1; margin-bottom: 20px;">
                        <label style="display: block; font-weight: bold; margin-bottom: 8px; color: #2c3e50;">Prénom</label>
                        <input type="text" name="prenom" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
                    </div>
                    <div style="flex: 1; margin-bottom: 20px;">
                        <label style="display: block; font-weight: bold; margin-bottom: 8px; color: #2c3e50;">Nom</label>
                        <input type="text" name="nom" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
                    </div>
                </div>
                
                <div style="margin-bottom: 20px;">
                    <label style="display: block; font-weight: bold; margin-bottom: 8px; color: #2c3e50;">Courriel</label>
                    <input type="email" name="courriel" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;" placeholder="vous@exemple.com">
                </div>
                
                <div style="margin-bottom: 20px;">
                    <label style="display: block; font-weight: bold; margin-bottom: 8px; color: #2c3e50;">Mot de passe</label>
                    <input type="password" name="mot_de_passe" required minlength="8" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;" placeholder="Au moins 8 caractères">
                </div>
                
                <div style="text-align: right;">
                    <button type="submit" style="background-color: #3498db; color: white; border: none; padding: 12px 24px; border-radius: 4px; font-weight: bold; cursor: pointer;">Créer mon compte</button>
                </div>
            </form>
        </div>
    </section>

<?php include 'pied-page.php'; ?>

connexion.php

Le formulaire de connexion.

<?php include 'entete.php'; ?>
    
    <section id="contenu">
        <div id="entete-catalogue">
            <h2>Connexion</h2>
            <p>Entrez le courriel et le mot de passe de votre compte.</p>
        </div>
        
        <div style="max-width: 600px; margin: 40px auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.05);">
            <?php if (isset($_GET['erreur'])): ?>
                <p id="message-erreur-connexion" style="background: #fdecea; color: #c0392b; padding: 12px; border-radius: 4px; margin-bottom: 20px;">Courriel ou mot de passe incorrect.</p>
            <?php endif; ?>
            <form action="action/connecter-membre.php" method="POST">
                <div style="margin-bottom: 20px;">
                    <label style="display: block; font-weight: bold; margin-bottom: 8px; color: #2c3e50;">Courriel</label>
                    <input type="email" name="courriel" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
                </div>
                
                <div style="margin-bottom: 20px;">
                    <label style="display: block; font-weight: bold; margin-bottom: 8px; color: #2c3e50;">Mot de passe</label>
                    <input type="password" name="mot_de_passe" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
                </div>
                
                <div style="display: flex; justify-content: space-between; align-items: center;">
                    <a href="inscription.php" style="color: #3498db;">Pas encore de compte ? Inscrivez-vous</a>
                    <button type="submit" style="background-color: #3498db; color: white; border: none; padding: 12px 24px; border-radius: 4px; font-weight: bold; cursor: pointer;">Me connecter</button>
                </div>
            </form>
        </div>
    </section>

<?php include 'pied-page.php'; ?>

profil.php

Le profil du membre connecté : sans session, redirection vers la connexion.

<?php
if (session_status() === PHP_SESSION_NONE) {
    session_start();
}
require_once 'accesseur/MembreDAO.php';

// Pas de session, pas de profil : on renvoie a la connexion
if (!Membre::estConnecte()) {
    header('Location: connexion.php');
    exit;
}

$membre = MembreDAO::detaillerMembre(Membre::identifiantConnecte());
if ($membre === null) {
    Membre::deconnecter();
    header('Location: connexion.php');
    exit;
}

include 'entete.php';
?>
    
    <section id="contenu">
        <div id="entete-catalogue">
            <h2>Bonjour, <?= htmlspecialchars($membre->prenom) ?></h2>
            <p>Voici les informations de votre compte.</p>
        </div>
        
        <div id="fiche-profil" style="max-width: 600px; margin: 40px auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.05);">
            <div class="profil-ligne" style="display: flex; justify-content: space-between; padding: 12px 0; border-bottom: 1px solid #eee;">
                <span class="profil-etiquette" style="font-weight: bold; color: #2c3e50;">Nom complet</span>
                <span class="profil-valeur"><?= htmlspecialchars($membre->nomComplet()) ?></span>
            </div>
            <div class="profil-ligne" style="display: flex; justify-content: space-between; padding: 12px 0; border-bottom: 1px solid #eee;">
                <span class="profil-etiquette" style="font-weight: bold; color: #2c3e50;">Courriel</span>
                <span class="profil-valeur"><?= htmlspecialchars($membre->courriel) ?></span>
            </div>
            <div class="profil-ligne" style="display: flex; justify-content: space-between; padding: 12px 0;">
                <span class="profil-etiquette" style="font-weight: bold; color: #2c3e50;">Membre depuis</span>
                <span class="profil-valeur"><?= htmlspecialchars($membre->dateInscription) ?></span>
            </div>
            
            <form method="post" action="action/deconnecter-membre.php" style="text-align: right; margin-top: 30px;">
                <button type="submit" style="background: transparent; color: #c0392b; border: 1px solid #c0392b; padding: 10px 20px; border-radius: 4px; font-weight: bold; cursor: pointer;">Me déconnecter</button>
            </form>
        </div>
    </section>

<?php include 'pied-page.php'; ?>

entete.php

Le haut de chaque page : ouvre la session et affiche les liens de membre selon l'état.

<?php
if (session_status() === PHP_SESSION_NONE) {
    session_start();
}
require_once __DIR__ . '/modele/Membre.php';
?>
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Contrats & Co</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div id="header">
        <h1><a href="index.php">CONTRATS & CO</a></h1>
        <div id="recherche">
            <input type="text" class="saisie-recherche" placeholder="Rechercher par mot-clé, référence de contrat ou prestataire...">
        </div>
        <div id="navigation-membre">
            <?php if (Membre::estConnecte()): ?>
                <a href="profil.php" class="action-navigation-membre">MON PROFIL</a>
                <form method="post" action="action/deconnecter-membre.php" class="formulaire-navigation-deconnexion">
                    <button type="submit" class="action-navigation-deconnexion">Déconnexion</button>
                </form>
            <?php else: ?>
                <a href="connexion.php" class="action-navigation-membre">CONNEXION</a>
                <a href="inscription.php" class="action-navigation-membre">INSCRIPTION</a>
            <?php endif; ?>
            <a href="admin/index.php" class="action-navigation-membre">ADMIN</a>
        </div>
    </div>
    
    <nav id="menu">
        <a class="lien-page" href="index.php?domaine=developpement-web">Développement Web</a>
        <a class="lien-page" href="index.php?domaine=mobile">Mobile</a>
        <a class="lien-page" href="index.php?domaine=cybersecurite">Cybersécurité</a>
        <a class="lien-page" href="index.php?domaine=logiciels-pro">Logiciels Pro</a>
    </nav>

pied-page.php

Le bas de chaque page.

</body>
</html>

.htaccess

Une seule ligne : index.php comme page par défaut.

DirectoryIndex index.php

Le modèle - les objets métier

Une classe par table. Chaque objet se construit depuis un tableau, ligne SQL ou formulaire, le filtre, et expose des propriétés propres.

modele/Contrat.php

L'objet Contrat : se construit depuis une ligne SQL ou un formulaire, filtre ce qu'on lui donne, calcule ses étoiles et son prix affiché.

<?php

class Contrat
{
    public const FILTRES = array( // Requiert PHP 5.6+
        'id' => FILTER_VALIDATE_INT,
        'reference' => FILTER_UNSAFE_RAW,
        'titre' => FILTER_UNSAFE_RAW,
        'type_service' => FILTER_UNSAFE_RAW,
        'icone' => FILTER_UNSAFE_RAW,
        'evaluation' => FILTER_VALIDATE_FLOAT,
        'nombre_evaluations' => FILTER_VALIDATE_INT,
        'prix' => FILTER_UNSAFE_RAW,
        'statut' => FILTER_UNSAFE_RAW
    );
        
    protected $id;
    protected $reference;
    protected $titre;
    protected $typeService;
    protected $icone;
    protected $evaluation;
    protected $nombreEvaluations;
    protected $prix;
    protected $statut;
    
    protected $etoilesCalculees;
    protected $prixDollars;
    protected $prixCents;
    
    public function __construct($tableau)
    {
        $tableau = filter_var_array($tableau, self::FILTRES);

        $this->id = $tableau['id'];
        $this->reference = $tableau['reference'];
        $this->titre = $tableau['titre'];
        $this->typeService = $tableau['type_service'];
        $this->icone = $tableau['icone'];
        $this->evaluation = $tableau['evaluation'] ?? 0;
        $this->nombreEvaluations = $tableau['nombre_evaluations'] ?? 0;
        $this->prix = $tableau['prix'];
        $this->statut = $tableau['statut'] ?? 'publie';
        
        $etoilesPleines = floor((float)$this->evaluation);
        $etoilesVides = 5 - $etoilesPleines;
        $this->etoilesCalculees = str_repeat('★', $etoilesPleines) . str_repeat('☆', $etoilesVides);
        
        $prixParts = explode('.', $this->prix);
        $this->prixDollars = $prixParts[0];
        $this->prixCents = isset($prixParts[1]) ? $prixParts[1] : '00';
    }
    
    public function __set($propriete, $valeur)
    {
        switch($propriete)
        {
            case 'id': $this->id = $valeur; break;
            case 'reference': $this->reference = $valeur; break;
            case 'titre': $this->titre = $valeur; break;
            case 'typeService': $this->typeService = $valeur; break;
            case 'icone': $this->icone = $valeur; break;
            case 'evaluation': $this->evaluation = $valeur; break;
            case 'nombreEvaluations': $this->nombreEvaluations = $valeur; break;
            case 'prix': $this->prix = $valeur; break;
            case 'statut': $this->statut = $valeur; break;
            case 'etoilesCalculees': $this->etoilesCalculees = $valeur; break;
            case 'prixDollars': $this->prixDollars = $valeur; break;
            case 'prixCents': $this->prixCents = $valeur; break;
        }
    }

    public function __get($propriete)
    {
        $self = get_object_vars($this);
        return $self[$propriete] ?? null;
    }    
}
?>

modele/Soumission.php

L'objet Soumission : une offre déposée sur un contrat.

<?php

class Soumission
{
    public const FILTRES = array(
        'id' => FILTER_VALIDATE_INT,
        'id_contrat' => FILTER_VALIDATE_INT,
        'courriel' => FILTER_SANITIZE_EMAIL,
        'montant_offre' => FILTER_VALIDATE_FLOAT,
        'message' => FILTER_UNSAFE_RAW,
        'date_soumission' => FILTER_UNSAFE_RAW
    );
        
    protected $id;
    protected $idContrat;
    protected $courriel;
    protected $montantOffre;
    protected $message;
    protected $dateSoumission;
    
    public function __construct($tableau)
    {
        $tableau = filter_var_array($tableau, self::FILTRES);

        $this->id = $tableau['id'] ?? null;
        $this->idContrat = $tableau['id_contrat'];
        $this->courriel = $tableau['courriel'];
        $this->montantOffre = $tableau['montant_offre'];
        $this->message = $tableau['message'];
        $this->dateSoumission = $tableau['date_soumission'] ?? null;
    }
    
    public function __set($propriete, $valeur)
    {
        switch($propriete)
        {
            case 'id': $this->id = $valeur; break;
            case 'idContrat': $this->idContrat = $valeur; break;
            case 'courriel': $this->courriel = $valeur; break;
            case 'montantOffre': $this->montantOffre = $valeur; break;
            case 'message': $this->message = $valeur; break;
            case 'dateSoumission': $this->dateSoumission = $valeur; break;
        }
    }

    public function __get($propriete)
    {
        $self = get_object_vars($this);
        return $self[$propriete] ?? null;
    }    
}
?>

modele/Membre.php

L'objet Membre : ses propriétés, et la session (connecter, déconnecter, estConnecte, identifiantConnecte).

<?php

class Membre
{
    public const FILTRES = array(
        'id' => FILTER_VALIDATE_INT,
        'courriel' => FILTER_SANITIZE_EMAIL,
        'mot_de_passe' => FILTER_UNSAFE_RAW,
        'nom' => FILTER_UNSAFE_RAW,
        'prenom' => FILTER_UNSAFE_RAW,
        'date_inscription' => FILTER_UNSAFE_RAW
    );

    protected $id;
    protected $courriel;
    protected $motDePasse;
    protected $nom;
    protected $prenom;
    protected $dateInscription;

    public function __construct($tableau)
    {
        $tableau = filter_var_array($tableau, self::FILTRES);

        $this->id = $tableau['id'] ?? null;
        $this->courriel = $tableau['courriel'];
        $this->motDePasse = $tableau['mot_de_passe'];
        $this->nom = $tableau['nom'];
        $this->prenom = $tableau['prenom'];
        $this->dateInscription = $tableau['date_inscription'] ?? null;
    }

    public function __set($propriete, $valeur)
    {
        switch($propriete)
        {
            case 'id': $this->id = $valeur; break;
            case 'courriel': $this->courriel = $valeur; break;
            case 'motDePasse': $this->motDePasse = $valeur; break;
            case 'nom': $this->nom = $valeur; break;
            case 'prenom': $this->prenom = $valeur; break;
            case 'dateInscription': $this->dateInscription = $valeur; break;
        }
    }

    public function __get($propriete)
    {
        $self = get_object_vars($this);
        return $self[$propriete] ?? null;
    }

    public function nomComplet()
    {
        return trim($this->prenom . ' ' . $this->nom);
    }

    // Gestion de la session : le membre connecte est identifie par son id, rien d'autre n'est garde en session

    public static function connecter(Membre $membre)
    {
        $_SESSION['idMembre'] = $membre->id;
    }

    public static function deconnecter()
    {
        unset($_SESSION['idMembre']);
        session_destroy();
    }

    public static function estConnecte()
    {
        return isset($_SESSION['idMembre']);
    }

    public static function identifiantConnecte()
    {
        return $_SESSION['idMembre'] ?? null;
    }
}
?>

L'accesseur - la porte vers les données

Une seule connexion PDO, un DAO par table qui hérite de Accesseur et implémente son interface SQL. Un DAO ne connaît ni $_GET ni $_POST.

accesseur/Accesseur.php

La connexion PDO unique, partagée par tous les DAO. Les identifiants montrés sont des exemples.

<?php
class Accesseur {
    protected static $pdo = null;
    
    protected static function obtenirConnexion() {
        if (self::$pdo === null) {
            $dsn = 'mysql:host=localhost;dbname=contrats;charset=utf8mb4';
            $usager = 'usager_exemple';
            $motdepasse = 'mot_de_passe_exemple';
            self::$pdo = new PDO($dsn, $usager, $motdepasse);
            self::$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        }
        return self::$pdo;
    }
}
?>

accesseur/ContratDAO.php

Le DAO du contrat : lister, filtrer par domaine, détailler, ajouter, éditer, effacer. C'est lui qui parle à la base.

<?php
require_once 'Accesseur.php';
require_once 'ContratSQL.php';
require_once __DIR__ . '/../modele/Contrat.php';

class ContratDAO extends Accesseur implements ContratSQL {
    
    // Pour l'interface d'administration
    public static function listerTousLesContrats() {
        $pdo = self::obtenirConnexion();
        $requete = $pdo->query(self::SQL_LISTER_TOUS);
        $liste = $requete->fetchAll(PDO::FETCH_ASSOC);
        
        $contrats = [];
        foreach ($liste as $ligne) {
            $contrats[] = new Contrat($ligne);
        }
        return $contrats;
    }

    // Pour l'interface publique (tous les actifs)
    public static function listerContratsActifs() {
        $pdo = self::obtenirConnexion();
        $requete = $pdo->query(self::SQL_LISTER_ACTIFS);
        $liste = $requete->fetchAll(PDO::FETCH_ASSOC);
        
        $contrats = [];
        foreach ($liste as $ligne) {
            $contrats[] = new Contrat($ligne);
        }
        return $contrats;
    }
    
    // Pour l'interface publique (filtrés par domaine)
    public static function listerContratsParDomaineActifs($domaine) {
        $pdo = self::obtenirConnexion();
        $requete = $pdo->prepare(self::SQL_LISTER_PAR_DOMAINE_ACTIFS);
        $requete->execute([$domaine]);
        $liste = $requete->fetchAll(PDO::FETCH_ASSOC);
        
        $contrats = [];
        foreach ($liste as $ligne) {
            $contrats[] = new Contrat($ligne);
        }
        return $contrats;
    }
    
    public static function detaillerContrat($id) {
        $pdo = self::obtenirConnexion();
        $requete = $pdo->prepare(self::SQL_DETAILLER_CONTRAT);
        $requete->execute([$id]);
        $ligne = $requete->fetch(PDO::FETCH_ASSOC);
        
        if ($ligne) {
            return new Contrat($ligne);
        }
        return null;
    }
    
    public static function ajouterContrat(Contrat $contrat) {
        $pdo = self::obtenirConnexion();
        $requete = $pdo->prepare(self::SQL_AJOUTER_CONTRAT);
        $requete->execute([
            $contrat->reference,
            $contrat->titre,
            $contrat->typeService,
            $contrat->icone,
            $contrat->prix,
            $contrat->statut
        ]);
    }
    
    public static function editerContrat(Contrat $contrat) {
        $pdo = self::obtenirConnexion();
        $requete = $pdo->prepare(self::SQL_EDITER_CONTRAT);
        $requete->execute([
            $contrat->reference,
            $contrat->titre,
            $contrat->typeService,
            $contrat->icone,
            $contrat->prix,
            $contrat->statut,
            $contrat->id
        ]);
    }
    
    public static function effacerContrat($id) {
        $pdo = self::obtenirConnexion();
        $requete = $pdo->prepare(self::SQL_EFFACER_CONTRAT);
        $requete->execute([$id]);
    }
}
?>

accesseur/ContratSQL.php

Les requêtes SQL du contrat, en constantes d'interface : jamais une requête dans une vue ni dans une action.

<?php
interface ContratSQL {
    public const SQL_LISTER_TOUS = "SELECT * FROM contrat";
    public const SQL_LISTER_ACTIFS = "SELECT * FROM contrat WHERE statut = 'publie'";
    public const SQL_LISTER_PAR_DOMAINE_ACTIFS = "SELECT * FROM contrat WHERE statut = 'publie' AND type_service = ?";
    public const SQL_DETAILLER_CONTRAT = "SELECT * FROM contrat WHERE id = ?";
    public const SQL_AJOUTER_CONTRAT = "INSERT INTO contrat (reference, titre, type_service, icone, prix, statut) VALUES (?, ?, ?, ?, ?, ?)";
    public const SQL_EDITER_CONTRAT = "UPDATE contrat SET reference = ?, titre = ?, type_service = ?, icone = ?, prix = ?, statut = ? WHERE id = ?";
    public const SQL_EFFACER_CONTRAT = "DELETE FROM contrat WHERE id = ?";
}
?>

accesseur/SoumissionDAO.php

Le DAO de la soumission : ajouter, lister par contrat.

<?php
require_once 'Accesseur.php';
require_once 'SoumissionSQL.php';
require_once __DIR__ . '/../modele/Soumission.php';

class SoumissionDAO extends Accesseur implements SoumissionSQL {
    
    public static function ajouterSoumission(Soumission $soumission) {
        $pdo = self::obtenirConnexion();
        $requete = $pdo->prepare(self::SQL_AJOUTER_SOUMISSION);
        $requete->execute([
            $soumission->idContrat,
            $soumission->courriel,
            $soumission->montantOffre,
            $soumission->message
        ]);
    }

    public static function listerParContrat($idContrat) {
        $pdo = self::obtenirConnexion();
        $requete = $pdo->prepare(self::SQL_LISTER_PAR_CONTRAT);
        $requete->execute([$idContrat]);
        $liste = $requete->fetchAll(PDO::FETCH_ASSOC);
        
        $soumissions = [];
        foreach ($liste as $ligne) {
            $soumissions[] = new Soumission($ligne);
        }
        return $soumissions;
    }
}
?>

accesseur/SoumissionSQL.php

Les requêtes SQL de la soumission.

<?php
interface SoumissionSQL {
    public const SQL_AJOUTER_SOUMISSION = "INSERT INTO soumission (id_contrat, courriel, montant_offre, message) VALUES (?, ?, ?, ?)";
    public const SQL_LISTER_PAR_CONTRAT = "SELECT * FROM soumission WHERE id_contrat = ? ORDER BY date_soumission DESC";
}
?>

accesseur/MembreDAO.php

Le DAO du membre : ajouter, détailler par identifiant ou par courriel, lister.

<?php
require_once 'Accesseur.php';
require_once 'MembreSQL.php';
require_once __DIR__ . '/../modele/Membre.php';

class MembreDAO extends Accesseur implements MembreSQL {

    public static function ajouterMembre(Membre $membre) {
        $pdo = self::obtenirConnexion();
        $requete = $pdo->prepare(self::SQL_AJOUTER_MEMBRE);
        $requete->execute([
            $membre->courriel,
            $membre->motDePasse,
            $membre->nom,
            $membre->prenom
        ]);
        return $pdo->lastInsertId();
    }

    public static function detaillerMembre($id) {
        $pdo = self::obtenirConnexion();
        $requete = $pdo->prepare(self::SQL_DETAILLER_MEMBRE);
        $requete->execute([$id]);
        $ligne = $requete->fetch(PDO::FETCH_ASSOC);
        if ($ligne) {
            return new Membre($ligne);
        }
        return null;
    }

    public static function detaillerParCourriel($courriel) {
        $pdo = self::obtenirConnexion();
        $requete = $pdo->prepare(self::SQL_DETAILLER_PAR_COURRIEL);
        $requete->execute([$courriel]);
        $ligne = $requete->fetch(PDO::FETCH_ASSOC);
        if ($ligne) {
            return new Membre($ligne);
        }
        return null;
    }

    public static function listerTousLesMembres() {
        $pdo = self::obtenirConnexion();
        $requete = $pdo->query(self::SQL_LISTER_TOUS_LES_MEMBRES);
        $liste = $requete->fetchAll(PDO::FETCH_ASSOC);

        $membres = [];
        foreach ($liste as $ligne) {
            $membres[] = new Membre($ligne);
        }
        return $membres;
    }
}
?>

accesseur/MembreSQL.php

Les requêtes SQL du membre.

<?php
interface MembreSQL {
    public const SQL_AJOUTER_MEMBRE = "INSERT INTO membre (courriel, mot_de_passe, nom, prenom) VALUES (?, ?, ?, ?)";
    public const SQL_DETAILLER_MEMBRE = "SELECT * FROM membre WHERE id = ?";
    public const SQL_DETAILLER_PAR_COURRIEL = "SELECT * FROM membre WHERE courriel = ?";
    public const SQL_LISTER_TOUS_LES_MEMBRES = "SELECT * FROM membre ORDER BY date_inscription DESC";
}
?>

L'action - le traitement des formulaires

Aucun HTML : on reçoit $_POST, on construit un modèle, on appelle un DAO, on redirige. Cinq fichiers à plat dans un seul dossier, et une redirection à la fin de chacun : c'est ce que la version MVC changera.

action/proposer-contrat.php

Enregistre un contrat proposé, puis redirige vers merci.php.

<?php
require_once '../accesseur/ContratDAO.php';

// Le statut 'en_attente' est forcé par le formulaire public via le champ caché
$nouveauContrat = new Contrat($_POST);

// Sauvegarde en base de données
ContratDAO::ajouterContrat($nouveauContrat);

// Redirection vers la page de remerciement au lieu de l'administration
header('Location: ../merci.php');
exit;
?>

action/bidder-contrat.php

Enregistre une soumission, puis redirige vers le contrat avec un indicateur de succès.

<?php
require_once '../accesseur/SoumissionDAO.php';

$soumission = new Soumission($_POST);
SoumissionDAO::ajouterSoumission($soumission);

// On ramène l'utilisateur sur le contrat avec un message de succès
header('Location: ../contrat.php?id=' . $_POST['id_contrat'] . '&succes=1');
exit;
?>

action/inscrire-membre.php

Refuse un courriel déjà pris, garde le mot de passe en empreinte, connecte le membre, redirige vers le profil.

<?php
if (session_status() === PHP_SESSION_NONE) {
    session_start();
}
require_once '../accesseur/MembreDAO.php';

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    header('Location: ../inscription.php');
    exit;
}

$nouveauMembre = new Membre($_POST);

// Un courriel ne sert qu'a un seul compte
if (MembreDAO::detaillerParCourriel($nouveauMembre->courriel) !== null) {
    header('Location: ../inscription.php?erreur=courriel');
    exit;
}

// Le mot de passe n'est jamais garde en clair : on en stocke l'empreinte
$nouveauMembre->motDePasse = password_hash($nouveauMembre->motDePasse, PASSWORD_DEFAULT);
$nouveauMembre->id = MembreDAO::ajouterMembre($nouveauMembre);

// Le nouveau membre est connecte immediatement
Membre::connecter($nouveauMembre);
header('Location: ../profil.php');
exit;
?>

action/connecter-membre.php

Vérifie l'empreinte du mot de passe ; même erreur que le courriel existe ou non.

<?php
if (session_status() === PHP_SESSION_NONE) {
    session_start();
}
require_once '../accesseur/MembreDAO.php';

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    header('Location: ../connexion.php');
    exit;
}

$courriel = filter_input(INPUT_POST, 'courriel', FILTER_SANITIZE_EMAIL);
$motDePasse = $_POST['mot_de_passe'] ?? '';

$membre = MembreDAO::detaillerParCourriel($courriel);

// Meme message d'erreur que le courriel existe ou non : on ne renseigne pas un inconnu
if ($membre === null || !password_verify($motDePasse, $membre->motDePasse)) {
    header('Location: ../connexion.php?erreur=1');
    exit;
}

Membre::connecter($membre);
header('Location: ../profil.php');
exit;
?>

action/deconnecter-membre.php

Ferme la session, seulement sur un POST : se déconnecter change l'état, jamais par un lien.

<?php
if (session_status() === PHP_SESSION_NONE) {
    session_start();
}
require_once '../modele/Membre.php';

// Se deconnecter change l'etat : on exige un formulaire POST, jamais un simple lien
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    Membre::deconnecter();
}

header('Location: ../index.php');
exit;
?>

L'admin - le volet administratif

Le même dessin que le volet public, avec ses propres entête et pied de page : un tableau de bord, un formulaire d'ajout et d'édition, et trois actions. L'effacement n'accepte que le POST d'un formulaire : un lien se suit par accident, un formulaire s'envoie exprès.

admin/index.php

Le tableau de bord : inclut la liste des contrats et celle des membres.

<?php
include 'entete-admin.php';
?>
    
    <section id="contenu" class="contenu-admin">
        <h1>Tableau de bord administrateur</h1>
        <p class="introduction-admin">Bienvenue dans l'espace de gestion. Vous pouvez administrer les différentes entités du système ci-dessous.</p>
        
        <?php 
        // 1. Gestion des Contrats (Entité active)
        include 'liste-contrats.php'; 
        
        // 2. Gestion des Membres (Entité active)
        include 'liste-membres.php';

        // 3. Gestion des Transactions (À venir)
        // include 'liste-transactions.php';
        ?>
    </section>

<?php include 'pied-page-admin.php'; ?>

admin/liste-contrats.php

La liste des contrats avec les actions d'édition et d'effacement.

<?php
require_once '../accesseur/ContratDAO.php';
$listeContrats = ContratDAO::listerTousLesContrats();
?>
<div class="panneau-entite">
    <div id="liste-barre-actions">
        <h2>Gestion des contrats</h2>
        <a href="formulaire-contrat.php" class="action-contrat-creation">➕ Ajouter un contrat</a>
    </div>
    
    <div id="boite-administration-contrats">
        <div class="entete-administration-contrats">
            <div>Visuel</div>
            <div>Contrat & Description</div>
            <div>Domaine</div>
            <div>Tarif Unitaire</div>
                <div>Statut</div>
            <div>Actions</div>
        </div>
        
        <?php foreach ($listeContrats as $contrat): ?>
        <div class="ligne-administration-contrat">
            <div class="contrat-illustration"><?= $contrat->icone ?></div>
            <div>
                <strong><?= $contrat->titre ?></strong>
                <span class="contrat-reference"><?= $contrat->reference ?> | <?= $contrat->typeService ?></span>
            </div>
            <div><?= $contrat->typeService ?></div>
            <div class="contrat-prix"><?= $contrat->prixDollars ?>.<?= $contrat->prixCents ?>&nbsp;$</div>
            <div><?= $contrat->statut === "en_attente" ? "⏳ En attente" : "✅ Publié" ?></div>
            <div class="bloc-actions-contrat">
                <a href="formulaire-contrat.php?id=<?= $contrat->id ?>" class="action-contrat-edition">Éditer</a>
                <form method="post" action="action/effacer-contrat.php" class="formulaire-contrat-suppression" onsubmit="return confirm('Voulez-vous vraiment effacer ce contrat ?');">
                    <input type="hidden" name="id" value="<?= $contrat->id ?>">
                    <button type="submit" class="action-contrat-suppression">Effacer</button>
                </form>
            </div>
        </div>
        <?php endforeach; ?>
    </div>
</div>

admin/liste-membres.php

La liste des membres inscrits.

<?php
require_once '../accesseur/MembreDAO.php';
$listeMembres = MembreDAO::listerTousLesMembres();
?>
<div class="panneau-entite">
    <div id="liste-barre-actions-membres">
        <h2>Gestion des membres</h2>
    </div>
    
    <div id="boite-administration-membres">
        <div class="entete-administration-membres">
            <div>Membre</div>
            <div>Courriel</div>
            <div>Inscrit le</div>
        </div>
        
        <?php foreach ($listeMembres as $membre): ?>
        <div class="ligne-administration-membre">
            <div><strong><?= htmlspecialchars($membre->nomComplet()) ?></strong></div>
            <div><?= htmlspecialchars($membre->courriel) ?></div>
            <div><?= htmlspecialchars($membre->dateInscription) ?></div>
        </div>
        <?php endforeach; ?>
        
        <?php if (count($listeMembres) === 0): ?>
        <div class="ligne-administration-membre">
            <div>Aucun membre inscrit pour le moment.</div>
        </div>
        <?php endif; ?>
    </div>
</div>

admin/formulaire-contrat.php

Le formulaire d'ajout, ou d'édition quand un identifiant est fourni.

<?php
require_once '../accesseur/ContratDAO.php';

$actionFormulaire = "action/ajouter-contrat.php";
$contrat = null;

if (isset($_GET['id']) && !empty($_GET['id'])) {
    $idContrat = $_GET['id'];
    $contrat = ContratDAO::detaillerContrat($idContrat);
    if ($contrat) {
        $actionFormulaire = "action/editer-contrat.php";
    }
}

include 'entete-admin.php'; 
?>
    <section id="contenu" class="contenu-formulaire">
        <nav id="navigation-historique-admin">
            <a href="index.php">← Retour à la liste</a>
        </nav>
        
        <div id="formulaire-conteneur-principal">
            <h2 id="titre-edition-contrat"><?= $contrat ? 'Éditer un contrat' : 'Ajouter un contrat' ?></h2>
            
            <form action="<?= $actionFormulaire ?>" method="POST" id="formulaire-saisie-contrat">
                <?php if ($contrat): ?>
                    <input type="hidden" name="id" value="<?= $contrat->id ?>">
                <?php endif; ?>

                <div class="contrat-ligne-saisie">
                    <div class="contrat-groupe-saisie">
                        <label class="contrat-etiquette-saisie">Titre du contrat <span>*</span></label>
                        <input type="text" name="titre" class="contrat-champ-saisie" placeholder="Ex: Audit de Cybersécurité" value="<?= $contrat->titre ?? '' ?>" required>
                    </div>
                    <div class="contrat-groupe-saisie" style="flex: 0.5;">
                        <label class="contrat-etiquette-saisie">Statut <span>*</span></label>
                        <select name="statut" class="contrat-champ-saisie">
                            <option value="publie" <?= ($contrat && $contrat->statut === "publie") ? "selected" : "" ?>>Publié</option>
                            <option value="en_attente" <?= ($contrat && $contrat->statut === "en_attente") ? "selected" : "" ?>>En attente</option>
                        </select>
                    </div>
                </div>
                
                <div class="contrat-ligne-saisie">
                    <div class="contrat-groupe-saisie">
                        <label class="contrat-etiquette-saisie">Référence (SKU)</label>
                        <input type="text" name="reference" class="contrat-champ-saisie" placeholder="Ex: SEC-2026-03" value="<?= $contrat->reference ?? '' ?>">
                    </div>
                    <div class="contrat-groupe-saisie">
                        <label class="contrat-etiquette-saisie">Valeur du contrat ($) <span>*</span></label>
                        <input type="number" name="prix" class="contrat-champ-saisie" step="0.01" placeholder="0.00" value="<?= $contrat->prix ?? '' ?>" required>
                    </div>
                </div>
                
                <div class="contrat-ligne-saisie">
                    <div class="contrat-groupe-saisie">
                        <label class="contrat-etiquette-saisie">Domaine d'expertise</label>
                        <select name="type_service" class="contrat-champ-saisie">
                            <option value="Développement Web" <?= ($contrat && $contrat->typeService === 'Développement Web') ? 'selected' : '' ?>>Développement Web</option>
                            <option value="Mobile" <?= ($contrat && $contrat->typeService === 'Mobile') ? 'selected' : '' ?>>Mobile</option>
                            <option value="Cybersécurité" <?= ($contrat && $contrat->typeService === 'Cybersécurité') ? 'selected' : '' ?>>Cybersécurité</option>
                            <option value="Logiciels Pro" <?= ($contrat && $contrat->typeService === 'Logiciels Pro') ? 'selected' : '' ?>>Logiciels Pro</option>
                        </select>
                    </div>
                    <div class="contrat-groupe-saisie">
                        <label class="contrat-etiquette-saisie">Icône (Émoji)</label>
                        <select name="icone" class="contrat-champ-saisie">
                            <option value="🌐" <?= ($contrat && $contrat->icone === '🌐') ? 'selected' : '' ?>>🌐 Web</option>
                            <option value="📱" <?= ($contrat && $contrat->icone === '📱') ? 'selected' : '' ?>>📱 Mobile</option>
                            <option value="🛡️" <?= ($contrat && $contrat->icone === '🛡️') ? 'selected' : '' ?>>🛡️ Sécurité</option>
                            <option value="🎹" <?= ($contrat && $contrat->icone === '🎹') ? 'selected' : '' ?>>🎹 Audio</option>
                        </select>
                    </div>
                </div>
                
                <div id="barre-actions-edition">
                    <a href="index.php" class="action-contrat-annulation">Annuler</a>
                    <button type="submit" class="action-contrat-sauvegarde">Enregistrer le contrat</button>
                </div>
            </form>
        </div>
    </section>

<?php include 'pied-page-admin.php'; ?>

admin/entete-admin.php

L'entête propre au volet admin.

<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Administration - Contrats & Co</title>
    <link rel="stylesheet" href="../style.css">
    <link rel="stylesheet" href="style-admin.css">
</head>
<body>
    <div id="header" class="header-admin">
        <h1><a href="../index.php">[ADMIN] CONTRATS & CO</a></h1>
        <div>
        </div>
    </div>

admin/pied-page-admin.php

Le pied de page du volet admin.

</body>
</html>

admin/action/ajouter-contrat.php

Ajoute un contrat, puis redirige vers le tableau de bord.

<?php
require_once '../../accesseur/ContratDAO.php';

// Création de l'objet Modèle qui s'auto-filtre
$contrat = new Contrat($_POST);

// Sauvegarde via le DAO
ContratDAO::ajouterContrat($contrat);

// Redirection (pourrait être retirée si appelé strictement en AJAX)
header('Location: ../index.php');
exit;
?>

admin/action/editer-contrat.php

Édite un contrat, puis redirige.

<?php
require_once '../../accesseur/ContratDAO.php';

$contrat = new Contrat($_POST);
ContratDAO::editerContrat($contrat);

header('Location: ../index.php');
exit;
?>

admin/action/effacer-contrat.php

Efface un contrat sur un POST avec identifiant entier valide, puis redirige.

<?php
require_once '../../accesseur/ContratDAO.php';

// Un effacement ne se fait jamais sur un simple lien (GET) : on exige un envoi de formulaire (POST)
// et un identifiant entier valide. Tout le reste est ignore et renvoie a la liste.
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $idContrat = filter_input(INPUT_POST, 'id', FILTER_VALIDATE_INT);
    if ($idContrat !== false && $idContrat !== null) {
        ContratDAO::effacerContrat($idContrat);
    }
}

header('Location: ../index.php');
exit;
?>