Depuis plusieurs jours, vous progressez pas à pas dans la construction du site Créa-code. Après avoir mis en place une structure MVC solide avec un routeur et un système de layout réutilisable, vous êtes désormais prêts à connecter votre site web à une base de données MySQL en PHP avec PDO. C’est une étape fondamentale dans tout projet web dynamique.
Sans base de données, pas d’utilisateurs, pas de sessions, pas de contenus dynamiques. Tout resterait figé. La base de données, c’est la mémoire longue de votre site. Elle permet de stocker, retrouver, mettre à jour et supprimer des informations.
Dans notre cas, nous allons y sauvegarder les utilisateurs, les exercices, les scores, les messages, les badges, et bien plus encore dans les semaines à venir.
Aujourd’hui, vous allez donc apprendre à connecter proprement et efficacement notre projet Créa-code à une base de données MySQL en utilisant PDO, l’extension recommandée en PHP.
Vous verrez aussi comment se connecter en toute sécurité, sans jamais afficher d’erreurs techniques visibles aux utilisateurs, et avec un système qui garantit une seule connexion unique utilisée partout dans le projet. C’est ce qu’on appelle un singleton.
- 1. Préparation : le fichier config.php
- 2. Mise en place de la classe Database
- 3. Connexion automatique ou à la demande ?
- 4. Mais au fait… c’est quoi un singleton ?
- 5. Vérifier que la connexion fonctionne
- 6. Exemple de requête sur la table users
- 7. Utilisation dans un modèle
- 10. La classe BaseModel
- 9. Bonnes pratiques de sécurité
- 10. Sécuriser le fichier config.php
1. Préparation : le fichier config.php
Commençons par la base : la configuration.
Dans la majorité des projets PHP, les informations sensibles, comme les identifiants de connexion à la base de données, sont stockées dans un fichier à part, que l’on appelle souvent config.php
. Ce fichier se trouve à la racine du projet, c’est-à-dire au même niveau que les dossiers app
, public
, ou encore vendor
si vous utilisez Composer.
Voici le fichier config.php
que nous avons mis en place jusqu’ici :
<?php
// Informations de connexion à la base de données
define('DB_HOST', 'localhost');
define('DB_NAME', 'creacode');
define('DB_USER', 'root');
define('DB_PASS', 'root');
define('BASE_URL', '/public/');
Ce fichier contient des constantes PHP que nous allons utiliser dans toute l’application pour nous connecter à la base de données. Grâce à cela, vous n’avez pas besoin de répéter vos identifiants un peu partout dans le code. Si un jour vous changez d’hébergement ou de mot de passe, il vous suffit de modifier une seule fois ce fichier pour que tout suive automatiquement.
Pensez bien à ne jamais versionner ce fichier sur un dépôt public (comme GitHub) s’il contient des identifiants réels.
En production, on utilisera une autre méthode plus sécurisée, par exemple un fichier .env
, mais pour le moment, en local, cette solution est simple et efficace.
2. Mise en place de la classe Database
Venons-en au cœur du sujet : la connexion à la base de données via une classe Database
.
L’objectif est simple : créer une classe unique, centralisée, que vous pourrez appeler à n’importe quel moment depuis vos contrôleurs ou vos modèles, sans avoir besoin de réécrire tout le code de connexion à chaque fois.
Voici la classe améliorée et sécurisée que nous allons utiliser. Elle sera écrite dans le fichier app/Core/Databse.php
:
<?php
namespace App\Core;
use PDO;
use PDOException;
class Database
{
private static $instance = null;
private $pdo;
private function __construct()
{
try {
$this->pdo = new PDO(
'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4',
DB_USER,
DB_PASS,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);
} catch (PDOException $e) {
error_log('[DATABASE ERROR] ' . $e->getMessage());
exit('Une erreur est survenue. Merci de réessayer plus tard.');
}
}
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new Database();
}
return self::$instance->pdo;
}
}
Prenons maintenant le temps de décortiquer chaque partie de cette classe pour bien comprendre son fonctionnement.
Espace de nom
La ligne namespace App\Core;
indique à PHP que cette classe se trouve dans l’espace App\Core
. C’est important pour l’organisation de votre code. Dans un projet bien structuré, chaque fichier appartient à un namespace précis. Cela évite les conflits de noms, surtout si vous utilisez des bibliothèques externes.
Utilisation des classes PDO
Nous utilisons use PDO;
et use PDOException;
en haut du fichier. Cela permet d’éviter d’avoir à écrire \PDO
ou \PDOException
dans tout le code. C’est plus propre et plus lisible.
Propriété $pdo
La propriété $pdo
contiendra l’objet de connexion à la base de données. C’est grâce à lui que vous allez pouvoir exécuter vos requêtes SQL dans tout le projet.
Constructeur privé
Le constructeur est marqué private
, ce qui signifie qu’on ne peut pas créer un objet Database
directement depuis l’extérieur. Cela fait partie du design pattern Singleton. Le but est d’empêcher la création de multiples connexions. Une seule doit exister.
Tentative de connexion
Dans le try
, on instancie PDO avec les bonnes informations : hôte, nom de base de données, encodage, utilisateur, mot de passe. On précise aussi quelques options très importantes :
PDO::ERRMODE_SILENT
: en production, on n’affiche jamais les erreurs SQL à l’écran. Cela évite de révéler des informations sensibles. En cas de problème, on peut les enregistrer dans un fichier grâce àerror_log()
.PDO::ATTR_DEFAULT_FETCH_MODE
: ce mode permet de récupérer les résultats sous forme de tableaux associatifs. C’est plus pratique.PDO::ATTR_EMULATE_PREPARES
: on le désactive pour éviter les injections SQL dans certains cas avancés.
Gestion des erreurs
En cas de problème de connexion, on attrape l’exception avec catch (PDOException $e)
. Le message d’erreur est envoyé dans les logs avec error_log()
, mais rien n’est affiché à l’utilisateur. C’est une bonne pratique de sécurité. Ensuite, on arrête immédiatement le script avec exit()
.
En savoir plus sur La gestion des erreurs en PHP.
Méthode getInstance()
C’est cette méthode que vous allez appeler depuis n’importe quelle page pour récupérer l’objet PDO. Si la connexion n’existe pas encore, elle est créée. Sinon, on réutilise celle qui existe. Grâce à ça, une seule connexion est ouverte par chargement de page, ce qui évite de gaspiller des ressources.
3. Connexion automatique ou à la demande ?
Maintenant que notre classe Database
est prête, une question importante se pose : devons-nous établir la connexion à chaque fois que le site charge une page, ou uniquement lorsqu’une requête vers la base est vraiment nécessaire ?
La réponse est simple : il vaut mieux se connecter à la demande.
Et c’est exactement ce que permet notre classe grâce au singleton. Vous n’avez pas besoin de lancer la connexion dans public/index.php
, ce serait inutile si la page ne consulte pas la base. Au lieu de cela, vous allez appeler Database::getInstance()
uniquement dans les parties de votre code qui en ont besoin, par exemple dans vos modèles.
Ce fonctionnement est intelligent, économe en ressources et facile à maintenir.
4. Mais au fait… c’est quoi un singleton ?
Le mot peut paraître complexe, mais le concept est simple.
Un singleton, c’est un modèle de conception (un « design pattern« ) utilisé en programmation orientée objet. Il sert à faire en sorte qu’il n’y ait qu’un seul et unique objet d’une certaine classe pendant toute l’exécution du programme.
Imaginez que vous êtes dans une maison. Vous avez besoin d’eau, donc vous ouvrez le robinet. Est-ce que vous créez un nouveau château d’eau à chaque fois que vous avez besoin d’un verre d’eau ? Non. Il existe une seule source, et tout le monde y puise. C’est exactement ça un singleton.
Dans notre cas, ce qu’on ne veut surtout pas, c’est ouvrir 10 connexions différentes à la base de données à chaque page chargée. Ce serait comme créer 10 tunnels vers le château d’eau pour un seul verre. Inutile et dangereux.
La classe Database
que nous avons créée vous garantit cela :
- Une seule connexion est créée dans toute la page
- Si vous l’appelez plusieurs fois, c’est toujours la même
- C’est plus rapide, plus stable et plus sûr
Concrètement, la propriété statique $instance
de la classe joue le rôle de mémoire. Tant qu’elle est null
, on crée une nouvelle connexion. Ensuite, on réutilise la même.
5. Vérifier que la connexion fonctionne
Avant d’aller plus loin, il faut s’assurer que la classe fonctionne réellement. Le test le plus simple, c’est d’écrire une toute petite ligne de code dans un contrôleur ou même dans une page temporaire, et d’essayer d’établir la connexion.
Voici un exemple que vous pouvez mettre dans une page de test, par exemple dans un fichier test-bdd.php
(hors production, bien sûr) :
<?php
require_once __DIR__ . '/../config.php';
require_once __DIR__ . '/../autoload.php';
use App\Core\Database;
$pdo = Database::getInstance();
if ($pdo) {
echo "Connexion réussie à la base de données.";
} else {
echo "Échec de la connexion.";
}
Si tout est bien configuré, vous devriez voir le message « Connexion réussie à la base de données. » s’afficher dans votre navigateur.
Sinon, vérifiez :
- Vos identifiants dans
config.php
- Que votre base
creacode
(ou plutôt le nom que vous lui avez donné) existe bien - Que le serveur MySQL est bien démarré
Pensez aussi à bien supprimer cette page de test ensuite, pour des raisons de sécurité.
6. Exemple de requête sur la table users
Maintenant que vous êtes connectés, voyons comment utiliser cette connexion pour lire des données depuis la base. Supposons que vous avez une table nommée users
avec quelques utilisateurs inscrits.
Voici une requête simple pour récupérer les pseudos des utilisateurs depuis la table :
<?php
use App\Core\Database;
$pdo = Database::getInstance();
$stmt = $pdo->query("SELECT pseudo FROM users");
$utilisateurs = $stmt->fetchAll();
foreach ($utilisateurs as $utilisateur) {
echo $utilisateur['pseudo'] . '<br>';
}
Expliquons en détail :
Database::getInstance()
retourne notre objet PDO.- On utilise la méthode
query()
pour envoyer une requête SQL simple. fetchAll()
permet de récupérer tous les résultats sous forme de tableau.- On boucle ensuite pour afficher chaque pseudo.
Cela vous permet très facilement d’interagir avec la base de données. Si vous utilisez des données externes (comme celles envoyées par un formulaire), on préférera la méthode prepare()
avec bindParam()
pour sécuriser la requête, mais ici, pour de la lecture simple, query()
suffit. Mais dans une architecture MVC, il faut passer par un Model, nous séparons les logiques de traitement de données et d’affichage de données.
7. Utilisation dans un modèle
Dans le cadre de votre architecture MVC, il est important de respecter la séparation des responsabilités. Cela signifie que ce genre de requête ne doit pas se retrouver dans un contrôleur ou une vue. Elle doit se trouver dans un modèle.

Des formations informatique pour tous !
Débutant ou curieux ? Apprenez le développement web, le référencement, le webmarketing, la bureautique, à maîtriser vos appareils Apple et bien plus encore…
Formateur indépendant, professionnel du web depuis 2006, je vous accompagne pas à pas et en cours particulier, que vous soyez débutant ou que vous souhaitiez progresser. En visio, à votre rythme, et toujours avec pédagogie.
Découvrez mes formations Qui suis-je ?Créons donc un premier modèle nommé User.php
dans app/Models
:
<?php
namespace App\Models;
use App\Core\Database;
class User extends Model
{
public static function count(): int
{
$pdo = Database::getInstance();
$stmt = $pdo->query("SELECT COUNT(*) FROM users");
return (int) $stmt->fetchColumn();
}
}
Ici, vous avez la méthodes utiles count()
qui récupère le nombre d’utilisateurs.
Cette méthode peut être appelé depuis vos contrôleurs comme ceci. Ici nous l’appelons depuis le contrôleur app/Controllers/HomeController.php :
<?php
namespace App\Controllers;
use App\Core\Controller;
use App\Core\View;
use App\Models\User;
class HomeController extends Controller
{
public function index(): void
{
$title = "Accueil - Créa-code";
$nombreInscrits = User::count();
View::render('home/index', compact('title', 'nombreInscrits'));
}
}
Et nous affichons le résultat dans la vue app/Views/home/index.php. Pour l’instant, nous n’avons aucun utilisateur inscrit, donc cela affichera 0 :
<h1>Page d’accueil</h1>
<p>Bienvenue sur votre plateforme d'exercices de code en ligne.</p>
<p>Il y a <?= $nombreInscrits; ?> utilisateur(s) inscrit(s)</p>
10. La classe BaseModel
Nous allons coder la class app/Models/BaseModel.php :
<?php
namespace App\Model;
use PDO;
abstract class BaseModel
{
protected PDO $db;
protected string $table;
public function __construct(PDO $db)
{
$this->db = $db;
}
public function findAll(): array
{
$stmt = $this->db->query("SELECT * FROM {$this->table}");
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function findById(int $id): ?array
{
$stmt = $this->db->prepare("SELECT * FROM {$this->table} WHERE id = :id");
$stmt->execute(['id' => $id]);
return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
}
public function delete(int $id): bool
{
$stmt = $this->db->prepare("DELETE FROM {$this->table} WHERE id = :id");
return $stmt->execute(['id' => $id]);
}
}
C’est une classe que l’on n’utilisera pas directement, mais qui va servir de base à tous les autres modèles comme User
, ExerciceModel
, etc. C’est un peu le squelette sur lequel on construit tout ce qui touche aux données.
À quoi sert la classe BaseModel ?
Quand on crée un site comme code.crea-troyes.fr, on a besoin de lire, insérer, mettre à jour ou supprimer des données dans une base MySQL. Ces actions, appelées CRUD (Create, Read, Update, Delete), sont souvent répétitives. Par exemple, pour afficher tous les utilisateurs ou récupérer un exercice par son ID, on répète souvent le même type de requêtes.
Plutôt que de réécrire ces instructions SQL dans chaque modèle, on regroupe ce qui est commun dans une seule classe : BaseModel.
Pourquoi est-elle abstraite ?
La classe BaseModel
est abstraite, ce qui signifie que vous ne pouvez pas l’utiliser telle quelle directement. Elle a été conçue pour être étendue par d’autres classes plus précises.
Cela permet d’imposer une structure commune à tous les modèles, tout en laissant la liberté d’ajouter des méthodes spécifiques dans chaque modèle enfant.
Par exemple, le modèle User
pourra hériter de BaseModel
, profiter des méthodes findAll()
, findById()
ou delete()
, et ajouter sa propre méthode findByEmail()
, par exemple, pour retrouver un utilisateur à partir de son adresse e-mail.
L’avantage est double :
- Moins de répétition dans le code
- Plus de cohérence entre les différents modèles
Comment fonctionne la classe ?
Regardons-la de plus près, avec des mots simples.
Le constructeur
La méthode __construct(PDO $db)
est appelée automatiquement quand vous créez un objet basé sur un modèle. Elle permet d’enregistrer la connexion à la base de données (l’objet PDO) pour que les autres méthodes puissent s’en servir.
La propriété $table
Chaque modèle enfant doit définir le nom de la table dans laquelle il travaille. Par exemple, User
indiquera $table = 'users'
. Grâce à cette propriété, la classe BaseModel
peut générer des requêtes automatiquement sur la bonne table.
La méthode findAll()
Elle exécute une requête SELECT * FROM ma_table
et retourne tous les résultats sous forme de tableau associatif. Très pratique pour afficher une liste complète (ex : tous les utilisateurs ou tous les exercices disponibles).
La méthode findById($id)
Elle prépare une requête SQL avec un paramètre :id
pour éviter les injections SQL, et retourne une seule ligne correspondant à l’ID demandé, ou null
si rien n’est trouvé.
La méthode delete($id)
Elle supprime la ligne correspondant à un identifiant précis. Là encore, la requête est préparée proprement pour garantir la sécurité.
Comment l’utiliser dans notre site web ?
Prenons notre exemple précédent. Pour afficher le nombre des utilisateurs, nous allons d’abord ajouter la méthode count()
à notre class BaseModel
puis réécrire le fichier User.php
dans le dossier app/Model
.
<?php
namespace App\Models;
use PDO;
abstract class BaseModel
{
protected PDO $db;
protected string $table;
public function __construct(PDO $db)
{
$this->db = $db;
}
public function findAll(): array
{
$stmt = $this->db->query("SELECT * FROM {$this->table}");
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function findById(int $id): ?array
{
$stmt = $this->db->prepare("SELECT * FROM {$this->table} WHERE id = :id");
$stmt->execute(['id' => $id]);
return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
}
public function delete(int $id): bool
{
$stmt = $this->db->prepare("DELETE FROM {$this->table} WHERE id = :id");
return $stmt->execute(['id' => $id]);
}
public function count(): int
{
$stmt = $this->db->query("SELECT COUNT(*) FROM {$this->table}");
return (int) $stmt->fetchColumn();
}
}
Le modèle User.php
va hériter de BaseModel
, comme ceci :
<?php
namespace App\Models;
class User extends BaseModel
{
protected string $table = 'users';
}
Ensuite, dans notre contrôleur HomeController.php, nous pouvons utiliser ce modèle de la manière suivante :
<?php
namespace App\Controllers;
use App\Core\Controller;
use App\Core\View;
use App\Core\Database;
use App\Models\User;
class HomeController extends Controller
{
public function index(): void
{
$title = "Accueil - Créa-code";
$user = new User(Database::getInstance());
$nombreInscrits = $user->count();
//$nombreInscrits = User::count();
View::render('home/index', compact('title', 'nombreInscrits'));
}
}
Grâce à cette structure, vous n’avez plus besoin de réécrire des requêtes pour chaque action de base, car elles sont centralisées dans BaseModel
.
Quels sont les avantages concrets ?
Ce système vous apporte :
- Une organisation claire du code
- Un gain de temps important pour toutes les opérations classiques
- Une meilleure sécurité, grâce à l’utilisation de requêtes préparées
- Une base évolutive, que vous pouvez enrichir au fil du projet
Par exemple, nous pourrons plus tard ajouter des méthodes dans BaseModel
comme update()
, ou une gestion automatique des champs created_at
/ updated_at
.
Maintenant que notre modèle de base est prêt, nous pouvons continuer à créer les modèles spécifiques pour gérer les exercices, les quizz, les utilisateurs, les statistiques, etc. Chaque nouveau modèle héritera de BaseModel
, et pourra ajouter ses propres méthodes si nécessaire.
La classe
BaseModel
est une fondation très utile dans un site comme Créa-code. Elle vous permet de gérer facilement les requêtes les plus fréquentes, tout en gardant une structure souple, claire et sécurisée. Grâce à cette base abstraite, vous codez plus vite, plus proprement, et avec moins d’erreurs.
Nous verrons bientôt comment ajouter une méthode create()
pour insérer de nouveaux utilisateurs dans la base — et même gérer les erreurs en douceur si un e-mail est déjà utilisé.
9. Bonnes pratiques de sécurité
Avant de conclure, il est bon de rappeler quelques bonnes pratiques :
- Ne jamais afficher d’erreur SQL ou d’exception PDO à l’écran.
- Toujours utiliser des requêtes préparées (
prepare()
etexecute()
) dès que vous utilisez des données dynamiques (venant d’un formulaire, d’un lien, etc.). - Protéger votre fichier
config.php
pour qu’il ne soit jamais accessible publiquement. - Ne pas créer plusieurs connexions à la base sur la même page : utilisez le singleton comme on l’a fait.
- En cas d’erreur grave, logguez-la avec
error_log()
et affichez un message générique.
10. Sécuriser le fichier config.php
Le fichier config.php
est placé à la racine de notre site PHP et contient des informations sensibles, notamment les identifiants de connexion à la base de données. Voici comment le sécuriser efficacement, tout en continuant à l’utiliser proprement dans notre architecture MVC :
Empêcher l’accès direct via le navigateur
Même si notre fichier config.php
est en .php
et ne s’affichera pas directement dans le navigateur, il est fortement conseillé de bloquer l’accès HTTP à ce fichier. Vous pouvez le faire via .htaccess
si vous êtes sur Apache.
Ajoutez à la racine du site (ou dans le dossier contenant config.php
) un fichier .htaccess
contenant ceci :
<Files "config.php">
Order allow,deny
Deny from all
</Files>
Cela empêchera toute tentative d’accès direct à config.php
via l’URL.
N’affichez jamais les erreurs PHP en production
Dans config.php
, vous définissez vos constantes DB_USER
, DB_PASS
… En cas d’erreur, PHP pourrait révéler des chemins de fichiers ou du code. Pour éviter cela :
Ajoutez ceci dans config.php
ou dans un fichier init.php
appelé très tôt :
ini_set('display_errors', 0);
ini_set('display_startup_errors', 0);
error_reporting(E_ALL);
Et configurez un log d’erreurs vers un fichier local plutôt que d’afficher l’erreur à l’écran.
Protégez votre serveur avec les bons droits d’accès
Vérifiez que le fichier config.php
:
- n’est pas modifiable par n’importe quel utilisateur :
chmod 640 config.php
- n’est pas accessible depuis l’extérieur (voir point
.htaccess
) - n’est pas versionné : ajoutez-le au
.gitignore
si vous utilisez Git.

La base de données est le cœur battant de votre application web. Grâce à cette classe Database
, vous avez mis en place un système de connexion simple, sécurisé, centralisé et performant. Elle respecte les bonnes pratiques modernes, utilise PDO, et s’intègre parfaitement dans votre structure MVC.
Vous êtes désormais prêts à enregistrer des utilisateurs, valider leur mot de passe, gérer leurs scores et débloquer des badges. Tout commence ici : la connexion à la base est désormais votre alliée.
Dans les jours suivants, vous apprendrez à :
- Créer des modèles complets pour chaque table de votre base
- Sécuriser les données envoyées par formulaire
- Gérer les erreurs et les validations
- Structurer les interactions entre contrôleurs, modèles et vues
Bravo pour cette étape majeure franchie. Nous venons de poser l’un des fondements les plus importants de votre projet Créa-code.
La suite demain …