Leçon 13 / 18

Enregistrer les contacts dans un fichier JSON

35 min PHP 8 — premiers scripts et formulaires Gratuit

Objectif : Enregistrer et relire la liste des contacts dans un fichier JSON, en préservant les données déjà présentes et en gérant les erreurs de stockage.

Jusqu’ici, un contact valide pouvait être accepté, mais il disparaissait dès que le script se terminait. Vous allez conserver la liste dans un fichier JSON local et la recharger à chaque affichage. Une erreur de lecture ou d’écriture ne doit pas être confondue avec une liste vide : cette distinction évite d’effacer les données par accident.

Préparer le fichier de stockage

Le fichier contacts.json sera créé à la racine du projet, à côté de index.php. Il contient les données, pas du code PHP ; il ne fait donc pas partie des fichiers PHP listés dans le contrat. Le processus qui exécute PHP doit avoir le droit de créer ce fichier et d’y écrire. N’accordez pas des droits d’écriture plus larges que nécessaire sur le dossier du projet.

Isoler les opérations JSON

Ajoutez ces fonctions à includes/fonctions.php, en conservant les fonctions déjà présentes, dont résuméContact. Elles gardent l’accès au fichier à l’écart des pages et ne produisent ni HTML ni sortie directe. Le contenu est décodé en tableaux associatifs pour rester compatible avec la représentation des contacts déjà utilisée dans le carnet.

PHP
<?php

declare(strict_types=1);

function lireContacts(): array
{
    $chemin = dirname(__DIR__) . '/contacts.json';

    if (!is_file($chemin)) {
        return [];
    }

    $contenu = file_get_contents($chemin);

    if ($contenu === false) {
        throw new RuntimeException('Lecture du carnet impossible.');
    }

    try {
        $contacts = json_decode(
            $contenu,
            true,
            512,
            JSON_THROW_ON_ERROR
        );
    } catch (JsonException $erreur) {
        throw new RuntimeException(
            'Le fichier du carnet contient des données invalides.',
            0,
            $erreur
        );
    }

    if (!is_array($contacts)) {
        throw new RuntimeException('Le format du carnet est invalide.');
    }

    foreach ($contacts as $contact) {
        if (
            !is_array($contact)
            || !isset($contact['nom'], $contact['courriel'])
            || !is_string($contact['nom'])
            || !is_string($contact['courriel'])
            || (isset($contact['telephone']) && !is_string($contact['telephone']))
        ) {
            throw new RuntimeException('Un contact du carnet est invalide.');
        }
    }

    return $contacts;
}

function enregistrerContacts(array $contacts): bool
{
    try {
        $contenu = json_encode(
            $contacts,
            JSON_PRETTY_PRINT
                | JSON_UNESCAPED_UNICODE
                | JSON_THROW_ON_ERROR
        );
    } catch (JsonException) {
        return false;
    }

    $chemin = dirname(__DIR__) . '/contacts.json';
    $resultat = file_put_contents($chemin, $contenu . PHP_EOL, LOCK_EX);

    return $resultat !== false;
}

function résuméContact(array $contact): string
{
    return $contact['nom'] . ' — ' . $contact['courriel'];
}

json_decode reçoit l’option JSON_THROW_ON_ERROR : un fichier mal formé ne se transforme pas silencieusement en valeur inutilisable. La fonction vérifie également que la racine est un tableau et que chaque entrée possède au minimum un nom et une adresse e-mail textuels. file_put_contents reçoit la liste complète encodée ; LOCK_EX demande un verrou exclusif pendant l’écriture. Le résultat booléen indique à la page si l’enregistrement a réussi.

Charger le carnet dans la liste

Dans index.php, lisez les contacts avant de produire le HTML. En cas d’erreur, préparez un message et une liste vide uniquement pour le rendu de cette requête ; aucune écriture n’est effectuée. Les inclusions communes restent celles déjà en place, et l’échappement des valeurs affichées reprend la méthode vue dans la leçon sur la sortie HTML.

PHP
<?php

declare(strict_types=1);

require_once __DIR__ . '/includes/fonctions.php';

$contacts = [];
$erreurLecture = '';

try {
    $contacts = lireContacts();
} catch (RuntimeException) {
    $erreurLecture = 'Le carnet ne peut pas être lu pour le moment.';
}

require __DIR__ . '/includes/entete.php';
?>
<main class="carnet-principal">
  <section aria-labelledby="titre-contacts">
    <h2 id="titre-contacts">Liste des contacts</h2>
    <?php if ($erreurLecture !== ''): ?>
      <p class="carnet-message carnet-message--erreur" role="alert">
        <?= htmlspecialchars($erreurLecture, ENT_QUOTES, 'UTF-8') ?>
      </p>
    <?php elseif ($contacts === []): ?>
      <p class="carnet-message" role="status">Aucun contact enregistré.</p>
    <?php else: ?>
      <p><?= count($contacts) ?> contact(s) enregistré(s).</p>
      <ul class="carnet-liste">
        <?php foreach ($contacts as $contact): ?>
          <li class="carnet-contact">
            <h3><?= htmlspecialchars($contact['nom'], ENT_QUOTES, 'UTF-8') ?></h3>
            <p><?= htmlspecialchars($contact['courriel'], ENT_QUOTES, 'UTF-8') ?></p>
            <?php if (isset($contact['telephone'])): ?>
              <p><?= htmlspecialchars($contact['telephone'], ENT_QUOTES, 'UTF-8') ?></p>
            <?php endif; ?>
          </li>
        <?php endforeach; ?>
      </ul>
    <?php endif; ?>
  </section>
</main>
<?php require __DIR__ . '/includes/pied.php';
Résultat attendu
Au premier chargement, la page affiche « Aucun contact enregistré. ». Après un ajout réussi, elle affiche le compteur et les coordonnées enregistrées. Si le fichier est invalide ou illisible, un message d’erreur apparaît sans tentative de sauvegarde.

Enregistrer un ajout valide

Dans contact-ajouter.php, gardez la validation et la préparation des valeurs avant le rendu, comme dans la leçon précédente. Une fois les deux champs valides, lisez la liste actuelle, ajoutez le nouveau contact, puis écrivez la liste complète. En cas d’échec de lecture, ne poursuivez pas vers l’écriture : vous ne pourriez pas garantir la conservation des contacts précédents.

PHP
<?php

declare(strict_types=1);

require_once __DIR__ . '/includes/fonctions.php';

$valeurs = ['nom' => '', 'courriel' => ''];
$erreurs = [];
$message = '';
$erreurStockage = false;

if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
    $nomSoumis = filter_input(INPUT_POST, 'nom', FILTER_UNSAFE_RAW);
    $courrielSoumis = filter_input(INPUT_POST, 'courriel', FILTER_UNSAFE_RAW);

    $valeurs['nom'] = is_string($nomSoumis) ? trim($nomSoumis) : '';
    $valeurs['courriel'] = is_string($courrielSoumis) ? trim($courrielSoumis) : '';

    if ($valeurs['nom'] === '') {
        $erreurs['nom'] = 'Saisissez un nom.';
    } elseif (strlen($valeurs['nom']) > 100) {
        $erreurs['nom'] = 'Le nom ne doit pas dépasser 100 octets.';
    }

    if (filter_var($valeurs['courriel'], FILTER_VALIDATE_EMAIL) === false) {
        $erreurs['courriel'] = 'Saisissez une adresse e-mail valide.';
    }

    if ($erreurs === []) {
        try {
            $contacts = lireContacts();
            $contacts[] = [
                'nom' => $valeurs['nom'],
                'courriel' => $valeurs['courriel'],
            ];

            if (enregistrerContacts($contacts)) {
                $message = 'Le contact a été enregistré.';
                $valeurs = ['nom' => '', 'courriel' => ''];
            } else {
                $erreurStockage = true;
            }
        } catch (RuntimeException) {
            $erreurStockage = true;
        }
    }
}

require __DIR__ . '/includes/entete.php';
?>
<main class="carnet-principal">
  <section aria-labelledby="titre-contact">
    <h2 id="titre-contact">Ajouter un contact</h2>
    <?php if ($message !== ''): ?>
      <p class="carnet-message" role="status">
        <?= htmlspecialchars($message, ENT_QUOTES, 'UTF-8') ?>
      </p>
    <?php endif; ?>
    <?php if ($erreurStockage): ?>
      <p class="carnet-message carnet-message--erreur" role="alert">
        L’enregistrement a échoué. Vérifiez le fichier du carnet et ses droits d’accès.
      </p>
    <?php endif; ?>
    <form class="carnet-formulaire" action="contact-ajouter.php" method="post">
      <label class="carnet-champ" for="nom">Nom</label>
      <input id="nom" name="nom" type="text" required maxlength="100"
        value="<?= htmlspecialchars($valeurs['nom'], ENT_QUOTES, 'UTF-8') ?>"
        <?= isset($erreurs['nom']) ? 'aria-invalid="true" aria-describedby="erreur-nom"' : '' ?>>
      <?php if (isset($erreurs['nom'])): ?>
        <p id="erreur-nom" class="carnet-message carnet-message--erreur" role="alert">
          <?= htmlspecialchars($erreurs['nom'], ENT_QUOTES, 'UTF-8') ?>
        </p>
      <?php endif; ?>
      <label class="carnet-champ" for="courriel">Adresse e-mail</label>
      <input id="courriel" name="courriel" type="email" required
        value="<?= htmlspecialchars($valeurs['courriel'], ENT_QUOTES, 'UTF-8') ?>"
        <?= isset($erreurs['courriel']) ? 'aria-invalid="true" aria-describedby="erreur-courriel"' : '' ?>>
      <?php if (isset($erreurs['courriel'])): ?>
        <p id="erreur-courriel" class="carnet-message carnet-message--erreur" role="alert">
          <?= htmlspecialchars($erreurs['courriel'], ENT_QUOTES, 'UTF-8') ?>
        </p>
      <?php endif; ?>
      <div class="carnet-actions"><button type="submit">Enregistrer</button></div>
    </form>
  </section>
</main>
<?php require __DIR__ . '/includes/pied.php';
Résultat attendu
Après un POST valide, le fichier contient un tableau JSON lisible, par exemple : [{"nom":"Ada Lovelace","courriel":"[email protected]"}]. Après une erreur de validation ou de stockage, les valeurs saisies restent présentes dans le formulaire.

Les fichiers includes/entete.php et includes/pied.php restent les inclusions communes déjà créées : l’en-tête fournit le début de la page et son conteneur, le pied la termine. Si votre version de contact-ajouter.php possède déjà des attributs et messages d’erreur pour les champs, gardez ce rendu et remplacez seulement le traitement de soumission par la lecture, l’ajout et l’appel à enregistrerContacts montrés ici. Le code ci-dessus montre l’ensemble de la page pour rendre le parcours exécutable avec ces inclusions.

À retenir

  • Un fichier absent représente une liste vide ; un fichier illisible ou invalide doit rester une erreur distincte.
  • Décodez le JSON et vérifiez sa structure avant de modifier la liste.
  • Après validation, relisez la liste, ajoutez le contact, puis enregistrez l’ensemble.
  • Vérifiez le résultat de l’écriture et affichez un message compréhensible sans exposer les détails techniques.

La prochaine leçon ajoutera le contrôle d’accès au carnet ; le stockage JSON restera isolé derrière ces fonctions.

Envie d'aller plus loin ?

Les cours Enovatic sont gratuits et le resteront. Si vous cherchez un accompagnement encadré, nos formations professionnelles sont animées par les mêmes formateurs.