Identifiant SQL et position dans un tableau

Le troisième élément visible peut posséder l’id 27. Après un filtre, il peut devenir le premier. Une commande reçoit donc l’identifiant de l’enregistrement, pas l’indice de sa carte dans la liste. Cette distinction vaut pour les boutons, les paramètres HTTP et les clauses SQL.

OpérationMéthode du contratCible et réponse
Renommer.PATCHId existant et champ modifié.
Changer done.PATCHId existant et booléen validé.
Supprimer.DELETEId existant, confirmation selon l’API.

UPDATE et DELETE avec WHERE

UPDATE todos SET title = :title WHERE id = :id;
DELETE FROM todos WHERE id = :id;

WHERE limite les lignes concernées. Sans cette condition, une commande peut modifier ou supprimer toute la table. Les valeurs sont liées par une requête préparée. Un identifiant provenant du navigateur reste une entrée à valider côté serveur ; la présence d’un bouton correct n’est pas une garantie.

Ligne absente et valeur inchangée

Une tâche peut exister et recevoir le même titre qu’avant. Selon la configuration du pilote, le nombre de lignes signalées comme changées peut être nul ; cela ne prouve pas à lui seul que l’id est absent. Le contrat doit distinguer enregistrement inexistant et opération sans changement.

Pour une suppression, demandez une confirmation montrant la cible. Si la requête échoue, ne retirez pas définitivement la carte en annonçant un succès. Pour le socle du TP, la mise à jour visuelle suit la confirmation du serveur ; un comportement optimiste demanderait une stratégie de retour arrière qui n’est pas nécessaire ici.

Modifier une tâche par son identifiant

Une liste peut être triée ou filtrée. Son troisième élément n’est donc pas toujours la tâche numéro 3. Les opérations ciblent la clé primaire id reçue du serveur. Dans notre contrat, PATCH modifie title et/ou done ; DELETE supprime la ligne. Un identifiant absent en base donne 404.

await fetch(`/api/todos.php?id=${id}`, {
  method: 'PATCH',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ done: true }),
});

Le code complet vérifie response.ok, conserve l’état de départ si l’envoi échoue et recharge ensuite la liste. done: true est un booléen JSON, pas "true". N’envoyez pas tous les champs si un seul doit changer.

$stmt = $pdo->prepare('UPDATE todos SET done = :done WHERE id = :id');
$stmt->execute(['done' => $done ? 1 : 0, 'id' => $id]);

$stmt = $pdo->prepare('DELETE FROM todos WHERE id = :id');
$stmt->execute(['id' => $id]);

Sans WHERE, ces instructions toucheraient toutes les lignes. Vérifiez l’existence de la tâche avant une modification : rowCount() = 0 après UPDATE peut aussi signifier qu’une valeur est restée identique. Ne confondez pas « absent » et « déjà à cette valeur ».

Changer et supprimer par identifiant — simulation

<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();
          });
          const titleInput = document.createElement("input");
          titleInput.value = task.title;
          titleInput.maxLength = 120;
          titleInput.setAttribute(
            "aria-label",
            `Nouveau titre de ${task.title}`,
          );
          const rename = document.createElement("button");
          rename.textContent = "Renommer";
          rename.addEventListener("click", () => {
            const title = titleInput.value.trim();
            if (!title) {
              document.querySelector("#etat").textContent =
                "Le titre est obligatoire.";
              return;
            }
            task.title = title;
            render();
          });
          li.append(label, titleInput, rename, 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>

Tableau de référence — notions et opérations

NotionRègle à retenir
IdentitéLes opérations utilisent l’id stable et pas la position affichée.
PATCHPATCH change certains champs de la tâche existante.
DELETEDELETE supprime la tâche ciblée.
Paramètre idLe contrat place l’id dans le paramètre de requête id.
Booléen strictLe champ done est un booléen JSON dans le contrat.
Modification SQLUPDATE modifie une ligne existante.
Suppression SQLDELETE FROM supprime les lignes ciblées.
Filtrage SQLWHERE id = :id limite la modification à la tâche visée.
Sans WHEREUn UPDATE sans WHERE peut toucher toutes les lignes.
ParamètresLes valeurs et l’id sont transmis par paramètres préparés.
Id positifL’id doit être un entier strictement positif.
AbsenceUne tâche absente reçoit 404.
Modification identiqueUn UPDATE sans changement de valeur ne prouve pas que la ligne est absente.
Vérification existenceUne lecture par id permet de distinguer absence et valeur inchangée.
Confirmation interfaceresponse.ok doit être vérifié avant d’annoncer le succès.
Conservation étatUne requête échouée ne doit pas présenter un nouvel état comme enregistré.
Contrôle nomméLe bouton Supprimer identifie la tâche concernée dans son nom accessible.
Confirmation suppressionUne suppression est précédée d’une confirmation explicite dans le travail attendu.
Suppression répétéeUn second DELETE sur une tâche déjà supprimée reçoit 404 dans notre contrat.
Vérification voisinsUn test vérifie que les tâches voisines restent intactes.