Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 423 minIntermédiaire

Formulaire PHP : $_GET, $_POST et validation

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • recevoir les données d'un formulaire avec $_POST et $_GET ;
  • choisir entre GET et POST ;
  • valider chaque donnée côté serveur et réafficher les erreurs ;
  • rediriger après un envoi avec le motif Post/Redirect/Get.

3 exercices corrigés1 schéma

Un formulaire HTML envoie les saisies de l'utilisateur au serveur ; PHP les reçoit dans les tableaux superglobaux $_POST (méthode POST) et $_GET (paramètres de l'URL). Ce chapitre montre comment traiter un formulaire du début à la fin : recevoir les données, les valider côté serveur, réafficher le formulaire avec les erreurs, puis rediriger après un enregistrement réussi. La partie HTML du formulaire (balises form, input, label) est expliquée dans le cours HTML et CSS.

Les superglobales

PHP remplit automatiquement quelques tableaux, accessibles partout dans le code : on les appelle les superglobales.

SuperglobaleContenuExemple
$_GETLes paramètres de l'URL (après le ?)materiel.php?id=4 donne $_GET['id'] === '4'
$_POSTLes champs d'un formulaire envoyé en POST$_POST['libelle']
$_FILESLes fichiers envoyés par un formulairechapitre 13
$_COOKIELes cookies envoyés par le navigateurchapitre 6
$_SESSIONLes données de session de l'utilisateurchapitre 6
$_SERVERLes informations sur la requête et le serveur$_SERVER['REQUEST_METHOD'] vaut 'GET' ou 'POST'
$_ENVLes variables d'environnement (souvent vide, selon la configuration)préfère getenv() (chapitre 15)
$_REQUESTUn mélange de $_GET, $_POST et $_COOKIEà éviter : tu ne sais plus d'où vient la valeur

GET ou POST : quelle méthode choisir

GETPOST
Où vont les donnéesDans l'URL : liste.php?categorie=portable&page=2Dans le corps de la requête
Visible dans l'historique, les journaux du serveur, les favorisOuiNon
UsageLire : recherche, filtre, pagination, page de détailModifier : créer, modifier, supprimer, se connecter
Rejouer la requête (bouton Actualiser)Sans conséquenceLe navigateur demande s'il faut renvoyer le formulaire

La règle : une requête GET ne doit jamais modifier de données. Un lien « Supprimer » en GET (supprimer.php?id=4) est une erreur classique : un robot d'indexation ou une page malveillante peut le déclencher, et le jeton CSRF ne s'applique qu'aux formulaires POST. Le cours API REST détaille les méthodes HTTP et la notion de méthode sûre.

Recevoir un formulaire

Le gestionnaire de Prêtmat ajoute un matériel avec ce formulaire (version simplifiée, sans la mise en forme) :

html
<form method="post" action="/ajouter.php">
  <label for="reference">Référence</label>
  <input id="reference" name="reference" required pattern="[A-Za-z]{2,3}-[0-9]{4}" placeholder="PC-0101">

  <label for="libelle">Libellé</label>
  <input id="libelle" name="libelle" required maxlength="100">

  <label for="categorie">Catégorie</label>
  <select id="categorie" name="categorie" required>
    <option value="">-- choisir --</option>
    <option value="portable">Ordinateur portable</option>
    <option value="videoprojecteur">Vidéoprojecteur</option>
    <option value="tablette">Tablette</option>
    <option value="camera">Caméra</option>
    <option value="autre">Autre</option>
  </select>

  <label for="date_achat">Date d'achat</label>
  <input id="date_achat" name="date_achat" type="date">

  <label for="valeur">Valeur (€)</label>
  <input id="valeur" name="valeur" inputmode="decimal">

  <button>Enregistrer</button>
</form>

Chaque champ est envoyé sous le nom de son attribut name : à la soumission, $_POST contient ['reference' => 'PC-0103', 'libelle' => '…', 'categorie' => 'portable', 'date_achat' => '2026-10-15', 'valeur' => '1180,00']. Toutes les valeurs sont des chaînes, même la valeur et la date. Un champ vide arrive sous forme de chaîne vide '' ; une case à cocher non cochée n'arrive pas du tout.

La page qui affiche le formulaire est souvent la même que celle qui le traite. Elle distingue les deux cas avec la méthode de la requête :

php
<?php

declare(strict_types=1);

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // Le formulaire vient d'être envoyé : valider, puis enregistrer ou réafficher les erreurs
} else {
    // Premier affichage : formulaire vide
}

Valider les données côté serveur

Les attributs required, pattern ou maxlength du formulaire aident l'utilisateur, mais ils ne protègent rien : ils se contournent en deux clics (les limites de la validation côté client). La validation côté serveur est obligatoire, à chaque requête, même si le formulaire a déjà tout vérifié.

Valider, c'est vérifier que chaque donnée a le type, le format et la plage attendus, et refuser la requête sinon. Ce n'est pas la même chose que nettoyer : retirer les caractères gênants d'une saisie et l'accepter quand même transforme une erreur en donnée fausse. Les fonctions filter_var() et filter_input() font l'essentiel du travail :

FiltreAccepteRenvoie en cas d'échec
FILTER_VALIDATE_INT'42', ' 42 ' ; options min_range, max_rangefalse ('42abc', '4.2')
FILTER_VALIDATE_FLOAT'1180.50' ; option decimal pour accepter la virgulefalse
FILTER_VALIDATE_EMAILune adresse syntaxiquement validefalse
FILTER_VALIDATE_BOOL'1', 'on', 'yes', 'true' et leurs contrairesfalse, ou null avec l'option FILTER_NULL_ON_FAILURE
FILTER_VALIDATE_URLune URL complètefalse
php
<?php

$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
// int si valide ; false si invalide ; null si le paramètre est absent

$valeur = filter_var('1180,50', FILTER_VALIDATE_FLOAT, ['options' => ['decimal' => ',']]);
// float(1180.5)

Pour le reste (une liste de valeurs autorisées, une date, un format de référence), tu écris les contrôles toi-même. Voici la fonction de validation du matériel de Prêtmat, rangée dans src/validation.php et réutilisée au chapitre 10 :

php
<?php

declare(strict_types=1);

const CATEGORIES = ['portable', 'videoprojecteur', 'tablette', 'camera', 'autre'];

/** Lit un champ texte : une chaîne sans espaces autour, ou '' s'il est absent ou n'est pas une chaîne. */
function texte(array $source, string $cle): string
{
    $valeur = $source[$cle] ?? '';
    return is_string($valeur) ? trim($valeur) : '';
}

/**
 * Valide la saisie d'un matériel.
 * Renvoie les valeurs propres (typées) et les messages d'erreur, indexés par nom de champ.
 */
function validerMateriel(array $saisie): array
{
    $erreurs = [];

    $reference = mb_strtoupper(texte($saisie, 'reference'));
    if (preg_match('/^[A-Z]{2,3}-\d{4}$/', $reference) !== 1) {
        $erreurs['reference'] = 'Format attendu : 2 ou 3 lettres, un tiret et 4 chiffres (PC-0101).';
    }

    $libelle = texte($saisie, 'libelle');
    if ($libelle === '' || mb_strlen($libelle) > 100) {
        $erreurs['libelle'] = 'Le libellé est obligatoire (100 caractères au plus).';
    }

    $categorie = texte($saisie, 'categorie');
    if (!in_array($categorie, CATEGORIES, true)) {
        $erreurs['categorie'] = 'Choisis une catégorie dans la liste.';
    }

    $dateAchat = texte($saisie, 'date_achat');
    if ($dateAchat !== '') {
        $date = DateTimeImmutable::createFromFormat('!Y-m-d', $dateAchat);
        if ($date === false || $date->format('Y-m-d') !== $dateAchat) {
            $erreurs['date_achat'] = 'Date invalide.';
        } elseif ($date > new DateTimeImmutable('today')) {
            $erreurs['date_achat'] = 'La date d\'achat ne peut pas être dans le futur.';
        }
    }

    $valeur = null;
    $valeurSaisie = texte($saisie, 'valeur');
    if ($valeurSaisie !== '') {
        $valeur = filter_var(str_replace(',', '.', $valeurSaisie), FILTER_VALIDATE_FLOAT,
            ['options' => ['min_range' => 0, 'max_range' => 99999.99]]);
        if ($valeur === false) {
            $erreurs['valeur'] = 'La valeur doit être un nombre entre 0 et 99 999,99.';
            $valeur = null;
        }
    }

    return [
        'valeurs' => [
            'reference' => $reference,
            'libelle' => $libelle,
            'categorie' => $categorie,
            'date_achat' => $dateAchat === '' ? null : $dateAchat,
            'valeur' => $valeur,
        ],
        'erreurs' => $erreurs,
    ];
}

Quelques choix à comprendre :

  • texte() vérifie que la valeur est une chaîne. Un client peut envoyer libelle[]=x : $_POST['libelle'] est alors un tableau, et trim() lèverait une TypeError. Le code ne doit jamais planter sur une saisie inattendue.
  • La liste blanche (in_array(…, true)) est la seule validation sûre pour une valeur qui doit appartenir à un ensemble connu : on n'essaie pas de deviner ce qui est interdit, on n'accepte que ce qui est prévu.
  • La date est relue après analyse. createFromFormat() accepte le 30 février et le transforme en 2 mars ; comparer avec la chaîne d'origine détecte ce cas.
  • Les messages d'erreur sont indexés par champ, pour les afficher à côté du bon champ.
  • La fonction ne fait que valider : elle n'affiche rien et ne touche pas à la base. Elle se teste facilement.

Réafficher le formulaire avec les erreurs

Si la validation échoue, on réaffiche le formulaire avec ce que l'utilisateur a saisi et un message à côté de chaque champ en erreur. Rien n'est plus agaçant qu'un formulaire qui se vide à la moindre faute.

php
<?php

declare(strict_types=1);

require __DIR__ . '/../src/validation.php';

$saisie = [];
$erreurs = [];

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $saisie = $_POST;
    ['valeurs' => $materiel, 'erreurs' => $erreurs] = validerMateriel($saisie);

    if ($erreurs === []) {
        // Enregistrement en base : chapitre 10
        header('Location: /materiels.php?ajout=1', true, 303);
        exit;
    }
}
?>
<form method="post" action="/ajouter.php" novalidate>
  <label for="libelle">Libellé</label>
  <input id="libelle" name="libelle"
         value="<?= htmlspecialchars(texte($saisie, 'libelle')) ?>"
         <?= isset($erreurs['libelle']) ? 'aria-invalid="true" aria-describedby="err-libelle"' : '' ?>>
  <?php if (isset($erreurs['libelle'])): ?>
    <p id="err-libelle" class="erreur"><?= htmlspecialchars($erreurs['libelle']) ?></p>
  <?php endif; ?>

  <label for="categorie">Catégorie</label>
  <select id="categorie" name="categorie">
    <option value="">-- choisir --</option>
    <?php foreach (CATEGORIES as $c): ?>
      <option value="<?= $c ?>" <?= texte($saisie, 'categorie') === $c ? 'selected' : '' ?>><?= $c ?></option>
    <?php endforeach; ?>
  </select>

  <button>Enregistrer</button>
</form>

La valeur réaffichée dans value="…" passe par htmlspecialchars() : sinon, une saisie contenant un guillemet casserait l'attribut, et une saisie malveillante pourrait injecter du HTML (chapitre 5). Les attributs aria-invalid et aria-describedby relient le message d'erreur au champ pour les lecteurs d'écran. L'attribut novalidate désactive la validation du navigateur, pratique pour tester tes contrôles serveur ; retire-le en production.

Rediriger après l'envoi : le motif Post/Redirect/Get

Après un enregistrement réussi, si la page de traitement affiche directement « Matériel ajouté », un clic sur Actualiser renvoie le formulaire : le matériel est ajouté une deuxième fois. La solution est le motif Post/Redirect/Get (PRG) : après un POST réussi, le serveur ne renvoie pas de page, mais une redirection vers une page en GET.

NavigateurServeur PHPPOST /ajouter.php (le formulaire)valide, enregistreen base (INSERT)303 See OtherLocation: /materiels.php?ajout=1GET /materiels.php?ajout=1200 OK : la liste et « Matériel ajouté »Actualiser (F5)GET /materiels.php?ajout=1200 OKseule la lecture est rejouée : rien n'est enregistré deux fois
Post/Redirect/Get : après un POST réussi, une redirection 303 vers une page en GET. Actualiser ne renvoie plus le formulaire.
php
<?php

header('Location: /materiels.php?ajout=1', true, 303);   // 303 See Other : « va voir cette page en GET »
exit;                                                     // indispensable : sinon le script continue

Trois règles avec header() :

  • Rien ne doit avoir été envoyé avant, pas même un espace ou une ligne vide avant <?php : les en-têtes partent avant le corps de la réponse. Sinon, PHP affiche « Cannot modify header information - headers already sent ».
  • exit juste après une redirection. header() n'arrête pas le script : sans exit, la suite s'exécute quand même, y compris un éventuel affichage de données confidentielles.
  • Le code 303 dit explicitement au navigateur de suivre la redirection en GET. Le code par défaut de header('Location: …') est 302, que les navigateurs traitent de la même façon, mais 303 est le code prévu pour ce cas.

Pour afficher « Matériel ajouté » sur la page de destination, on passe un indicateur dans l'URL (?ajout=1) ou, mieux, un message flash stocké dans la session, affiché une seule fois puis effacé (chapitre 6).

Les paramètres d'URL avec $_GET

Une page de détail reçoit l'identifiant de l'élément dans l'URL : materiel.php?id=4. C'est une saisie comme une autre, à valider :

php
<?php

declare(strict_types=1);

$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
if ($id === null || $id === false) {
    http_response_code(400);
    exit('Identifiant de matériel invalide.');
}
// $id est un int strictement positif : on peut chercher le matériel (chapitre 7)

Pour construire une URL avec des paramètres, utilise http_build_query(), qui encode correctement les espaces, les accents et les caractères spéciaux :

php
<?php

$params = ['categorie' => 'vidéoprojecteur', 'page' => 2, 'q' => 'Epson & co'];
$url = '/materiels.php?' . http_build_query($params);
// /materiels.php?categorie=vid%C3%A9oprojecteur&page=2&q=Epson+%26+co

Sans encodage, le & de « Epson & co » serait lu comme le début d'un nouveau paramètre. Pour encoder une seule valeur, urlencode() fait le même travail. N'oublie pas d'échapper l'URL obtenue si tu l'insères dans un attribut href, avec htmlspecialchars().

Exercices

Exercice 1 sur 3 Trouver les défauts du traitement Réussi À revoir

Un étudiant a écrit le traitement du formulaire d'emprunt de Prêtmat. Relève cinq défauts et propose une correction pour chacun.

php
<?php
$nbJours = $_REQUEST['nb_jours'];
$idMateriel = $_POST['id_materiel'];
if ($nbJours > 14) {
    echo "Durée trop longue";
}
echo "Emprunt enregistré pour le matériel $idMateriel";
// ... enregistrement en base ...
header('Location: /mes-emprunts.php');
Voir la réponseMasquer la réponse de l'exercice 1
  1. $_REQUEST mélange GET, POST et cookies : on ne sait pas d'où vient la valeur. Utiliser $_POST.
  2. Aucune validation de type : $nbJours est une chaîne, peut être absente (avertissement « Undefined array key »), négative ou valoir 'abc'. Valider avec filter_input(INPUT_POST, 'nb_jours', FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 14]]), et de même pour id_materiel avec min_range à 1.
  3. Le script continue après l'erreur : « Durée trop longue » s'affiche, puis l'emprunt est quand même enregistré. Il faut arrêter (réafficher le formulaire avec l'erreur, ou exit) quand la validation échoue.
  4. echo avant header() : du contenu est déjà envoyé, la redirection échoue avec « headers already sent ». Et sans exit après header(), le script continuerait. Supprimer l'echo, faire header('Location: /mes-emprunts.php', true, 303); exit;, et passer le message de confirmation par un message flash en session.
  5. $idMateriel est affiché sans échappement : une valeur forgée pourrait injecter du HTML. Avec la validation en entier du point 2, le risque disparaît, mais la règle reste : htmlspecialchars() à l'affichage.

On peut ajouter un sixième point : le formulaire ne vérifie pas que le matériel existe, qu'il est disponible et que l'utilisateur est connecté. Ce sont des règles de gestion et de contrôle d'accès, vérifiées côté serveur aux chapitres 8 et 9.

Tu avais trouvé ?

Exercice 2 sur 3 Valider des dates d'emprunt Réussi À revoir

Le formulaire d'emprunt contient deux champs date_debut et date_retour_prevue, au format AAAA-MM-JJ. Écris la fonction validerDatesEmprunt(array $saisie, DateTimeImmutable $aujourdhui): array qui renvoie un tableau d'erreurs indexé par champ, avec ces règles : les deux dates sont obligatoires et valides ; le début n'est pas dans le passé ; le retour prévu n'est pas avant le début ; la durée ne dépasse pas 14 jours. Teste-la avec un début le 2026-11-02 et un retour le 2026-11-16, puis le 2026-11-17 (en supposant que nous sommes le 2 novembre 2026).

Voir la réponseMasquer la réponse de l'exercice 2
php
<?php
declare(strict_types=1);

function lireDate(array $saisie, string $cle): ?DateTimeImmutable
{
    $texte = is_string($saisie[$cle] ?? null) ? trim($saisie[$cle]) : '';
    $date = DateTimeImmutable::createFromFormat('!Y-m-d', $texte);
    return ($date !== false && $date->format('Y-m-d') === $texte) ? $date : null;
}

function validerDatesEmprunt(array $saisie, DateTimeImmutable $aujourdhui): array
{
    $erreurs = [];
    $debut = lireDate($saisie, 'date_debut');
    $retour = lireDate($saisie, 'date_retour_prevue');

    if ($debut === null) {
        $erreurs['date_debut'] = 'Date de début obligatoire (AAAA-MM-JJ).';
    } elseif ($debut < $aujourdhui) {
        $erreurs['date_debut'] = 'Le début ne peut pas être dans le passé.';
    }
    if ($retour === null) {
        $erreurs['date_retour_prevue'] = 'Date de retour obligatoire (AAAA-MM-JJ).';
    }
    if ($debut !== null && $retour !== null) {
        if ($retour < $debut) {
            $erreurs['date_retour_prevue'] = 'Le retour ne peut pas précéder le début.';
        } elseif ($debut->diff($retour)->days > 14) {
            $erreurs['date_retour_prevue'] = 'Un prêt dure 14 jours au plus.';
        }
    }
    return $erreurs;
}

$aujourdhui = new DateTimeImmutable('2026-11-02');
var_dump(validerDatesEmprunt(['date_debut' => '2026-11-02', 'date_retour_prevue' => '2026-11-16'], $aujourdhui));
// array(0) {} : 14 jours, accepté
var_dump(validerDatesEmprunt(['date_debut' => '2026-11-02', 'date_retour_prevue' => '2026-11-17'], $aujourdhui));
// ['date_retour_prevue' => 'Un prêt dure 14 jours au plus.'] : 15 jours

La date du jour est passée en paramètre au lieu d'être lue dans la fonction : on peut ainsi tester la fonction avec une date fixe, et le test donnera le même résultat demain. En production, on l'appelle avec new DateTimeImmutable('today'). Le signe ! du format remet l'heure à minuit, pour que la comparaison avec « aujourd'hui » ne dépende pas de l'heure.

Tu avais trouvé ?

Exercice 3 sur 3 GET ou POST ? Réussi À revoir

Pour chaque action de Prêtmat, indique la méthode HTTP à utiliser et justifie.

  1. Afficher la liste des tablettes disponibles, page 2.
  2. Se connecter avec son email et son mot de passe.
  3. Enregistrer le retour d'un portable.
  4. Afficher la fiche du matériel n° 4.
  5. Supprimer un matériel réformé.
Voir la réponseMasquer la réponse de l'exercice 3
  1. GET (materiels.php?categorie=tablette&page=2) : c'est une lecture ; l'URL se partage et se met en favori.
  2. POST : le mot de passe ne doit jamais apparaître dans l'URL, l'historique ou les journaux.
  3. POST : l'action modifie des données.
  4. GET (materiel.php?id=4) : lecture.
  5. POST, avec un jeton anti-CSRF et une confirmation : l'action modifie (et détruit) des données. Jamais un simple lien en GET.

Tu avais trouvé ?

Chapitre terminé !

Fais les 3 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Faille XSS en PHP : htmlspecialchars et échappementRevoir le sommaire du cours