NULL, chaîne vide et réponse en erreur
Un projet en cours peut avoir une date de fin absente, représentée par NULL en base et null en JSON. Une description vide est une chaîne ; une table vide est une liste sans projet ; une connexion en échec est une erreur. Ces situations appellent des réponses différentes.
| Situation | Représentation attendue | Sens |
|---|---|---|
| Projet sans date de fin. | end_date: null | Projet encore en cours. |
| Aucun projet enregistré. | [] | Lecture réussie sans résultat. |
| Identifiant du projet. | Nombre entier. | Identité indépendante du classement. |
| Panne de base. | Réponse d’erreur contrôlée. | Lecture impossible, pas liste vide. |
Le schéma JSON des projets
Sélectionnez explicitement les colonnes utiles et définissez un ordre. Les noms du JSON doivent rester ceux du dictionnaire. Le serveur transforme les types si nécessaire avant l’encodage. Un champ absent par accident ne doit pas être traité comme une valeur optionnelle sans vérifier le contrat.
Le DSN et les identifiants de connexion restent côté PHP. Un fichier JavaScript public ne doit pas contenir un mot de passe SQL. Le navigateur consomme une adresse d’API, même si les deux services tournent sur le même ordinateur.
La réponse de l’API avant le rendu
Importez le schéma dans une base de test, insérez quelques projets variés et ouvrez la réponse GET. Vérifiez identité, noms, types et ordre avant de construire des cartes. Testez aussi une table vide et une panne contrôlée. Cela sépare une erreur de stockage d’une erreur de rendu.
Attention aux dates : une valeur DATE représente une date de calendrier, pas un instant avec fuseau horaire. Le dictionnaire et le formulaire doivent annoncer le format attendu. Ne convertissez pas silencieusement une date de calendrier en un autre jour à cause d’un fuseau.
Le schéma SQL du portfolio
Le portfolio réutilise les échanges Fetch/PHP/PDO vus en développement web. On change le domaine métier, pas les rôles du navigateur, du serveur et de la base. Une clé primaire reste l’identité du projet ; le titre seul ne suffit pas car deux créations peuvent porter le même nom.
CREATE TABLE projects (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(120) NOT NULL,
description TEXT NOT NULL,
image_url VARCHAR(500) NOT NULL DEFAULT '',
start_date DATE NOT NULL,
end_date DATE NULL,
technologies VARCHAR(200) NOT NULL DEFAULT '',
project_url VARCHAR(500) NOT NULL DEFAULT '',
code_url VARCHAR(500) NOT NULL DEFAULT '',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
NULL signifie une valeur absente ; ce n’est ni la chaîne "NULL" ni une date vide. created_at est un instant d’enregistrement, distinct de la date à laquelle le projet a commencé. Le dictionnaire fixe ces différences avant la saisie.
$sql = 'SELECT id, title, description, image_url, start_date, end_date,
technologies, project_url, code_url
FROM projects ORDER BY start_date DESC, id DESC';
$projects = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
Sortie attendue : un tableau JSON, [] si la table est vide. Le tri utilise d’abord la date, puis l’id pour départager deux dates identiques. Sélectionner explicitement les colonnes stabilise le contrat et évite d’envoyer des champs internes sans nécessité.
Replacer le portfolio dans l'architecture — 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;
}
pre {
white-space: pre-wrap;
overflow-wrap: anywhere;
}
</style>
<h1>Navigateur → PHP → MySQL → réponse</h1>
<p>
Simulation pédagogique : aucun serveur PHP ni base ne s’exécute dans cet
aperçu.
</p>
<label for="scenario">Scénario</label>
<select id="scenario">
<option value="ok">Lecture réussie</option>
<option value="invalid">Titre refusé</option>
<option value="server">Base indisponible</option>
</select>
<button id="suivant">Étape suivante</button>
<button id="reset">Recommencer</button>
<ol id="trajet"></ol>
<pre id="sortie" aria-live="polite"></pre>
<script>
const scenarios = {
ok: [
"Navigateur : GET /api/todos.php",
"PHP : connexion PDO",
"MySQL : SELECT id, title, done FROM todos",
"PHP : réponse 200 application/json",
"Navigateur : JSON analysé, puis DOM construit",
],
invalid: [
"Navigateur : POST avec un titre vide",
"PHP : validation refusée avant SQL",
"PHP : réponse 422 application/json",
"Navigateur : message d’erreur ; saisie conservée",
],
server: [
"Navigateur : GET /api/todos.php",
"PHP : connexion PDO impossible",
"PHP : réponse 500 application/json",
"Navigateur : message général et possibilité de réessayer",
],
};
let index = 0;
const trajet = document.querySelector("#trajet");
const sortie = document.querySelector("#sortie");
function reset() {
index = 0;
trajet.replaceChildren();
sortie.textContent = "";
}
document.querySelector("#scenario").addEventListener("change", reset);
document.querySelector("#reset").addEventListener("click", reset);
document.querySelector("#suivant").addEventListener("click", () => {
const scenario = document.querySelector("#scenario").value;
const etapes = scenarios[scenario];
if (index >= etapes.length) return;
const li = document.createElement("li");
li.textContent = etapes[index++];
trajet.append(li);
if (index === etapes.length) {
sortie.textContent =
scenario === "ok"
? '[{"id":1,"title":"Préparer les photos","done":false}]'
: '{"error":"Opération impossible"}';
}
});
</script>