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.
Sommaire
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
|
namespace NamespaceMyBundleEntity;
use DoctrineORMMapping as ORM;
class Entity
{
/**
* @var datetime $deletedAt
*
* @ORMColumn(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
|
namespace NamespaceMyBundleListener;
use DoctrineORMEventPreFlushEventArgs;
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: NamespaceMyBundleListenerDoctrineListener
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
|
namespace NamespaceMyBundleRepositoryFilters;
use DoctrineORMMappingClassMetaData;
use DoctrineORMQueryFilterSQLFilter;
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: ‘NamespaceMyBundleRepositoryFiltersDeletedFilter’
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 ».