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.
| Superglobale | Contenu | Exemple |
|---|---|---|
$_GET | Les paramètres de l'URL (après le ?) | materiel.php?id=4 donne $_GET['id'] === '4' |
$_POST | Les champs d'un formulaire envoyé en POST | $_POST['libelle'] |
$_FILES | Les fichiers envoyés par un formulaire | chapitre 13 |
$_COOKIE | Les cookies envoyés par le navigateur | chapitre 6 |
$_SESSION | Les données de session de l'utilisateur | chapitre 6 |
$_SERVER | Les informations sur la requête et le serveur | $_SERVER['REQUEST_METHOD'] vaut 'GET' ou 'POST' |
$_ENV | Les variables d'environnement (souvent vide, selon la configuration) | préfère getenv() (chapitre 15) |
$_REQUEST | Un mélange de $_GET, $_POST et $_COOKIE | à éviter : tu ne sais plus d'où vient la valeur |
GET ou POST : quelle méthode choisir
| GET | POST | |
|---|---|---|
| Où vont les données | Dans l'URL : liste.php?categorie=portable&page=2 | Dans le corps de la requête |
| Visible dans l'historique, les journaux du serveur, les favoris | Oui | Non |
| Usage | Lire : recherche, filtre, pagination, page de détail | Modifier : créer, modifier, supprimer, se connecter |
| Rejouer la requête (bouton Actualiser) | Sans conséquence | Le 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) :
<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
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 :
| Filtre | Accepte | Renvoie en cas d'échec |
|---|---|---|
FILTER_VALIDATE_INT | '42', ' 42 ' ; options min_range, max_range | false ('42abc', '4.2') |
FILTER_VALIDATE_FLOAT | '1180.50' ; option decimal pour accepter la virgule | false |
FILTER_VALIDATE_EMAIL | une adresse syntaxiquement valide | false |
FILTER_VALIDATE_BOOL | '1', 'on', 'yes', 'true' et leurs contraires | false, ou null avec l'option FILTER_NULL_ON_FAILURE |
FILTER_VALIDATE_URL | une URL complète | false |
<?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
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 envoyerlibelle[]=x:$_POST['libelle']est alors un tableau, ettrim()lèverait uneTypeError. 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
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.
<?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 ». exitjuste après une redirection.header()n'arrête pas le script : sansexit, 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
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
$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
$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
$_REQUESTmélange GET, POST et cookies : on ne sait pas d'où vient la valeur. Utiliser$_POST.- Aucune validation de type :
$nbJoursest une chaîne, peut être absente (avertissement « Undefined array key »), négative ou valoir'abc'. Valider avecfilter_input(INPUT_POST, 'nb_jours', FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 14]]), et de même pourid_materielavecmin_rangeà 1. - 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. echoavantheader(): du contenu est déjà envoyé, la redirection échoue avec « headers already sent ». Et sansexitaprèsheader(), le script continuerait. Supprimer l'echo, faireheader('Location: /mes-emprunts.php', true, 303); exit;, et passer le message de confirmation par un message flash en session.$idMaterielest 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
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.
- Afficher la liste des tablettes disponibles, page 2.
- Se connecter avec son email et son mot de passe.
- Enregistrer le retour d'un portable.
- Afficher la fiche du matériel n° 4.
- Supprimer un matériel réformé.
Voir la réponseMasquer la réponse de l'exercice 3
- GET (
materiels.php?categorie=tablette&page=2) : c'est une lecture ; l'URL se partage et se met en favori. - POST : le mot de passe ne doit jamais apparaître dans l'URL, l'historique ou les journaux.
- POST : l'action modifie des données.
- GET (
materiel.php?id=4) : lecture. - 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é ?