Rétention des données dans Laravel : nettoyage planifié, anonymisation et demandes d'effacement

Laravel Sécurité / Confidentialité / RGPD Architecture
Rétention des données dans Laravel : nettoyage planifié, anonymisation et demandes d'effacement

Aucun framework ne fournit la rétention GDPR prête à l'emploi — Laravel inclus. Voici comment s'appuyer sur Prunable, ajouter une logique d'anonymisation, gérer la suppression en cascade des modèles liés et traiter les demandes d'effacement en production.

Pourquoi aucun framework ne résout ce problème à votre place

Si vous avez lu notre article précédent sur la rétention des données RGPD, vous connaissez la théorie : la base juridique détermine la durée de conservation, l'anonymisation n'est pas la même chose que la suppression, et le soft delete ne compte pas. Cet article porte sur ce qu'il faut pour que tout cela fonctionne réellement dans une application.

Nous avons vérifié chaque framework et CMS majeur — Laravel, Django, Rails, Symfony, WordPress, Drupal, Statamic, Craft. Aucun ne fournit la rétention des données conforme au RGPD prête à l'emploi. WordPress a un outil d'effacement manuel. Drupal a un module communautaire pour le nettoyage des formulaires. Laravel a un trait Prunable pour les suppressions définitives planifiées. Mais un pipeline de rétention complet — anonymisation, logique de cascade, piste d'audit, workflow d'effacement — n'existe nulle part comme fonctionnalité intégrée.

Ce n'est pas un défaut. Les règles de rétention sont trop spécifiques à chaque projet pour une solution générique. Ce que vous devez conserver, combien de temps, et ce qui se passe à l'expiration — tout dépend de votre modèle de données, de vos obligations légales et de votre logique métier. Le framework vous donne des briques. C'est à vous de les assembler.

Nous utilisons Laravel comme exemple ici parce que c'est ce avec quoi nous travaillons au quotidien, mais la réflexion s'applique à n'importe quelle stack.

Ce que Laravel offre nativement

Le trait Prunable de Laravel (disponible depuis la version 8.50) permet de marquer des modèles comme « prunables » et de définir une requête identifiant les enregistrements expirés :

class ContactSubmission extends Model
{
    use Prunable;

    public function prunable(): Builder
    {
        return static::where('created_at', '<=', now()->subMonths(6));
    }
}

Planifiez model:prune pour s'exécuter quotidiennement, et Laravel parcourt tous les modèles prunables et supprime les lignes correspondantes. Simple, intégré, aucune dépendance.

Mais Prunable ne supprime que des lignes entières. Il n'anonymise pas des champs spécifiques tout en conservant les autres. Il ne sait pas que supprimer un utilisateur devrait aussi nettoyer ses commentaires, uploads et journaux d'activité. Il n'enregistre pas ce qui a été supprimé ni pourquoi. Pour le RGPD, Prunable est un point de départ — pas une solution complète.

Deux approches : choisissez celle qui convient

Tous les projets n'ont pas besoin d'un framework de rétention complet. Un site marketing avec un formulaire de contact et une inscription newsletter est très différent d'une plateforme e-commerce avec des commandes, un historique de paiements et des tickets de support.

Pour les petits projets (2–5 modèles avec des données personnelles) : une seule commande planifiée qui liste explicitement chaque modèle et l'action à effectuer sur les enregistrements expirés suffit. Un seul fichier, un seul endroit à vérifier, facile à comprendre :

// Une commande, des règles explicites, fait en un après-midi
ContactSubmission::where('created_at', '<=', now()->subMonths(6))->delete();

Order::whereNull('anonymized_at')
    ->where('created_at', '<=', now()->subYears(2))
    ->each(fn (Order $order) => $order->anonymize());

Pour les projets plus importants (10+ modèles, plusieurs développeurs, changements continus) : vous avez besoin d'un contrat. Une interface que chaque modèle contenant des données personnelles doit implémenter :

interface Retainable
{
    public function retentionPeriod(): CarbonInterval;
    public function anonymize(): void;
    public function isRetained(): bool;
}

Quand un nouveau développeur ajoute un modèle qui stocke des e-mails ou des numéros de téléphone, le pattern est évident : implémenter l'interface, ajouter les règles d'anonymisation, écrire le test. Sans cela, les décisions de rétention sont implicites — enfouies quelque part dans une commande, faciles à manquer quand le modèle de données évolue.

Commencez simple, introduisez l'abstraction quand la répétition commence à causer des erreurs.

Anonymisation : plus que vider des champs

L'erreur d'implémentation la plus courante est de traiter l'anonymisation comme « mettre l'e-mail à null ». C'est plus nuancé que ça.

Un enregistrement de commande doit perdre le nom du client, l'e-mail et l'adresse de livraison — mais conserver la date, le montant et la catégorie de produit. Ces champs financiers peuvent être requis pour la conformité fiscale pendant 7 ans. Les identifiants personnels deviennent sans objet après 2 ans. Même table, durées de conservation différentes par champ :

public function anonymize(): void
{
    $this->update([
        'customer_name' => null,
        'customer_email' => null,
        'shipping_address' => null,
        'phone' => null,
        // amount, tax, product_category — inchangés
        'anonymized_at' => now(),
    ]);
}

Certains champs sont vidés. D'autres sont remplacés par un placeholder générique — « Utilisateur anonymisé » au lieu d'un nom est utile quand l'enregistrement apparaît encore dans les interfaces d'administration ou les rapports. Le timestamp anonymized_at permet de suivre ce qui a été traité et de le sauter lors des exécutions suivantes.

Le test critique : après anonymisation, regardez tout ce qui reste dans l'enregistrement. La combinaison restante de champs peut-elle identifier une personne ? Ville plus date de naissance plus un achat de produit de niche dans une petite ville — ça peut suffire. L'anonymisation ne se fait pas champ par champ. C'est une question de vue d'ensemble.

Cascade : la partie que tout le monde sous-estime

Supprimer ou anonymiser un utilisateur touche une seule table. Mais les données de cet utilisateur vivent dans de nombreux endroits : commentaires postés, commandes passées, journaux d'activité, fichiers uploadés sur disque ou stockage objet, sessions, jobs en file d'attente, tokens de réinitialisation de mot de passe, enregistrements de notifications.

Chaque enregistrement lié nécessite une décision explicite :

  • Supprimer — journaux d'activité, sessions, tokens de mot de passe. Aucune valeur sans la personne.

  • Anonymiser — commandes (conserver les données financières, supprimer les identifiants personnels), commentaires (remplacer le nom de l'auteur par « Anonyme »).

  • Supprimer les fichiers — avatars, documents, pièces jointes uploadés. Ceux-ci vivent en dehors de la base de données, souvent dans S3 ou un stockage similaire. Une suppression en base ne les touche pas.

  • Conserver avec justification — un ticket de support peut devoir rester pour des raisons juridiques. Documentez pourquoi.

Le modèle doit gérer sa cascade complète — pas seulement ses propres champs :

public function anonymize(): void
{
    $this->update([
        'name' => 'Utilisateur anonymisé',
        'email' => "anon-{$this->id}@removed.local",
        'phone' => null,
        'anonymized_at' => now(),
    ]);

    $this->orders->each(fn (Order $o) => $o->anonymize());
    $this->comments()->update(['author_name' => 'Anonyme']);
    $this->activityLogs()->delete();
    $this->uploads->each(function (Upload $upload) {
        Storage::delete($upload->path);
        $upload->delete();
    });
}

La carte de cascade est l'artefact le plus important de votre implémentation de rétention. Un conseil pratique : parcourez votre schéma de base de données pour chaque clé étrangère pointant vers la table users. Puis vérifiez les tables qui stockent des données utilisateur sans clé étrangère — champs dénormalisés, colonnes JSON, entrées de log avec des adresses e-mail en texte libre. C'est votre vraie carte de cascade, et elle est généralement plus grande que prévu.

Droit à l'effacement : un workflow, pas un bouton

Le nettoyage planifié traite les données naturellement expirées. Le droit à l'effacement est différent — c'est une demande initiée par l'utilisateur avec son propre processus et un délai de réponse de 30 jours.

Étape 1 : Vérifier l'identité. Une demande depuis un compte authentifié est simple. Une demande envoyée par e-mail par quelqu'un prétendant être un utilisateur ne l'est pas — vous devez confirmer que c'est bien lui. Un lien de confirmation vers l'adresse e-mail enregistrée est généralement suffisant. L'objectif est d'empêcher quelqu'un de supprimer les données d'une autre personne.

Étape 2 : Vérifier les blocages. Un abonnement actif, une facture impayée, une obligation légale de conservation — ce sont des motifs légitimes pour refuser partiellement ou totalement la demande. Les données fiscales qui doivent être conservées 7 ans ne sont pas supprimées simplement parce que quelqu'un le demande. Mais vous devez expliquer ce que vous conservez et pourquoi.

Étape 3 : Exécuter la cascade. Parcourez la carte de données complète. Chaque table, chaque relation, chaque emplacement de stockage de fichiers. C'est là que votre carte de cascade se rentabilise.

Étape 4 : Notifier les tiers. Stripe, Mailchimp, outils d'analytics, CRM — partout où des données personnelles ont été envoyées, la demande de suppression doit suivre.

Étape 5 : Journaliser l'action. Enregistrez ce qui a été fait, quand, et quelles catégories de données étaient concernées. Ne journalisez pas les données réellement supprimées.

Étape 6 : Répondre dans les 30 jours. Même si la réponse est « nous avons partiellement refusé, voici ce que nous conservons, et voici la base juridique pour le conserver. »

Conservez ce workflow dans une classe de service dédiée — pas dans un contrôleur, pas dans une commande console. Il sera appelé depuis les deux (panneau d'administration, self-service utilisateur, outils de l'équipe support), et il doit être testable indépendamment.

Sauvegardes : la vérité qui dérange

Vous avez supprimé l'utilisateur de la base de données. Mais la sauvegarde d'hier sur S3 contient encore son enregistrement complet. C'est la question que chaque équipe redoute — et la réponse est plus raisonnable qu'on ne le pense.

Le RGPD n'exige pas que vous supprimiez des enregistrements individuels de sauvegardes chiffrées. Ce serait techniquement irréalisable pour la plupart des systèmes. Ce qu'il exige :

  • Une durée de conservation définie pour les sauvegardes — 30 jours est courant. « Pour toujours, au cas où » n'est pas acceptable.

  • De la documentation — votre politique de confidentialité devrait mentionner la fenêtre de conservation des sauvegardes.

  • De la transparence dans les réponses d'effacement — « Vos données ont été supprimées de tous les systèmes actifs. Les sauvegardes chiffrées sont automatiquement purgées après 30 jours. »

Le mot clé est « définie ». Avoir une fenêtre de sauvegarde de 90 jours avec une justification documentée est acceptable. Ne pas avoir de fenêtre définie est une faille de conformité.

Tiers : construisez le registre maintenant

Les données circulent vers l'extérieur : processeurs de paiement, plateformes d'e-mail marketing, outils de support, analytics, CRM. En tant que responsable du traitement, vous êtes responsable de vous assurer qu'une demande de suppression atteint chacun d'entre eux.

Le registre doit exister avant l'arrivée de la première demande. Pour chaque tiers, documentez : quelles données personnelles ils détiennent, comment la suppression fonctionne (appel API, demande manuelle, tableau de bord self-service), et qui contacter. Découvrir tout cela sous pression lors de votre première demande d'effacement est la recette pour des délais manqués.

Certains services facilitent les choses — Stripe permet de supprimer un client via API. D'autres exigent d'envoyer un e-mail à leur équipe vie privée et d'attendre des jours. Intégrez les deux scénarios dans votre calendrier de réponse.

Tests : le filet de sécurité dont vous avez vraiment besoin

Une méthode d'anonymisation qui oublie un champ anéantit tout l'objectif. Ce n'est pas du code qu'on livre et qu'on oublie.

Le pattern de test le plus précieux : maintenir une liste explicite des champs PII sur chaque modèle, et vérifier que anonymize() les nettoie tous :

public function test_order_anonymize_clears_all_pii(): void
{
    $order = Order::factory()->create();
    $order->anonymize();
    $order->refresh();

    foreach (Order::PII_FIELDS as $field) {
        $this->assertNull(
            $order->{$field},
            "Le champ PII '{$field}' n'a pas été nettoyé"
        );
    }
}

Ce test fait double emploi. Il attrape les bugs dans votre logique d'anonymisation aujourd'hui, et il attrape la régression demain — quand un développeur ajoute un nouveau champ phone ou address au modèle mais oublie de mettre à jour anonymize(). Ajoutez le champ à PII_FIELDS, et le test échoue jusqu'à ce que la logique d'anonymisation soit mise à jour.

Au-delà des modèles individuels : testez la cascade complète. Après avoir traité un utilisateur, vérifiez que les commentaires sont anonymisés, que les fichiers uploadés ont disparu du stockage, et que les journaux d'activité sont purgés. La cascade est l'endroit où les choses cassent silencieusement.

Monitoring : prouvez que ça tourne

Un nettoyage planifié qui échoue silencieusement est pire que pas de nettoyage du tout — parce que vous croyez être conforme alors que vous ne l'êtes pas.

  • Journalisez chaque exécution — pas seulement « commande terminée », mais combien d'enregistrements traités, anonymisés, supprimés, et combien d'erreurs se sont produites.

  • Alertez sur l'absence — si le nettoyage n'a pas tourné dans l'intervalle prévu, quelque chose est cassé. Un scheduler mort est une faille de conformité silencieuse.

  • Alertez sur la croissance — si le nombre d'enregistrements dépassant leur durée de conservation augmente au lieu de diminuer, le nettoyage ne suit pas.

  • Rapports périodiques — un résumé mensuel pour la personne responsable de la conformité RGPD. Pas un tableau de bord qu'elle ne consultera jamais — un e-mail ou message qu'elle verra réellement.

Documentation : prouvez que vous avez bien fait

Le RGPD exige la preuve de conformité, pas seulement la conformité elle-même. Votre projet a besoin au minimum de :

  • Calendrier de rétention — quels types de données, quelles durées de conservation, quelle base juridique justifie chacune.

  • Cartographie des données — où vivent les données personnelles : tables de base de données, stockage de fichiers, services tiers, sauvegardes.

  • Journal d'audit — enregistrements horodatés des événements d'anonymisation et de suppression, sans les données réellement supprimées.

  • Registre des tiers — quels services externes détiennent des données personnelles, comment la suppression fonctionne pour chacun.

  • Procédure d'effacement — le processus étape par étape pour traiter les demandes, y compris les modèles de réponse.

Cette documentation sert deux publics. Les régulateurs peuvent en avoir besoin pour vérifier la conformité. Mais le lecteur le plus immédiat est le développeur qui rejoint votre équipe l'année prochaine et a besoin de comprendre pourquoi la commande de nettoyage existe, ce qu'elle couvre, et ce qui se passe si elle s'arrête de tourner.

Puis-je utiliser le trait Prunable de Laravel pour la conformité RGPD ?

Prunable est un bon point de départ pour les suppressions définitives planifiées — comme supprimer les soumissions de formulaire de contact de plus de six mois. Mais il ne supprime que des lignes entières. Il ne peut pas anonymiser des champs spécifiques, gérer la logique de cascade entre modèles liés, ou journaliser ce qui a été supprimé et pourquoi. Considérez-le comme une brique, pas la solution complète.

Et si mon projet n'a qu'un formulaire de contact et une newsletter — ai-je besoin de tout cela ?

Non. Une seule commande planifiée qui supprime les soumissions de contact de plus de six mois et retire les e-mails newsletter désabonnés suffit. L'architecture complète basée sur une interface devient précieuse quand vous avez 10+ modèles avec des données personnelles et plusieurs développeurs qui doivent suivre des patterns cohérents.

Comment gérer les demandes d'effacement pour des données envoyées à des services tiers comme Stripe ou Mailchimp ?

En tant que responsable du traitement, vous êtes responsable de transmettre la demande de suppression. Construisez un registre de tous les tiers avant l'arrivée de la première demande : quelles données ils détiennent, comment la suppression fonctionne (API, e-mail manuel, tableau de bord), et le délai de réponse attendu. Certains services comme Stripe offrent la suppression par API ; d'autres nécessitent de contacter directement leur équipe vie privée.

Existe-t-il des packages Laravel qui gèrent la rétention RGPD ?

Quelques-uns existent — comme soved/laravel-gdpr et dialect/laravel-gdpr-compliance. Ils couvrent les bases comme l'export de données, le chiffrement et le nettoyage des utilisateurs inactifs. Mais aucun ne gère l'anonymisation par champ, les règles de cascade pour les enregistrements liés, ou un workflow complet de droit à l'effacement. Vous devrez encore construire la logique de rétention adaptée à votre modèle de données spécifique.

Dois-je supprimer les données personnelles des sauvegardes quand quelqu'un demande l'effacement ?

Le RGPD n'exige pas de purger des enregistrements individuels de sauvegardes chiffrées — c'est techniquement irréalisable. Ce qu'il exige, c'est une durée de conservation définie et documentée pour les sauvegardes (30 à 90 jours est courant) et de la transparence : informez l'utilisateur que les sauvegardes chiffrées sont automatiquement purgées dans cette fenêtre.

Besoin d'une logique de rétention ?

On S'occupe du Code de Nettoyage

Des workflows d'anonymisation au traitement des demandes d'effacement — nous construisons le pipeline de rétention des données pour que votre application Laravel reste conforme sans effort manuel.

En envoyant, vous acceptez que nous traitions vos données pour répondre à votre demande et, le cas échéant, prendre des mesures précontractuelles à votre demande (RGPD art. 6(1)(b)) ou sur la base de nos intérêts légitimes (art. 6(1)(f)). Évitez de partager des données sensibles. Voir notre Politique de confidentialité.
Réponse sous 1 jour ouvrable.