Salut, on repart sur un nouveau tuto de dev. Ce tutoriel ne va pas se contenter de montrer une app qui marche. L’objectif sera surtout de voir ensemble l’architecture d’une application PHP assez robuste, comment on la construit, pourquoi chaque brique existe, et comment éviter les pièges classiques .
À la fin, on obtiendra une application complète de vote électronique avec deux univers :
- un portail votant simple et rassurant, qui guide l’utilisateur jusqu’au vote,
- un back-office admin capable de créer et piloter des scrutins, suivre la participation en direct, gérer les utilisateurs et consulter les résultats.
Et surtout, on obtiendra une application structurée, un vrai projet avec séparation des responsabilités, règles métier centralisées, API cohérente, sécurité minimale sérieuse, et scénario de validation complet.
Prérequis
Le tutoriel est accessible si tu sais déjà :
- lancer PHP + MySQL en local (ou via Docker si tu veux),
- écrire des bases en PHP et SQL,
- utiliser Git (clone, commit, push) est optionnel, mais il faudra savoir cloner le projet.

Présentation des fonctionnalités
L’application de Vote Électronique est une plateforme de vote d’entreprise couvrant l’ensemble du cycle de vie d’un scrutin, de sa création jusqu’à l’archivage, en passant par le vote et la publication des résultats.
Elle permet de gérer :
- les notifications in-app et push.
- les comptes utilisateurs et les rôles,
- la création et la publication des élections,
- la gestion des candidats et des critères d’éligibilité,
- le vote sécurisé,
- le suivi de la participation,
- la consultation des résultats,
- l’audit des actions sensibles,
L’application est structurée autour de deux espaces distincts :
Côté votant (portail)
L’utilisateur se connecte, voit les scrutins disponibles, puis vote selon les règles du scrutin. L’application empêche les actions hors cadre : voter si le scrutin est fermé, votes internes vs votes externes voter deux fois, voter sans être éligible, ou soumettre un choix invalide.
Le votant reçoit ensuite une confirmation (une notification in-app) pour être rassuré que son vote a été enregistré. L’utilisateur reçoit également une notification à la création (publication) et clôture d’un scrutin.
Fonctionnalités côté votant
Le portail votant permet :
- l’affichage des scrutins accessibles selon les droits et le type d’audience,
- le vote sur les scrutins ouverts,
- la gestion des différents types de scrutin : SINGLE, MULTI, YESNO, RANKED,
- la consultation des résultats selon la politique de visibilité (notamment après clôture),
- la réception de notifications liées au cycle de vie du scrutin,
- la gestion du profil et des sessions utilisateur.
Règles importantes côté votant :
- Un reçu de vote est généré après validation du dépôt.
- Un votant non éligible ne peut pas voter.
- Un votant externe ne peut pas participer à un scrutin interne (et inversement).
- Un vote anonyme ne peut pas être modifié.
- La possibilité de modifier un vote dépend du paramètre
allow_vote_change.
Côté back-office
L’administrateur peut créer un scrutin, ajouter des candidats, publier le vote, suivre la participation et clôturer. Il peut aussi consulter les résultats, exporter des données (CSV), auditer les actions, et gérer les rôles / permissions. Parmis les utilisateurs autre qu’un simple votant, il y a un scrutateur, un admin et un superadmin.
Fonctionnalités côté SCRUTATEUR
Le rôle SCRUTATEUR est orienté vers la supervision opérationnelle.
Il dispose :
- d’un accès au dashboard de suivi,
- du suivi des scrutins,
- de l’accès à l’émargement et aux statistiques de participation,
- de la consultation des résultats.
Cependant, il n’a:
- Pas de gestion complète des utilisateurs,
- Pas de gestion des groupes ou départements,
- Pas de gestion complète des candidats,
- Pas d’accès aux fonctionnalités sensibles réservées au SUPERADMIN.
Fonctionnalités côté ADMIN
Le rôle ADMIN gère l’opérationnel métier.
Il peut :
- gérer les utilisateurs,
- gérer les départements (groupes),
- gérer les candidats,
- créer et modifier des scrutins,
- publier, clôturer et archiver les scrutins dont il est propriétaire,
- gérer les notifications,
- suivre la participation et les résultats.
Règle centrale pour ADMIN : Un ADMIN ne peut modifier ou supprimer qu’un scrutin dont il est le propriétaire. Le SUPERADMIN conserve un droit global sur l’ensemble des scrutins.
Fonctionnalités côté SUPERADMIN
Le rôle SUPERADMIN dispose de la gouvernance complète du système.
Il bénéficie :
- de tous les droits ADMIN,
- du droit de modification et suppression de tous les scrutins,
- d’un accès complet à l’audit,
- de la gestion des rôles,
- de l’accès au module de sauvegarde,
- de la supervision globale de la sécurité et de l’exploitation.
De façon globale:
L’application intègre un module complet de gestion des élections et des candidats couvrant tout le cycle de vie d’un scrutin.
Le module Élections permet la création via un wizard structuré (informations, règles, éligibilité, résumé), la définition du type de scrutin et de l’audience (INTERNAL, HYBRID, EXTERNAL), la sélection des candidats ainsi que la publication et la clôture manuelles.
Le module Candidats autorise l’ajout d’utilisateurs existants, la création rapide d’utilisateurs externes associés au scrutin, la synchronisation des candidats et le contrôle de cohérence audience/candidat. Un module d’émargement permet de figer la liste des électeurs éligibles et d’ajuster l’éligibilité si nécessaire, tandis qu’un module de participation assure un suivi en temps réel des votants, le calcul du taux de participation et l’export CSV.
Le système inclut des notifications administratives et in-app, des push navigateur et des déclencheurs automatiques (publication, vote enregistré, clôture), ainsi qu’un mécanisme de clôture automatique des scrutins expirés, exécuté via portail, API et script CLI, avec audit et notification.
Structure du projet et stack technique
Stack technique
Le projet repose sur une stack volontairement simple, lisible et robuste, pensée pour être accessible tout en restant professionnelle.
- Backend : PHP 8.x
- Base de données : MySQL / MariaDB
- Accès SQL : PDO avec requêtes préparées (sans ORM)
- Frontend : Bootstrap 5 + AdminLTE
- JavaScript côté administration : JavaScript natif, jQuery et DataTables
- Architecture : Clean Architecture pragmatique (Domain / Application / Infrastructure)
- Exploitation : scripts CLI PHP (migrate, backup, close_elections, reset_demo, user_create)
Le choix technologique est volontairement pragmatique car PHP et MySQL offrent une solution largement compatible, facilement hébergeable et idéale pour un projet concret orienté entreprise. L’utilisation de PDO permet un contrôle fin des requêtes SQL tout en garantissant la sécurité grâce aux requêtes préparées. Bootstrap et AdminLTE assurent une interface rapide à mettre en place, claire et cohérente. Enfin, l’approche Clean Architecture permet de séparer la logique métier des détails techniques, garantissant une meilleure maintenabilité et évolutivité du projet.
Structure du projet
vote/
├── api/
├── app/
├── assets/
├── enterprise/
├── migrations/
├── scripts/
├── src/
├── uploads/
├── views/
├── .env.example
├── .gitignore
├── .htaccess
└── README.md
Voyons maintenant pourquoi ce choix de structure et le rôle de chaque dossier:
api/
Contient les endpoints JSON de l’application enterprise (fichiers ent-*.php).
Exemples :
ent-elections.php: CRUD et actions sur les scrutinsent-users.php: gestion des utilisateurs et des rôlesent-participation.php: suivi de la participation en temps réelent-notifications.php: notifications administrativesent-my-notifications.php: inbox utilisateur
app/
Couche de compatibilité et bootstrap global de l’application.
On y retrouve :
- le chargement de l’environnement et des sessions (
bootstrap.php), - la connexion à la base de données (
db.php), - les helpers d’authentification, CSRF et réponse HTTP,
- la logique de clôture automatique (
election_auto_close.php).
assets/
Ressources statiques du projet :
css/: styles UIjs/: logique front-end (admin.js, live-notifications.js)brand/: éléments graphiques LM-Codescreenshots/: captures utilisées dans le README
enterprise/
Points d’entrée web pour le portail votant et le back-office administrateur.
Exemples :
login.phpelections.phpdashboard.php
migrations/
Versionning du schéma SQL via des fichiers numérotés.
Exemples :
005_enterprise_core.sql: cœur métier011_user_notifications_push.sql: inbox et notifications push
scripts/
Commandes CLI destinées à l’exploitation et à la maintenance.
Exemples :
migrate.phpclose_elections.phpbackup.phpuser_create.php
src/
Cœur applicatif organisé selon une architecture clean :
Domain/: règles métier puresApplication/: cas d’usage et services applicatifsInfrastructure/: adaptateurs techniques (PDO, HTTP, composition)Controller/: contrôleurs MVCView/: rendu côté serveur
views/
Templates PHP pour le portail votant et l’administration.
uploads/
Dossier destiné aux fichiers téléversés (images de candidats, etc.), protégé via des règles .htaccess.
Ce découpage permet de conserver une logique métier testable, de limiter le couplage avec les couches HTTP ou techniques et de faire évoluer l’application sans casser les routes existantes.
Développement étape par étape
Étape 1: Initialiser le projet
L’objectif est de poser une base de travail propre et structurée, avec une arborescence claire, des règles de versioning et un minimum de sécurité dès le départ. Avant d’écrire la moindre ligne de logique métier, on crée le dossier projet ainsi que les fichiers fondamentaux comme .gitignore, .htaccess ou README.md, afin d’établir un cadre professionnel.
cd C:\wamp64\www
mkdir vote
cd vote
Oncrée le projet directement dans le répertoire web local de WAMP, ce qui permet à Apache de servir immédiatement l’application via le navigateur.
Étape 2: Configurer l’environnement
On va centraliser toute la configuration (URL, base de données, timezone, mode application) dans un fichier .env. Ce fichier est chargé dans le bootstrap global afin que toutes les couches de l’application utilisent la même configuration.
require_once __DIR__ . '/env.php';
env_load(__DIR__ . '/../.env');
define('APP_ROOT', realpath(__DIR__ . '/..') ?: dirname(__DIR__));
define('APP_BASE_PATH', rtrim((string)env_get('APP_BASE_PATH', '/vote'), '/'));
function app_url(string $path): string {
$path = '/' . ltrim($path, '/');
return (APP_BASE_PATH === '' ? '' : APP_BASE_PATH) . $path;
}
Ce code va charger la configuration, définit la racine du projet et fournit un helper app_url() permettant de générer des URLs cohérentes dans toute l’application.
Étape 3: Connecter MySQL proprement avec PDO
L’objectif est d’avoir une connexion base de données fiable, sécurisée et uniforme. La connexion est encapsulée dans une factory unique avec des options PDO strictes afin d’éviter tout comportement ambigu.
$dsn = "mysql:host={$dbHost};dbname={$dbName};charset={$dbChar}";
return new PDO($dsn, $dbUser, $dbPass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
Le code ouvre donc une connexion MySQL en mode sécurisé, active les exceptions et utilise les requêtes préparées natives.
Étape 4: Mettre en place les migrations SQL
Ici on va versionner la base de données afin de pouvoir recréer automatiquement le schéma. Le script lit les fichiers SQL dans l’ordre et applique uniquement ceux qui n’ont pas encore été exécutés.
Ce code ouvre une connexion MySQL en mode sécurisé, active les exceptions et utilise les requêtes préparées natives.
Étape 4 — Mettre en place les migrations SQL
L’objectif est de versionner la base de données afin de pouvoir recréer automatiquement le schéma. Le script lit les fichiers SQL dans l’ordre et applique uniquement ceux qui n’ont pas encore été exécutés.
Le système de migartion garantit un ordre stable (001, 002, etc.) et évite de rejouer les mêmes migrations.
Étape 5: Authentification et rôles (RBAC)
L’objectif ici est de contrôler précisément qui peut accéder à quoi dans l’application. Chaque page ou endpoint sensible exige un rôle minimum, sinon l’accès est refusé.
function user_require_role(array $codes, bool $asJson = false): void
{
user_require_login($asJson);
foreach ($codes as $code) {
if (user_has_role((string)$code)) return;
}
if ($asJson) json_error('Acces refuse', 403);
http_response_code(403);
echo 'Acces refuse';
exit;
}
Le code impose l’authentification puis vérifie que l’utilisateur possède l’un des rôles autorisés avant de continuer.
Étape 6: Ajouter CSRF et format JSON standard
Cet ajout a pour but de sécuriser les requêtes d’écriture HTTP et d’unifier les réponses API. Toute action sensible doit vérifier un token CSRF valide lié à la session.
Ce code impose l’authentification puis vérifie que l’utilisateur possède l’un des rôles autorisés avant de continuer.
Étape 6 — Ajouter CSRF et format JSON standard
L’objectif est de sécuriser les requêtes d’écriture HTTP et d’unifier les réponses API. Toute action sensible doit vérifier un token CSRF valide lié à la session.
Le code bloque toute action de modification si le token CSRF est invalide.
Étape 7: Implémenter les règles métier du vote
On va ss’assurer d’empêcher tout vote en dehors des règles définies (scrutin fermé, utilisateur inéligible, etc.). Les règles sont implémentées dans les couches Domain et Application, et non dans les vues.
if (!$this->isOpen($election)) {
throw new \RuntimeException('Vote ferme');
}
if (!$this->isUserEligible($pdo, $electionId, $userId)) {
throw new \RuntimeException('Non eligible');
}
Le code assure de vérifier que le scrutin est ouvert et que l’utilisateur est éligible avant d’autoriser l’enregistrement du vote.
Étape 8: Construire l’API enterprise
L’objectif ici sera d’exposer toutes les opérations métier via des endpoints JSON sécurisés. Chaque endpoint effectue validation, contrôle d’autorisation, logique métier et audit.
function require_manage_election(array $election): void
{
if (user_can_manage_election($election)) return;
json_error('Seul le createur ou un SUPERADMIN peut modifier ce scrutin', 403);
}
Ce code applique la règle propriétaire ou SUPERADMIN pour les actions critiques sur un scrutin.
Étape 9: Construire l’interface admin et portail
L’objectif est de rendre les fonctionnalités métier accessibles selon les rôles concernés. Le back-office admin pilote les données tandis que le portail votant reste simple et orienté action.
setInterval(() => {
list(true).catch(() => {});
}, every * 1000);
Ce code active le rafraîchissement en temps réel de la participation côté interface.
Étape 10: Notifications in-app et push navigateur
Le but ici sera d’informer les utilisateurs aux moments clés (publication, vote enregistré, clôture). Le backend enregistre les notifications en base et le frontend interroge régulièrement l’inbox pour afficher badge et liste.
if (path === basePath || path.startsWith(`${basePath}/`)) {
return normalize(path);
}
return normalize(`${basePath}${path}`);
On normalise les URLs des notifications et évite les erreurs de double préfixe.
Étape 11: Clôture automatique des scrutins
L’objectif est d’éviter qu’un scrutin reste publié après sa date de fin. Une vérification automatique est exécutée côté portail, API et script CLI.
$st = $pdo->prepare("UPDATE elections SET status='CLOSED', closed_at=NOW(), updated_at=NOW() WHERE id=? AND status='PUBLISHED'");
$st->execute([$electionId]);
Ce code ferme le scrutin de manière atomique uniquement s’il est encore en statut PUBLISHED.
Étape 12: Créer les comptes de démonstration
On veut rapidement disposer rapidement de profils prêts pour les tests multi-rôles. Un script CLI permet de créer ou mettre à jour des utilisateurs avec des rôles précis.
php scripts/user_create.php --username=superadmin_lmcode --password=lm-code.be --roles=SUPERADMIN,ADMIN,SCRUTATEUR,VOTER --user_type=INTERNAL
Ce code crée ou met à jour un compte avec hash de mot de passe, type utilisateur et rôles définis.
Étape 13: Valider le scénario complet
L’objectif est de prouver que l’application fonctionne de bout en bout, sans faille métier. On teste la création, la publication, le vote, la participation live, les notifications, la clôture et les résultats.
php scripts/migrate.php status
php scripts/close_elections.php
Ce code permet de vérifier l’état des migrations et de tester le mécanisme de clôture automatique.
Visualisation des interfaces graphiques (UI)
UI Login
L’utilisateur saisit son identifiant et son mot de passe afin d’ouvrir une session sécurisée et accéder à l’application selon ses droits.

UI Dashboard Admin
L’administrateur consulte un tableau de bord synthétique présentant le volume de scrutins, l’activité récente, le niveau de participation et l’état opérationnel global. Il dospose aussi d’un bouton Aller au vote pour se rendre sur l’interface de vote.

UI Élections (liste)
L’administrateur peut rechercher, filtrer et piloter les scrutins existants selon leur statut, leur type et leur audience.

UI Wizard Élection
L’administrateur crée ou modifie un scrutin à travers un processus guidé étape par étape (informations générales, règles, éligibilité, résumé).

UI Candidats
L’administrateur ajoute, modifie, supprime ou associe des candidats à un scrutin, y compris en les liant à des utilisateurs déjà existants.

UI Utilisateurs
L’administrateur gère les comptes utilisateurs (création, modification, statut, rôles, type interne ou externe, département).

UI Départements (Groupes)
L’administrateur crée et maintient les départements utilisés pour structurer l’organisation et définir les règles d’éligibilité.

UI Émargement (Voter Roll)
L’utilisateur de supervision fige et ajuste la liste des électeurs éligibles pour un scrutin donné.

UI Résultats Admin
L’utilisateur de supervision visualise les résultats consolidés et peut exporter les données au format CSV.

UI Notifications Admin
L’administrateur crée des notifications ciblées (audience, niveau, lien associé) et peut les diffuser immédiatement.

UI Portail Votant (Scrutins)
Le votant visualise les scrutins accessibles selon ses droits et peut entrer dans un scrutin ouvert pour voter.

Application du changement de thème

Conclusion
Avec ce projet, nous avons construit bien plus qu’une simple interface de vote: une vraie base applicative métier, structurée, sécurisée et évolutive a été posée..
Le plus important maintenant, tu peux adapter cette base à ton propre contexte (entreprise, association, école), l’améliorer et la faire grandir étape par étape.
Si tu veux aller plus loin, la suite logique est d’ajouter une meilleur gestion des notifications (mails, sms), des tests automatisés, un temps réel via WebSocket, et pleins d’autres idées que tu trouveras sans doutes.
Si tu as aimé ousi tu as des remarques sur cet article, n’hésites pas de laisser un commentaire. N’hésites pas aussi à consulter nos articles ou à nous faire des propositions d’idées d’apps à développer sur LM-Code.