Retour aux projets
Projet associatif · en binôme 2026 Avec l'IA

Site du BDE MMI

Le site du bureau des étudiants MMI de Troyes. Les événements, une page par soirée, un back-office pour que le bureau publie tout seul, et un module de recrutement. Codé en Symfony avec mon binôme, en trois semaines.

Symfony
Page d'accueil du site du BDE MMI avec les prochains événements
Symfony
MySQL
Twig
9
9
Routes Symfony
2
2
Entités Doctrine
4
4
Tests PHPUnit
3
3
Semaines

Ma part et le rôle de l'IA

Le point de départ

Le BDE avait besoin d'un endroit fixe pour ses événements. La date, le lieu, le prix adhérent et non adhérent, l'affiche, le lien de billetterie. Et surtout, le bureau devait pouvoir tout publier lui-même, sans attendre un développeur.

On l'a fait à deux. Mon binôme a monté la structure SCSS mobile-first, la mise en page de l'accueil, le module de recrutement et la mise en prod. Moi j'ai pris le cœur Symfony : l'entité Evenement, les listes à venir et passés, la page détail, le bloc des 3 prochains événements sur l'accueil, et tout le back-office (connexion admin, CRUD, upload des affiches).

Les outils et pourquoi

Symfony 7.4
Le framework PHP qui tient tout le projet. Les routes sont déclarées en attributs au-dessus des méthodes, la sécurité et les formulaires sont fournis.
Routing · Security · Forms
Doctrine + MySQL
Un événement est une classe PHP. Doctrine crée la table, génère les migrations et me rend des objets au lieu de tableaux SQL.
Entités · Migrations · QueryBuilder
Twig
Un gabarit de base que chaque page complète. La carte d'un événement est écrite une fois et réutilisée sur l'accueil et sur la liste.
Héritage · Partials · Filtres
AssetMapper + Sass
Le SCSS est compilé par Symfony lui-même. Pas de Webpack, pas de Node à installer sur le serveur.
SCSS · Mobile-first · Sans Node
PHP natif + PDO
Le module de recrutement de mon binôme. Candidatures et créneaux d'entretien, en PHP sans framework, avec des requêtes préparées.
PDO · Requêtes préparées
PHPUnit
Quatre tests sur une base dédiée. Un brouillon ne s'affiche jamais, l'admin est verrouillé, et un simple lien ne peut rien supprimer.
WebTestCase · Base de test

Comment le code tourne

Route #[Route] sur la méthode
Contrôleur vérifie, décide
Repository QueryBuilder
Doctrine · MySQL objets Evenement
Twig page HTML

Chaque page suit le même trajet. Symfony lit l'URL et trouve la route, déclarée juste au-dessus de la méthode du contrôleur. Le contrôleur demande les données au repository, qui écrit la requête avec le QueryBuilder de Doctrine. Ce qui revient, ce sont des objets Evenement, pas des lignes SQL.

Le contrôleur passe ces objets à un template Twig. Tous les templates héritent de base.html.twig, et la carte d'un événement vit dans un partial à part. Le module de recrutement de mon binôme est rangé dans son propre dossier, en PHP natif. Même site, deux codes séparés.

Arborescence (simplifiée)
src/
├── Controller/
│   ├── AccueilController.php
│   ├── EvenementController.php
│   ├── SecurityController.php
│   └── Admin/EvenementAdminController.php
├── Entity/        Evenement, Utilisateur
├── Repository/    les requêtes Doctrine
├── Form/          EvenementType
└── Command/       créer un admin en ligne de commande
templates/
├── base.html.twig
├── accueil/  evenement/  admin/  security/
└── partials/_carte_evenement.html.twig
recrutement/       module PHP natif de mon binôme
tests/Controller/  4 tests PHPUnit

Les événements à venir

Une soirée sur plusieurs jours ne doit pas partir en archive dès le premier soir. COALESCE prend la date de fin si elle existe, sinon la date de début. Les brouillons sont filtrés dans la même requête.

src/Repository/EvenementRepository.php
public function trouverAVenir(): array
{
    return $this->createQueryBuilder('e')
        ->andWhere('e.publie = true')
        ->andWhere('COALESCE(e.dateFin, e.dateDebut) >= :maintenant')
        ->setParameter('maintenant', new \DateTime())
        ->orderBy('e.dateDebut', 'ASC')
        ->getQuery()
        ->getResult();
}

La page d'un événement

Le contrôleur ne reçoit pas un numéro mais directement un Evenement. Symfony le cherche en base et renvoie une 404 tout seul si l'id n'existe pas. Un brouillon donne aussi une 404 au public, mais un admin connecté peut le prévisualiser avant de le publier.

src/Controller/EvenementController.php
#[Route('/evenements/{id}', name: 'evenement_detail')]
public function detail(Evenement $evenement): Response
{
    // Un brouillon reste invisible pour le public,
    // mais un admin peut le prévisualiser
    if (!$evenement->isPublie() && !$this->isGranted('ROLE_ADMIN')) {
        throw $this->createNotFoundException('Événement non trouvé');
    }
    return $this->render('evenement/detail.html.twig', [
        'evenement' => $evenement,
    ]);
}

Supprimer, mais proprement

La suppression n'accepte que le POST, avec un jeton CSRF propre à chaque événement. Un lien piégé ou un simple GET tombe sur une erreur 405. Un des tests PHPUnit vérifie exactement ça.

src/Controller/Admin/EvenementAdminController.php
#[Route('/{id}/supprimer', name: 'admin_evenement_supprimer',
    requirements: ['id' => '\d+'], methods: ['POST'])]
public function supprimer(Evenement $evenement, Request $requete, ...): Response
{
    // Le jeton CSRF envoyé par le formulaire de la liste doit être le bon
    $jeton = $requete->getPayload()->getString('_token');
    if (!$this->isCsrfTokenValid('supprimer' . $evenement->getId(), $jeton)) {
        return $this->redirectToRoute('admin_evenement_liste');
    }
    $this->supprimerFichierAffiche($evenement->getAffiche());
    $entityManager->remove($evenement);
    $entityManager->flush();
    // ...
}

Ce que fait le site

Côté étudiants

  • →
    À venir et passés Deux listes séparées, triées par date. Le passage de l'une à l'autre est automatique.
  • →
    Une page par événement Affiche, date, lieu, prix adhérent et non adhérent, lien vers la billetterie.
  • →
    Les 3 prochains sur l'accueil Ce qui arrive bientôt, visible dès l'ouverture du site.
  • →
    Candidater au bureau Un formulaire pour proposer sa candidature et réserver un créneau d'entretien.

Côté bureau

  • →
    Connexion admin Tout /admin est fermé par le composant Security. Un compte se crée en ligne de commande.
  • →
    Créer, modifier, supprimer Un formulaire Symfony, et un message de confirmation après chaque action.
  • →
    Brouillon ou publié Une case à cocher décide si le public voit l'événement.
  • →
    Upload de l'affiche Le fichier est renommé, rangé, et l'ancienne affiche est supprimée quand on la remplace.
  • →
    Suivi du recrutement Candidatures et planning des entretiens, dans le module de mon binôme.

Les étapes du projet

1
Fondations
Projet Symfony, entité Evenement, première migration. Mon binôme pose le guide Git et la base SCSS.
2
Pages publiques
Listes à venir et passés, carte réutilisable, page détail, bloc des 3 prochains sur l'accueil.
3
Back-office
Connexion admin, CRUD complet, upload d'affiche, et les tests PHPUnit sur l'accès.
4
Recrutement et prod
Le module de recrutement, la config de production et la mise en ligne, menés par mon binôme.

Ce qui a coincé

Routing
L'ordre des routes
Symfony teste les routes dans l'ordre où il les trouve. Un {id} sans contrainte accepte n'importe quel texte, donc une route peut en avaler une autre.
La page détail est déclarée après la liste, et côté admin {id} n'accepte que des chiffres (\d+). Une URL bizarre donne une 404 propre.
Doctrine
Les événements sur plusieurs jours
Certains événements ont une date de fin, d'autres non. Filtrer sur la date de début aurait rangé dans les archives un événement encore en cours.
COALESCE(e.dateFin, e.dateDebut) dans le QueryBuilder : la date de fin si elle existe, sinon le début.
Sécurité
Les brouillons
Un événement pas encore publié ne doit apparaître nulle part côté public. Ni dans les listes, ni en tapant son URL.
Filtre publie = true dans chaque requête publique, 404 sur la page détail sauf pour ROLE_ADMIN, et un test PHPUnit qui le vérifie.
Fichiers
L'upload des affiches
Deux affiches envoyées avec le même nom pouvaient s'écraser. Et un nom de fichier trafiqué ne doit jamais faire sortir la suppression du dossier des affiches.
Nouveau nom avec le Slugger, uniqid() et la vraie extension. basename() avant de supprimer un fichier.

L'interface en image

Accueil du site du BDE MMI sur ordinateur
Accueil · Desktop
Accueil du site du BDE MMI sur téléphone
Accueil · Mobile
Liste des événements à venir et passés
Événements · Desktop
Page détail d'un événement sur téléphone
Détail · Mobile
Liste des événements dans le back-office
Back-office · Desktop
Formulaire d'édition d'un événement sur téléphone
Formulaire admin · Mobile
Planning des entretiens du module de recrutement
Recrutement · Desktop
Formulaire de candidature sur téléphone
Candidature · Mobile

Affiches de démo : photos Unsplash de Jordon Conner, Luis Villasmil, Ryan Ancill, Sandra Seitamaa et Laura Peruchi. Données fictives.

Envie de voir le code ?

Le code se montre
en entretien

Le dépôt est privé, le site appartient à l'association. Je fais la démo et je montre le code en entretien ou en visio, avec plaisir.

Symfony 7.4 · Doctrine · Twig
Fait à deux, en 3 semaines