Le trajet du POST jusqu’à INSERT
Le formulaire produit une intention ; JavaScript prépare un corps JSON ; PHP le lit et valide les champs ; PDO réalise l’insertion ; la réponse confirme le résultat. Ne confondez pas le texte encore présent dans le champ avec une donnée déjà enregistrée.
| Étape | Contrôle | Résultat si le contrôle échoue |
|---|---|---|
| Lire le corps. | JSON analysable. | Requête mal formée, pas une insertion. |
| Lire le titre. | Chaîne présente. | Erreur de champ/type. |
| Valider le titre. | Texte non vide après trim, longueur autorisée. | Réponse de validation refusée. |
| Insérer. | Requête préparée et base disponible. | Réponse serveur utile, sans détails SQL. |
| Confirmer. | Identifiant généré et donnée enregistrée. | Le client attend la confirmation avant de vider le champ. |
prepare, execute et paramètres SQL
$requete = $pdo->prepare('INSERT INTO todos (title) VALUES (:title)');
$requete->execute(['title' => $titre]);
Le SQL fixe l’opération ; :title est un emplacement réservé pour une valeur. La valeur est transmise séparément. Une apostrophe dans un titre reste une donnée au lieu de casser la construction de la requête. Cela ne remplace pas la vérification de longueur ni les règles métier.
La confirmation de création et le rendu
Désactivez la commande pendant l’envoi et rétablissez-la dans finally. En cas d’échec, conservez le texte pour permettre une correction. Après réussite, ajoutez l’objet confirmé ou rechargez la liste selon le choix documenté. Le nouvel identifiant vient du serveur : il ne se déduit pas du nombre de cartes affichées.
Testez un titre normal, un titre avec une apostrophe, des espaces seuls et un double clic. Une création réussie doit produire une seule ligne selon le fonctionnement attendu du formulaire, et une erreur ne doit pas faire disparaître la saisie.
Le formulaire, la validation PHP et la réponse 201
Le formulaire recueille une intention ; PHP valide les données ; MySQL enregistre ; la réponse confirme l’opération. Ne videz pas le champ avant cette confirmation : si le réseau échoue, l’utilisateur doit retrouver son texte. La validation HTML améliore l’expérience mais ne remplace pas la validation serveur.
form.addEventListener('submit', async (event) => {
event.preventDefault();
const title = new FormData(form).get('title').trim();
const response = await fetch('/api/todos.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title }),
});
if (!response.ok) throw new Error('Création refusée');
const nouvelleTache = await response.json();
form.reset();
await lireTaches();
});
Dans la version complète, encadrez le traitement par try/catch/finally, bloquez le bouton pendant l’envoi et réactivez-le même en cas d’erreur. L’extrait isole le trajet de création ; ne le considérez pas comme une gestion complète des erreurs.
$data = json_decode(file_get_contents('php://input'), true, 512, JSON_THROW_ON_ERROR);
$title = trim($data['title'] ?? '');
// Après vérification : chaîne non vide, au plus 120 caractères.
$stmt = $pdo->prepare('INSERT INTO todos (title) VALUES (:title)');
$stmt->execute(['title' => $title]);
$id = (int) $pdo->lastInsertId();
Le paramètre :title sépare la valeur de la structure SQL. Une apostrophe dans « L’atelier photo » ne doit pas casser la requête. Le titre n’est ni un nom de table ni un morceau de SQL. La réponse de notre POST est l’objet créé avec son identifiant et le statut 201.
Observer l'ajout d'une tâche — simulation en mémoire
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 20px;
font-family: system-ui, sans-serif;
color: #18324c;
background: #f1f5f9;
}
button,
input,
select,
textarea {
font: inherit;
max-width: 100%;
}
button {
padding: 8px 12px;
cursor: pointer;
}
input,
select,
textarea {
padding: 8px;
}
:focus-visible {
outline: 3px solid #2563eb;
outline-offset: 3px;
}
article,
.panel {
padding: 16px;
background: white;
border-radius: 12px;
min-width: 0;
}
.cards {
display: grid;
gap: 16px;
}
label {
display: block;
margin-block: 12px 4px;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.15rem;
}
p,
h2,
a {
overflow-wrap: anywhere;
}
.note {
font-size: 0.9rem;
}
</style>
<h1>Prototype de Todo List</h1>
<p class="note">
Simulation en mémoire : aucun appel API ; les ajouts disparaissent au
rechargement.
</p>
<form id="formulaire">
<label for="title">Tâche</label>
<input id="title" name="title" required maxlength="120" />
<button>Ajouter</button>
</form>
<label for="filtre">Afficher</label>
<select id="filtre">
<option value="all">Toutes</option>
<option value="todo">À faire</option>
<option value="done">Terminées</option>
</select>
<ul id="liste"></ul>
<p id="etat" role="status"></p>
<script>
let tasks = [{ id: 1, title: "Préparer les photos", done: false }];
let nextId = 2;
const form = document.querySelector("#formulaire");
function render() {
const filter = document.querySelector("#filtre").value;
const visible = tasks.filter(
(t) => filter === "all" || t.done === (filter === "done"),
);
const rows = visible.map((task) => {
const li = document.createElement("li");
const label = document.createElement("label");
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = task.done;
checkbox.addEventListener("change", () => {
task.done = checkbox.checked;
render();
});
label.append(
checkbox,
document.createTextNode(" " + task.title + " "),
);
const remove = document.createElement("button");
remove.textContent = "Supprimer";
remove.setAttribute("aria-label", `Supprimer ${task.title}`);
remove.addEventListener("click", () => {
tasks = tasks.filter((t) => t.id !== task.id);
render();
});
li.append(label, remove);
return li;
});
document.querySelector("#liste").replaceChildren(...rows);
document.querySelector("#etat").textContent =
`${visible.length} résultat(s), ${tasks.filter((t) => !t.done).length} à faire`;
}
form.addEventListener("submit", (event) => {
event.preventDefault();
const title = new FormData(form).get("title").trim();
if (!title) {
document.querySelector("#etat").textContent =
"Le titre est obligatoire.";
return;
}
tasks.push({ id: nextId++, title, done: false });
form.reset();
render();
});
document.querySelector("#filtre").addEventListener("change", render);
render();
</script>