Tutoriels gratuits et ouverts à toutes et à tous · mis à jour régulièrementNouveautésGlossaireÀ propos

Série « Parcours PHP » · partie 1 / 2

PHP 1 : de la page statique au site dynamique (formulaire et livre d’or)

Suite de l’introduction au Web : on transforme la carte de visite HTML en site PHP. Variables, tableaux, boucles, inclusions, traitement d’un formulaire, puis un livre d’or enregistré dans MariaDB avec PDO, en se protégeant du XSS et de l’injection SQL.

Développement✎ B. Jousseaume📅 22 octobre 2026⏱ 6 h📶 Débutant

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 serveurSur le serveur
Exécuté parLe navigateur, sur la machine du visiteurLe 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éesNonOui
Usage typiqueRéagir à un clic sans recharger la pageTraiter 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.

SituationInstallationLancement
Poste WindowsUn environnement tout-en-un comme Laragon ou XAMPP (Apache, PHP et MariaDB), ou PHP seul depuis windows.php.netSelon l’outil, ou php -S (ci-dessous)
Poste ou VM Linux (Debian, Ubuntu)sudo apt install php-cli php-mysql php-mbstring mariadb-serverphp -S (ci-dessous)
Serveur Debian 13Apache et PHP, comme pour MediaWikiHô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 <?php et ?>. 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>&copy; <?= 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'; ?>
  • require insère le contenu d’un autre fichier PHP à cet endroit. Si le fichier manque, le script s’arrête (avec include, 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.css et script.js sont ceux du cours précédent, copiés dans public/. Comme le bouton de thème n’existe que sur l’accueil, entourez le code de script.js d’un test if (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éthodeOù voyagent les donnéesTableau PHPUsage
GETDans l’URL : recherche.php?mot=vlan$_GETRecherche, filtre, pagination : rien n’est modifié
POSTDans le corps de la requête$_POSTEnvoi 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 &lt;, > par &gt;, les guillemets par &quot; et &#039;. 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émentRôle
mysql:host=localhost;dbname=carteLe DSN : type de base (le pilote mysql sert aussi pour MariaDB), serveur, nom de la base
charset=utf8mb4Encodage complet de l’Unicode (accents, émojis)
PDO::ERRMODE_EXCEPTIONEn cas d’erreur SQL, PDO lève une exception au lieu d’échouer en silence
PDO::FETCH_ASSOCChaque 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 :

  1. Le formulaire a-t-il été envoyé ? Seulement si la requête est un POST.
  2. 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.
  3. Enregistrement avec une requête préparée (voir § 8), puis redirection vers la même page en GET.
  4. 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é.

SituationMéthode
La requête ne contient aucune donnée variablequery() est acceptable (comme la lecture des messages du livre d’or)
La requête contient une seule donnée venant de l’extérieurprepare() 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éveloppementProduction
display_errorsOnOff
log_errorsOnOn : les erreurs vont dans le journal du serveur
error_reportingE_ALLE_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-site et faites pointer le site sur le dossier public : DocumentRoot /var/www/mon-site/public ;
  • le code appartient à root et reste en lecture seule pour Apache ; config.php est protégé : sudo chown root:www-data /var/www/mon-site/config.php puis sudo 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é

RisqueBon réflexe
Faille XSShtmlspecialchars() sur toute donnée affichée
Injection SQLRequêtes préparées dès qu’une donnée extérieure entre dans une requête
Contrôles contournésTout revérifier côté serveur, quelles que soient les vérifications du navigateur
Secrets exposésconfig.php hors du dossier public, droits 640, jamais dans Git
Compte de base trop puissantUn compte par application, limité aux droits et aux tables nécessaires
Messages d’erreur trop bavardsdisplay_errors = Off en production, erreurs dans les journaux
Envoi en doublePost / Redirect / Get
InterceptionHTTPS 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.

1. Où s’exécute le code PHP d’une page ?

💬 Le serveur exécute le PHP et n’envoie au navigateur que le résultat : du HTML. Le code PHP n’est jamais visible avec Ctrl+U.

2. Que se passe-t-il si l’on ouvre un fichier .php par un double-clic, sans serveur ?

💬 Le navigateur ne sait pas exécuter PHP : il faut passer par un serveur web (php -S, Apache…) qui l’interprète.

3. Quelle variable contient les champs d’un formulaire envoyé avec method="post" ?

💬 $_GET contient les paramètres de l’URL, $_POST les champs envoyés dans le corps de la requête POST.

4. Pourquoi passe-t-on toute donnée affichée dans la page par htmlspecialchars() ?

💬 Sans échappement, un visiteur peut injecter une balise <script> qui s’exécutera chez tous les autres visiteurs.

5. Quelle technique protège contre l’injection SQL ?

💬 Avec prepare() et execute(), la base ne confond jamais les données avec le code SQL.

6. Le compte MariaDB du livre d’or n’a que les droits SELECT et INSERT. Quel est l’intérêt ?

💬 C’est le principe du moindre privilège : il limite les dégâts d’une faille, sans dispenser de la corriger.

7. Pourquoi rediriger le navigateur (header('Location: …')) après l’enregistrement d’un message ?

💬 C’est le motif Post / Redirect / Get : après un POST réussi, on redirige vers une page affichée en GET.

© 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.

Pour aller plus loin

Introduction au Web : HTML, CSS et JavaScript

Comment une page web arrive sur votre écran (client, DNS, serveur, HTTP), où sont stockés et qui exécute le HTML, le CSS et le JavaScript, puis une première page complète à construire soi-même, avec les réflexes de cybersécurité.

Débutant⏱ 4 h · 22 octobre 2026