1. Ce que fait PHP
Dans le cours précédent, le serveur se comportait comme une bibliothèque : il envoyait les fichiers tels qu’ils étaient sur son disque. Tout le monde recevait la même page. C’est un site statique.
Avec PHP, le serveur ne se contente plus de transmettre : il exécute un programme qui fabrique la page à chaque demande. Le contenu peut alors changer selon l’heure, le visiteur, ce qu’il a saisi dans un formulaire ou ce que contient une base de données. C’est un site dynamique.
| JavaScript (côté client) | PHP (côté serveur) | |
|---|---|---|
| Stocké | Sur le serveur | Sur le serveur |
| Exécuté par | Le navigateur, sur la machine du visiteur | Le serveur, avant l’envoi de la page |
| Visible par le visiteur ? | Oui, en entier (Ctrl+U, F12) | Non : il ne reçoit que le résultat, du HTML |
| Accès à la base de données | Non | Oui |
| Usage typique | Réagir à un clic sans recharger la page | Traiter un formulaire, afficher des données enregistrées, gérer des comptes |
Le parcours d’une requête devient : navigateur → serveur web → PHP (→ base de données) → HTML renvoyé au navigateur. L’animation La requête web dynamique montre ce trajet étape par étape.
2. Préparer l’environnement
Un fichier .php doit être exécuté par un serveur. Ouvert par un double-clic, il s’affiche en texte brut ou se télécharge : le navigateur ne sait pas exécuter PHP.
| Situation | Installation | Lancement |
|---|---|---|
| Poste Windows | Un environnement tout-en-un comme Laragon ou XAMPP (Apache, PHP et MariaDB), ou PHP seul depuis windows.php.net | Selon l’outil, ou php -S (ci-dessous) |
| Poste ou VM Linux (Debian, Ubuntu) | sudo apt install php-cli php-mysql php-mbstring mariadb-server | php -S (ci-dessous) |
| Serveur Debian 13 | Apache et PHP, comme pour MediaWiki | Hôte virtuel Apache (§ 9) |
PHP contient un serveur web intégré, idéal pour développer. Placé dans le dossier du site :
php -v # vérifier que PHP est installé
php -S localhost:8000 -t public # servir le dossier public/ sur http://localhost:8000
Le terminal affiche chaque requête reçue ; Ctrl+C arrête le serveur.
3. Premier script
Créez un dossier essais, puis un fichier bonjour.php :
<?php
$prenom = "Camille"; // une variable commence toujours par $
$heure = date('H:i'); // fonction fournie par PHP : l'heure actuelle
?>
<!DOCTYPE html>
<html lang="fr">
<head><meta charset="UTF-8"><title>Premier script</title></head>
<body>
<h1>Bonjour <?php echo $prenom; ?> !</h1>
<p>Il est <?= $heure ?> sur le serveur.</p>
</body>
</html>
Lancez php -S localhost:8000 dans le dossier essais, puis ouvrez http://localhost:8000/bonjour.php.
- Le code PHP se place entre
<?phpet?>. Tout ce qui est en dehors est du HTML envoyé tel quel. echoécrit dans la page.<?= … ?>est un raccourci pour<?php echo …; ?>, très pratique dans le HTML.- Chaque instruction se termine par un point-virgule.
Affichez maintenant le code source de la page (Ctrl+U) : vous ne trouvez aucune trace de PHP, seulement <h1>Bonjour Camille !</h1>. Le serveur a exécuté le script et n’a envoyé que le résultat.
4. Les bases du langage
4.1 Variables et types
<?php
$prenom = "Camille"; // chaîne de caractères (string)
$age = 19; // entier (int)
$moyenne = 14.5; // nombre à virgule (float)
$inscrit = true; // booléen (bool)
echo "Prénom : " . $prenom; // le point colle (concatène) deux chaînes
echo "Elle a $age ans."; // entre guillemets doubles, la variable est remplacée
echo 'Elle a $age ans.'; // entre apostrophes, rien n'est remplacé
4.2 Conditions
<?php
$heure = (int) date('G'); // heure de 0 à 23, convertie en entier
if ($heure < 12) {
$salutation = 'Bonjour';
} elseif ($heure < 18) {
$salutation = 'Bon après-midi';
} else {
$salutation = 'Bonsoir';
}
Comparaisons : === (égal, et de même type), !== (différent), <, <=, >, >=. Opérateurs logiques : && (et), || (ou), ! (non). Préférez === à ==, qui convertit les types avant de comparer et réserve des surprises.
4.3 Tableaux et boucles
<?php
// Tableau simple (indices 0, 1, 2…)
$interets = ['Les réseaux', 'La cybersécurité', 'Le développement web'];
echo $interets[0]; // Les réseaux
echo count($interets); // 3
foreach ($interets as $interet) { // pour chaque élément du tableau…
echo "<li>$interet</li>";
}
// Tableau associatif (des clés nommées)
$profil = ['nom' => 'Martin', 'prenom' => 'Camille', 'formation' => 'BTS SIO'];
echo $profil['formation']; // BTS SIO
foreach ($profil as $cle => $valeur) {
echo "$cle : $valeur<br>";
}
4.4 Fonctions
<?php
function initiales(string $prenom, string $nom): string
{
return mb_substr($prenom, 0, 1) . '.' . mb_substr($nom, 0, 1) . '.';
}
echo initiales('Camille', 'Martin'); // C.M.
Les types (string, int…) sont facultatifs, mais ils rendent le code plus clair et PHP signale une erreur si l’on passe une valeur du mauvais type. Les fonctions mb_… gèrent correctement les caractères accentués.
5. La carte de visite devient un site PHP
On reprend la carte de visite du cours précédent pour en faire un petit site de deux pages : l’accueil et un livre d’or. Le dossier est organisé ainsi :
mon-site/
├── config.php paramètres de connexion à la base (secrets)
├── inc/
│ ├── fonctions.php fonctions utilitaires
│ ├── header.php début de page commun : <head>, en-tête, menu
│ └── footer.php fin de page commune : pied de page
└── public/ SEUL dossier accessible depuis le navigateur
├── index.php
├── livre-or.php
├── style.css
└── script.js
Le serveur web ne sert que le dossier public/ (option -t public de php -S, ou DocumentRoot d’Apache). Les fichiers de configuration et les morceaux de page restent en dehors : impossible de les demander par une URL.
5.1 Les fonctions communes
inc/fonctions.php
<?php
// Fuseau horaire utilisé par date() (sinon, souvent UTC par défaut)
date_default_timezone_set('Europe/Paris');
// Échappe une valeur avant de l'afficher dans la page (protection contre le XSS)
function e(string $texte): string
{
return htmlspecialchars($texte, ENT_QUOTES, 'UTF-8');
}
// Ouvre la connexion à la base, une seule fois par requête
function connexion(): PDO
{
static $pdo = null;
if ($pdo === null) {
$config = require __DIR__ . '/../config.php';
$pdo = new PDO($config['dsn'], $config['user'], $config['pass'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
}
return $pdo;
}
La fonction e() (pour échapper) sera utilisée pour chaque donnée affichée dans la page : on verra au § 6 pourquoi elle est indispensable. La fonction connexion() servira au § 7.
5.2 En-tête et pied de page communs
Plutôt que de recopier le <head>, l’en-tête et le menu dans chaque page, on les écrit une seule fois. Une modification du menu s’applique alors à tout le site.
inc/header.php
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= e($titre) ?> — Camille Martin</title>
<link rel="stylesheet" href="style.css">
<script src="script.js" defer></script>
</head>
<body>
<header>
<h1>Camille Martin</h1>
<p class="metier">Étudiante en BTS SIO</p>
<nav>
<a href="index.php">Accueil</a>
<a href="livre-or.php">Livre d'or</a>
</nav>
</header>
<main>
inc/footer.php
</main>
<footer>
<p>© <?= date('Y') ?> Camille Martin</p>
</footer>
</body>
</html>
date('Y') affiche l’année en cours : le pied de page n’aura plus jamais besoin d’être corrigé au 1er janvier.
5.3 La page d’accueil
public/index.php
<?php
require __DIR__ . '/../inc/fonctions.php';
$titre = 'Accueil';
$interets = ['Les réseaux', 'La cybersécurité', 'Le développement web'];
$heure = (int) date('G');
$salutation = ($heure < 18) ? 'Bonjour' : 'Bonsoir';
require __DIR__ . '/../inc/header.php';
?>
<section class="carte">
<h2><?= $salutation ?> et bienvenue !</h2>
<p>J'apprends à créer des sites web. Mes centres d'intérêt :</p>
<ul>
<?php foreach ($interets as $interet): ?>
<li><?= e($interet) ?></li>
<?php endforeach; ?>
</ul>
<button id="btn-theme">Changer de thème</button>
<p id="message"></p>
</section>
<?php require __DIR__ . '/../inc/footer.php'; ?>
requireinsère le contenu d’un autre fichier PHP à cet endroit. Si le fichier manque, le script s’arrête (avecinclude, il continuerait avec un simple avertissement).__DIR__désigne le dossier du fichier courant : le chemin reste juste quel que soit l’endroit d’où le script est lancé.- La syntaxe
foreach (…): … endforeach;est une écriture alternative de la boucle, plus lisible au milieu du HTML. style.cssetscript.jssont ceux du cours précédent, copiés danspublic/. Comme le bouton de thème n’existe que sur l’accueil, entourez le code descript.jsd’un testif (bouton) { … }: sinon, la console affichera une erreur sur les autres pages.
Lancez php -S localhost:8000 -t public depuis le dossier mon-site, puis ouvrez http://localhost:8000.
6. Recevoir un formulaire
6.1 GET et POST
Un formulaire envoie ses champs au serveur dans une requête HTTP. PHP les range dans deux tableaux prêts à l’emploi :
| Méthode | Où voyagent les données | Tableau PHP | Usage |
|---|---|---|---|
GET | Dans l’URL : recherche.php?mot=vlan | $_GET | Recherche, filtre, pagination : rien n’est modifié |
POST | Dans le corps de la requête | $_POST | Envoi d’un message, création d’un compte, toute modification |
$_SERVER['REQUEST_METHOD'] indique la méthode utilisée, ce qui permet de savoir si le formulaire vient d’être envoyé.
6.2 Le danger : afficher une saisie telle quelle
Créez public/bonjour.php :
<?php
$prenom = $_GET['prenom'] ?? 'inconnu';
?>
<p>Bonjour <?= $prenom ?> !</p>
Ouvrez http://localhost:8000/bonjour.php?prenom=Camille : tout va bien. Essayez maintenant :
http://localhost:8000/bonjour.php?prenom=<b>Camille</b>
http://localhost:8000/bonjour.php?prenom=<script>alert('XSS')</script>
Le prénom s’affiche en gras, puis une fenêtre s’ouvre : le navigateur a interprété la saisie comme du code. C’est une faille XSS (Cross-Site Scripting). Un attaquant n’a plus qu’à envoyer ce lien piégé à une victime pour exécuter son script dans le navigateur de celle-ci, sur votre site : vol de session, faux formulaire de connexion…
La parade consiste à échapper toute donnée avant de l’afficher : htmlspecialchars() remplace < par <, > par >, les guillemets par " et '. Le navigateur affiche alors les caractères au lieu de les interpréter.
<p>Bonjour <?= e($prenom) ?> !</p>
6.3 Vérifier côté serveur
Les attributs required ou maxlength du formulaire sont vérifiés par le navigateur : c’est du confort, pas une protection. Une requête peut être fabriquée à la main (avec curl, ou en modifiant la page avec F12) : le serveur revérifie tout.
<?php
$nom = trim($_POST['nom'] ?? ''); // ?? : valeur par défaut si le champ est absent
if ($nom === '' || mb_strlen($nom) > 50) {
$erreurs[] = 'Le nom est obligatoire (50 caractères au plus).';
}
$email = trim($_POST['email'] ?? '');
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
$erreurs[] = 'Adresse e-mail invalide.';
}
7. Le livre d’or : enregistrer dans MariaDB
Les messages du livre d’or doivent être conservés et affichés à tous les visiteurs : on les enregistre dans une base de données. Si MariaDB n’est pas encore installé sur votre serveur, voir Installer et sécuriser MariaDB.
7.1 La base, la table et un compte limité
base.sql
-- À exécuter en administrateur : sudo mariadb < base.sql
CREATE DATABASE carte CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE TABLE carte.message (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
nom VARCHAR(50) NOT NULL,
contenu VARCHAR(500) NOT NULL,
cree_le DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Compte utilisé par le site : il peut seulement lire et ajouter des messages
CREATE USER 'carte_web'@'localhost' IDENTIFIED BY 'mot_de_passe_genere';
GRANT SELECT, INSERT ON carte.message TO 'carte_web'@'localhost';
openssl rand -base64 24 # générer un vrai mot de passe, à reporter dans base.sql et config.php
sudo mariadb < base.sql
Le compte carte_web ne peut que lire et ajouter des messages, dans cette seule table. Vérifiez-le :
$ mariadb -u carte_web -p carte -e "DELETE FROM message"
ERROR 1142 (42000) at line 1: DELETE command denied to user 'carte_web'@'localhost' for table `carte`.`message`
C’est le principe du moindre privilège : si une faille du site est un jour exploitée, l’attaquant n’hérite que de ces droits-là. Il ne pourra ni effacer les messages, ni lire les autres bases du serveur.
7.2 Les paramètres de connexion
config.php (à la racine de mon-site, hors de public/)
<?php
// config.php — HORS du dossier public : jamais accessible depuis le navigateur
// Ne jamais publier ce fichier (dépôt Git, capture d'écran, compte rendu).
return [
'dsn' => 'mysql:host=localhost;dbname=carte;charset=utf8mb4',
'user' => 'carte_web',
'pass' => 'mot_de_passe_genere',
];
La connexion elle-même est assurée par la fonction connexion() de fonctions.php (§ 5.1). Elle utilise PDO, l’interface de PHP pour dialoguer avec les bases de données :
| Élément | Rôle |
|---|---|
mysql:host=localhost;dbname=carte | Le DSN : type de base (le pilote mysql sert aussi pour MariaDB), serveur, nom de la base |
charset=utf8mb4 | Encodage complet de l’Unicode (accents, émojis) |
PDO::ERRMODE_EXCEPTION | En cas d’erreur SQL, PDO lève une exception au lieu d’échouer en silence |
PDO::FETCH_ASSOC | Chaque ligne lue est un tableau associatif : $m['nom'], $m['contenu'] |
7.3 La page du livre d’or
public/livre-or.php
<?php
require __DIR__ . '/../inc/fonctions.php';
$titre = "Livre d'or";
$erreurs = [];
$nom = '';
$message = '';
// 1. Le formulaire a-t-il été envoyé ?
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$nom = trim($_POST['nom'] ?? '');
$message = trim($_POST['message'] ?? '');
// 2. Vérifier côté serveur : on ne fait jamais confiance au navigateur
if ($nom === '' || mb_strlen($nom) > 50) {
$erreurs[] = 'Le nom est obligatoire (50 caractères au plus).';
}
if ($message === '' || mb_strlen($message) > 500) {
$erreurs[] = 'Le message est obligatoire (500 caractères au plus).';
}
// 3. Enregistrer avec une requête préparée, puis rediriger
if (empty($erreurs)) {
$requete = connexion()->prepare(
'INSERT INTO message (nom, contenu) VALUES (:nom, :contenu)'
);
$requete->execute(['nom' => $nom, 'contenu' => $message]);
header('Location: livre-or.php?merci=1');
exit;
}
}
// 4. Lire les messages, du plus récent au plus ancien
$messages = connexion()
->query('SELECT nom, contenu, cree_le FROM message ORDER BY cree_le DESC LIMIT 20')
->fetchAll();
require __DIR__ . '/../inc/header.php';
?>
<section class="carte">
<h2>Livre d'or</h2>
<?php if (isset($_GET['merci'])): ?>
<p class="ok">Merci, votre message a été enregistré.</p>
<?php endif; ?>
<?php foreach ($erreurs as $erreur): ?>
<p class="erreur"><?= e($erreur) ?></p>
<?php endforeach; ?>
<form method="post" action="livre-or.php">
<label for="nom">Votre nom</label>
<input type="text" id="nom" name="nom" maxlength="50" required value="<?= e($nom) ?>">
<label for="message">Votre message</label>
<textarea id="message" name="message" maxlength="500" required><?= e($message) ?></textarea>
<button type="submit">Publier</button>
</form>
<?php foreach ($messages as $m): ?>
<article class="avis">
<p><strong><?= e($m['nom']) ?></strong>, le <?= e(date('d/m/Y à H:i', strtotime($m['cree_le']))) ?></p>
<p><?= nl2br(e($m['contenu'])) ?></p>
</article>
<?php endforeach; ?>
</section>
<?php require __DIR__ . '/../inc/footer.php'; ?>
Le script suit quatre étapes :
- Le formulaire a-t-il été envoyé ? Seulement si la requête est un
POST. - Vérification de chaque champ ; les erreurs sont rangées dans un tableau et affichées au-dessus du formulaire, qui garde les valeurs saisies.
- Enregistrement avec une requête préparée (voir § 8), puis redirection vers la même page en
GET. - Lecture des 20 derniers messages, affichés avec
e().nl2br()conserve les retours à la ligne du message.
8. L’injection SQL
8.1 Le code à ne jamais écrire
Imaginons une page de connexion qui construit sa requête en collant la saisie dans le texte SQL :
<?php
// DANGEREUX : la saisie est insérée directement dans la requête
$login = $_POST['login'];
$sql = "SELECT * FROM utilisateur WHERE login = '$login'";
$resultat = connexion()->query($sql);
Si un visiteur saisit x' OR '1'='1 comme identifiant, la requête exécutée devient :
SELECT * FROM utilisateur WHERE login = 'x' OR '1'='1'
La condition est toujours vraie : la requête renvoie tous les utilisateurs. Selon le code et les droits du compte, une injection permet de contourner une authentification, de lire toute la base, voire de la modifier. L’injection figure depuis toujours parmi les failles les plus exploitées du Web (OWASP Top 10).
8.2 La parade : les requêtes préparées
<?php
$requete = connexion()->prepare('SELECT * FROM utilisateur WHERE login = :login');
$requete->execute(['login' => $_POST['login']]);
$utilisateur = $requete->fetch();
La requête est envoyée d’abord, avec un paramètre :login à la place de la donnée ; la valeur est transmise séparément. MariaDB ne peut plus confondre la donnée avec du code SQL : x' OR '1'='1 est cherché tel quel comme identifiant, et rien n’est trouvé.
| Situation | Méthode |
|---|---|
| La requête ne contient aucune donnée variable | query() est acceptable (comme la lecture des messages du livre d’or) |
| La requête contient une seule donnée venant de l’extérieur | prepare() puis execute(), toujours |
9. Mettre en ligne : erreurs et serveur
9.1 Afficher les erreurs… seulement en développement
Pendant le développement, on veut voir toutes les erreurs ; en production, on ne veut jamais les montrer aux visiteurs, car un message d’erreur révèle des chemins de fichiers, des noms de tables ou des requêtes.
Réglage (php.ini) | Développement | Production |
|---|---|---|
display_errors | On | Off |
log_errors | On | On : les erreurs vont dans le journal du serveur |
error_reporting | E_ALL | E_ALL |
Sur Debian, ces réglages se placent dans un fichier dédié, par exemple /etc/php/8.4/apache2/conf.d/99-site.ini, comme pour MediaWiki. Ajoutez aussi expose_php = Off pour que le serveur n’annonce pas sa version de PHP dans l’en-tête X-Powered-By.
9.2 Publier sur un serveur Debian
Sur un serveur Debian 13 avec Apache et PHP (sudo apt install apache2 libapache2-mod-php php-mysql php-mbstring) :
- copiez le dossier dans
/var/www/mon-siteet faites pointer le site sur le dossier public :DocumentRoot /var/www/mon-site/public; - le code appartient à
rootet reste en lecture seule pour Apache ;config.phpest protégé :sudo chown root:www-data /var/www/mon-site/config.phppuissudo chmod 640 /var/www/mon-site/config.php; - publiez en HTTPS : les messages et, plus tard, les mots de passe ne doivent jamais circuler en clair.
La démarche complète (hôte virtuel, certificat, permissions) est détaillée dans Installer un wiki MediaWiki sur Debian 13.
10. Synthèse cybersécurité
| Risque | Bon réflexe |
|---|---|
| Faille XSS | htmlspecialchars() sur toute donnée affichée |
| Injection SQL | Requêtes préparées dès qu’une donnée extérieure entre dans une requête |
| Contrôles contournés | Tout revérifier côté serveur, quelles que soient les vérifications du navigateur |
| Secrets exposés | config.php hors du dossier public, droits 640, jamais dans Git |
| Compte de base trop puissant | Un compte par application, limité aux droits et aux tables nécessaires |
| Messages d’erreur trop bavards | display_errors = Off en production, erreurs dans les journaux |
| Envoi en double | Post / Redirect / Get |
| Interception | HTTPS obligatoire en production |
11. À retenir
🧠 Testez vos connaissances
7 questions. Répondez puis cliquez sur « Corriger ». Rien n’est enregistré : c’est de l’auto-évaluation.
© 2026 Bruno Jousseaume. Ce cours est publié sous licence CC BY-NC-SA 4.0 : vous pouvez le partager et l’adapter en citant l’auteur et un lien vers cette page, sans usage commercial, et en partageant vos adaptations sous la même licence.