Soft Delete : Comment se passer de Gedmo dans Symfony

Soft Delete : Comment se passer de Gedmo dans Symfony

Symfony propose de nombreuses fonctionnalités et Doctrine en ajoute également beaucoup. Au fur et à mesure que la standardisation de Symfony avance, les bundles les moins utiles et les plus consommateurs sont remplacés par des snippets efficaces. Avec l’utilisation de Doctrine comme ORM, on a souvent besoin de Gedmo pour gérer le Soft Delete. Je me suis posé quelques minutes pour mettre en place un système similaire dans le but de limiter les dépendances de mon application.

Comment fonctionne le Soft Delete ?

Vous voulez supprimer un objet. Plutôt que de directement le supprimer, vous voulez l’archiver de manière à ce qu’il puisse être restauré plus tard. Cet objet archivé pourra être réellement supprimé par la suite. Pour cela, nous ajoutons un champs deletedAt sur notre entité. Si le champs est null, l’objet est disponible. Au contraire si le champs est complété de la date de suppression effective ( < maintenant), l’article est supprimé.

Intégration à Doctrine

Nous voulons mettre en place le même système que ce que Gedmo peut nous proposer. Le seul comportement que nous devons surcharger est la méthode remove de l’entity manager.

La deuxième partie consiste à cacher les objets qui ont été supprimés.

1
2
3
4
5
6
7
8
9
10
$em = $this->container->get("doctrine.orm.default_entity_manager");
$object = $em->getRepository("NamespaceMyBundle:Entity")->find(1);
// $object != null
// $object->getDeletedAt();
$em->remove($object);
$object = $em->getRepository("NamespaceMyBundle:Entity")->find(1);
// $object == null

Codons bien, codons efficace

Préparons notre entité

Notre entité doit posséder un attribut de type date (datetime).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php
namespace Namespace\MyBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
class Entity
{
/**
* @var datetime $deletedAt
*
* @ORM\Column(name="deletedAt", type="datetime", nullable=true)
*/
private $deletedAt;
    /**
     * Set deletedAt
     *
     * @param  \DateTime $deletedAt
     * @return Plan
     */
    public function setDeletedAt($deletedAt)
    {
        $this->deletedAt = $deletedAt;
        return $this;
    }
    /**
     * Get deletedAt
     *
     * @return \DateTime
     */
    public function getDeletedAt()
    {
        return $this->deletedAt;
    }
}

Implémentation du listener

Un listener va être nécessaire pour venir corrompre le fonctionnement classique de Doctrine.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
namespace Namespace\MyBundle\Listener;
use Doctrine\ORM\Event\PreFlushEventArgs;
class DoctrineListener
{
public function preFlush(PreFlushEventArgs $event) {
$em = $event->getEntityManager();
foreach ($em->getUnitOfWork()->getScheduledEntityDeletions() as $object) {
if (method_exists($object, "getDeletedAt")) {
if ($object->getDeletedAt() instanceof \Datetime) {
continue;
} else {
$object->setDeletedAt(new \DateTime());
$em->merge($object);
$em->persist($object);
}
}
}
}
}

Détaillons rapidement le fonctionnement de cet event listener. Pour chaque objet faisant partie de la liste de suppressions, si l’objet ne possède pas de date dans son champs deletedAt on ajoute le date actuelle. Dans le cas contraire, on doit effectuer une réel suppression et on ne change pas le traitement en cours.

Ce traitement a été déporté dans un listener mais il aura été tout à fait possible d’affecter un événement de type PreRemove dans l’entité et d’effectuer le même traitement.

Enregistrer le listener

1
2
3
4
5
6
7
# src/namespace/mybundle/Resources/config/services.yml
services:
kernel.listener.doctrine:
class: Namespace\MyBundle\Listener\DoctrineListener
public: false
tags:
- { name: doctrine.event_listener, event: preFlush, method: preFlush }

Créer un filtre

Le filtre va nous permettre de facilement cacher les objets supprimés. Ce filtre activé, effectuer un find sur un élément supprimé ne retournera rien. La requête SQL initialement préparé est simplement complété d’une comparaison sur le champs deletedAt.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
namespace Namespace\MyBundle\Repository\Filters;
use Doctrine\ORM\Mapping\ClassMetaData;
use Doctrine\ORM\Query\Filter\SQLFilter;
class DeletedFilter extends SQLFilter
{
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
if ($targetEntity->hasField("deletedAt")) {
$date = date("Y-m-d h:m:s");
return $targetTableAlias.".deletedAt < '".$date."' OR ".$targetTableAlias.".deletedAt IS NULL";
}
return "";
}
}

Ce script est valable pour une base de donnée MySQL mais les changements pour le faire fonctionner avec un autre SGBD comme PostgreSQL, SQL Server et autres.

Configurer le filtre Doctrine pour se lancer systématiquement

Tout est mis en place mais le filtre Doctrine présenté juste au dessus, doit encore être activé volontairement. Puisque supprimer des objets définitivement (supprimer des objets déjà supprimés) est une action que l’on réalise moins souvent, nous voulons activé ce filtre de manière systématique.

1
2
3
4
5
6
7
# app/config/config.yml
doctrine:
orm:
filters:
deleted:
class: 'Namespace\MyBundle\Repository\Filters\DeletedFilter'
enabled: true

Utiliser facilement notre système de Soft Delete

Par défaut, le comportement expliqué dans Intégration à Doctrine est permanent. Si vous voulez toutefois chercher toutes les entités supprimées, voici la manipulation à suivre.

1
$this->container->get("doctrine.orm.default_entity_manager")->getFilters()->disable("deleted");

Avec cette action, le filtre est désactivé et vous pouvez récupérer n’importe quel objet. Une fois un objet récupérer, si celui-ci possède une date dans deletedAt, vous pourrez le supprimer définitivement.

Conclusion

Implémenter son propre Soft Delete est facile à réaliser, facile à comprendre. Cela vous permettra entre autre de limiter l’ajout d’une dépendance à votre projet ! Attention tout de même, la méthode présenté au-dessus couvre 99% des besoins que vous aurez. Gedmo apporte quant à lui la possibilité d’appliquer plus finement (mais également plus couteux) les règles à utiliser. Dans notre cas et avec une code identique à celui présenté, toutes les entités possédant un champs deletedAt ainsi que des accesseurs se verront appliqués une « soft deletion ».

Retrouvez le Gist complet sur GitHub.