浏览代码

[SoftDeleteable] First experimental version.

comfortablynumb 13 年之前
父节点
当前提交
de2736d486

+ 185 - 0
doc/softdeleteable.md

@@ -0,0 +1,185 @@
+# SoftDeleteable behavior extension for Doctrine 2
+
+**SoftDeleteable** behavior allows to "soft delete" objects, filtering them
+at SELECT time, but not really removing them from the DB
+
+Features:
+
+- Works with DQL DELETE queries (using a Query Hint).
+- All SELECT queries will be filtered, not matter from where they are executed (Repositories, DQL SELECT queries, etc).
+- For now, it works only with the ORM
+- Can be nested with other behaviors
+- Annotation, Yaml and Xml mapping support for extensions
+
+Content:
+
+- [Including](#including-extension) the extension
+- Entity [example](#entity-mapping)
+- [Yaml](#yaml-mapping) mapping example
+- [Xml](#xml-mapping) mapping example
+- Usage [examples](#usage)
+
+<a name="including-extension"></a>
+
+## Setup and autoloading
+
+Read the [documentation](http://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/annotations.md#em-setup)
+or check the [example code](http://github.com/l3pp4rd/DoctrineExtensions/tree/master/example)
+on how to setup and use the extensions in most optimized way.
+
+<a name="entity-mapping"></a>
+
+## SoftDeleteable Entity example:
+
+### SoftDeleteable annotations:
+- **@Gedmo\Mapping\Annotation\SoftDeleteable** this class annotation tells if a class is SoftDeleteable. It has a
+mandatory parameter "fieldName", which is the name of the field to be used to hold the known "deletedAt" field. It
+must be of any of the date types.
+
+Available configuration options:
+- **fieldName** - The name of the field that will be used to determine if the object is removed or not (NULL means
+it's not removed. A date value means it was removed). NOTE: The field MUST be nullable.
+
+**Note:** that SoftDeleteable interface is not necessary, except in cases there
+you need to identify entity as being SoftDeleteable. The metadata is loaded only once then
+cache is activated
+
+``` php
+<?php
+namespace Entity;
+
+use Gedmo\Mapping\Annotation as Gedmo;
+use Doctrine\ORM\Mapping as ORM;
+
+/**
+ * @ORM\Entity
+ * @Gedmo\SoftDeleteable(fieldName="deletedAt")
+ */
+class Article
+{
+    /**
+     * @ORM\Column(name="id", type="integer")
+     * @ORM\Id
+     * @ORM\GeneratedValue(strategy="IDENTITY")
+     */
+    private $id;
+
+    /**
+     * @ORM\Column(name="title", type="string")
+     */
+    private $title;
+
+    /**
+     * @ORM\Column(name="deletedAt", type="datetime", nullable=true)
+     */
+    private $deletedAt;
+
+    public function getId()
+    {
+        return $this->id;
+    }
+
+    public function setTitle($title)
+    {
+        $this->title = $title;
+    }
+
+    public function getTitle()
+    {
+        return $this->title;
+    }
+
+    public function getDeletedAt()
+    {
+        return $this->deletedAt;
+    }
+
+    public function setDeletedAt($deletedAt)
+    {
+        return $this->deletedAt;
+    }
+}
+```
+
+<a name="yaml-mapping"></a>
+
+## Yaml mapping example:
+
+Yaml mapped Article: **/mapping/yaml/Entity.Article.dcm.yml**
+
+```
+---
+Entity\Article:
+  type: entity
+  table: articles
+  gedmo:
+    soft_deleteable:
+      field_name: deletedAt
+  id:
+    id:
+      type: integer
+      generator:
+        strategy: AUTO
+  fields:
+    title:
+      type: string
+    deletedAt:
+      type: date
+      nullable: true
+```
+
+<a name="xml-mapping"></a>
+
+## Xml mapping example
+
+``` xml
+<?xml version="1.0" encoding="UTF-8"?>
+<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
+                  xmlns:gedmo="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping">
+
+    <entity name="Mapping\Fixture\Xml\Timestampable" table="timestampables">
+        <id name="id" type="integer" column="id">
+            <generator strategy="AUTO"/>
+        </id>
+
+        <field name="title" type="string" />
+
+        <field name="deletedAt" type="datetime" nullable="true" />
+
+        <gedmo:soft-deleteable field-name="deletedAt" />
+    </entity>
+
+</doctrine-mapping>
+```
+
+<a name="usage"></a>
+
+## Usage:
+
+``` php
+<?php
+$article = new Article;
+$article->setTitle('My Article');
+
+$em->persist($article);
+$em->flush();
+
+// Now if we remove it, it will set the deletedAt field to the actual date
+$em->remove($article);
+$em->flush();
+
+$repo = $em->getRepository('Article');
+$art = $repo->findOneBy(array('title' => 'My Article'));
+
+// It should NOT return the article now
+$this->assertNull($art);
+
+// But if we disable the filter, the article should appear now
+$em->getFilters()->disable('soft-deleteable');
+
+$art = $repo->findOneBy(array('title' => 'My Article'));
+
+$this->assertTrue(is_object($art));
+```
+
+Easy like that, any suggestions on improvements are very welcome.

+ 1 - 0
lib/Gedmo/Mapping/Annotation/All.php

@@ -14,6 +14,7 @@ include __DIR__.'/Language.php';
 include __DIR__.'/Locale.php';
 include __DIR__.'/Loggable.php';
 include __DIR__.'/Slug.php';
+include __DIR__.'/SoftDeleteable.php';
 include __DIR__.'/SortableGroup.php';
 include __DIR__.'/SortablePosition.php';
 include __DIR__.'/Timestampable.php';

+ 0 - 3
lib/Gedmo/Mapping/Annotation/SoftDeleteable.php

@@ -20,7 +20,4 @@ final class SoftDeleteable extends Annotation
 {
     /** @var string */
     public $fieldName = 'deletedAt';
-
-    /** @var bool */
-    public $autoMap = true;
 }

+ 66 - 0
lib/Gedmo/SoftDeleteable/Filter/SoftDeleteableFilter.php

@@ -0,0 +1,66 @@
+<?php
+
+namespace Gedmo\SoftDeleteable\Filter;
+
+use Doctrine\ORM\Mapping\ClassMetaData,
+    Doctrine\ORM\Query\Filter\SQLFilter,
+    Gedmo\SoftDeleteable\SoftDeleteableListener;
+
+/**
+ * The SoftDeleteableFilter adds the condition necessary to
+ * filter entities which were deleted "softly"
+ *
+ * @author Gustavo Falco <comfortablynumb84@gmail.com>
+ * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
+ * @package Gedmo.SoftDeleteable
+ * @subpackage Filter
+ * @link http://www.gediminasm.org
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+class SoftDeleteableFilter extends SQLFilter
+{
+    protected $configuration;
+
+
+    public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
+    {
+        $config = $this->getConfiguration($targetEntity);
+
+        if (!isset($config['softDeleteable']) || !$config['softDeleteable']) {
+            return '';
+        }
+
+        return $targetTableAlias.'.'.$config['fieldName'].' IS NULL';
+    }
+
+    protected function getConfiguration(ClassMetadata $meta)
+    {
+        if (is_null($this->configuration)) {
+            $refl = new \ReflectionProperty('Doctrine\ORM\Query\Filter\SQLFilter', 'em');
+            $refl->setAccessible(true);
+            $em = $refl->getValue($this);
+            $evm = $em->getEventManager();
+
+            foreach ($evm->getListeners() as $listeners) {
+                foreach ($listeners as $listener) {
+                    if ($listener instanceof SoftDeleteableListener) {
+                        $this->configuration = $listener->getConfiguration($em, $meta->name);
+
+                        break;
+                    }
+                }
+
+                if (!is_null($this->configuration)) {
+                    break;
+                }
+            }
+
+            if (is_null($this->configuration)) {
+                throw new \RuntimeException('Listener "SoftDeleteableListener" was not added to the EventManager!');
+            }
+        }
+
+        return $this->configuration;
+    }
+}

+ 9 - 3
lib/Gedmo/SoftDeleteable/Mapping/Driver/Annotation.php

@@ -4,7 +4,8 @@ namespace Gedmo\SoftDeleteable\Mapping\Driver;
 
 use Gedmo\Mapping\Driver\AnnotationDriverInterface,
     Doctrine\Common\Persistence\Mapping\ClassMetadata,
-    Gedmo\Exception\InvalidMappingException;
+    Gedmo\Exception\InvalidMappingException,
+    Gedmo\SoftDeleteable\Mapping\Validator;
 
 /**
  * This is an annotation mapping driver for SoftDeleteable
@@ -50,20 +51,25 @@ class Annotation implements AnnotationDriverInterface
      */
     public function validateFullMetadata(ClassMetadata $meta, array $config)
     {
+        // Nothing here for now
     }
 
     /**
      * {@inheritDoc}
      */
-    public function readExtendedMetadata(ClassMetadata $meta, array &$config)
+    public function readExtendedMetadata($meta, array &$config)
     {
         $class = $meta->getReflectionClass();
         // class annotations
         if ($annot = $this->reader->getClassAnnotation($class, self::SOFT_DELETEABLE)) {
             $config['softDeleteable'] = true;
+
+            Validator::validateField($meta, $annot->fieldName);
+            
             $config['fieldName'] = $annot->fieldName;
-            $config['autoMap'] = $annot->autoMap;
         }
+
+        $this->validateFullMetadata($meta, $config);
     }
 
     /**

+ 52 - 0
lib/Gedmo/SoftDeleteable/Mapping/Driver/Xml.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace Gedmo\SoftDeleteable\Mapping\Driver;
+
+use Gedmo\Mapping\Driver\Xml as BaseXml,
+    Gedmo\Exception\InvalidMappingException,
+    Gedmo\SoftDeleteable\Mapping\Validator;
+
+/**
+ * This is a xml mapping driver for SoftDeleteable
+ * behavioral extension. Used for extraction of extended
+ * metadata from xml specificaly for SoftDeleteable
+ * extension.
+ *
+ * @author Gustavo Falco <comfortablynumb84@gmail.com>
+ * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
+ * @author Miha Vrhovnik <miha.vrhovnik@gmail.com>
+ * @package Gedmo.Timestampable.Mapping.Driver
+ * @subpackage Xml
+ * @link http://www.gediminasm.org
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+class Xml extends BaseXml
+{
+    /**
+     * {@inheritDoc}
+     */
+    public function readExtendedMetadata($meta, array &$config)
+    {
+        /**
+         * @var \SimpleXmlElement $xml
+         */
+        $xml = $this->_getMapping($meta->name);
+        $xmlDoctrine = $xml;
+        $xml = $xml->children(self::GEDMO_NAMESPACE_URI);
+
+        if ($xmlDoctrine->getName() == 'entity' || $xmlDoctrine->getName() == 'mapped-superclass') {
+            if (isset($xmlDoctrine->soft_deleteable)) {
+                $field = $this->_getAttribute($xmlDoctrine, 'field-name');
+
+                if (!$field) {
+                    throw new InvalidMappingException('Field name for SoftDeleteable class is mandatory.');
+                }
+
+                Validator::validateField($meta, $field);
+
+                $config['softDeleteable'] = true;
+                $config['fieldName'] = $field;
+            }
+        }
+    }
+}

+ 63 - 0
lib/Gedmo/SoftDeleteable/Mapping/Driver/Yaml.php

@@ -0,0 +1,63 @@
+<?php
+
+namespace Gedmo\SoftDeleteable\Mapping\Driver;
+
+use Gedmo\Mapping\Driver\File,
+    Gedmo\Mapping\Driver,
+    Gedmo\Exception\InvalidMappingException,
+    Gedmo\SoftDeleteable\Mapping\Validator;
+
+/**
+ * This is a yaml mapping driver for Timestampable
+ * behavioral extension. Used for extraction of extended
+ * metadata from yaml specificaly for Timestampable
+ * extension.
+ *
+ * @author Gustavo Falco <comfortablynumb84@gmail.com>
+ * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
+ * @package Gedmo.SoftDeleteable.Mapping.Driver
+ * @subpackage Yaml
+ * @link http://www.gediminasm.org
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+class Yaml extends File implements Driver
+{
+    /**
+     * File extension
+     * @var string
+     */
+    protected $_extension = '.dcm.yml';
+
+    /**
+     * {@inheritDoc}
+     */
+    public function readExtendedMetadata($meta, array &$config)
+    {
+        $mapping = $this->_getMapping($meta->name);
+
+        if (isset($mapping['gedmo'])) {
+            $classMapping = $mapping['gedmo'];
+            if (isset($classMapping['soft_deleteable'])) {
+                $config['softDeleteable'] = true;
+
+                if (!isset($classMapping['soft_deleteable']['field_name'])) {
+                    throw new InvalidMappingException('Field name for SoftDeleteable class is mandatory.');
+                }
+
+                $fieldName = $classMapping['soft_deleteable']['field_name'];
+
+                Validator::validateField($meta, $fieldName);
+
+                $config['fieldName'] = $fieldName;
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected function _loadMappingFile($file)
+    {
+        return \Symfony\Component\Yaml\Yaml::load($file);
+    }
+}

+ 44 - 0
lib/Gedmo/SoftDeleteable/Mapping/Validator.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace Gedmo\SoftDeleteable\Mapping;
+
+use Doctrine\ORM\Mapping\ClassMetadataInfo;
+use Gedmo\Exception\InvalidMappingException;
+
+/**
+ * This class is used to validate mapping information
+ *
+ * @author Gustavo Falco <comfortablynumb84@gmail.com>
+ * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
+ * @package Gedmo.SoftDeleteable.Mapping
+ * @subpackage Validator
+ * @link http://www.gediminasm.org
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+class Validator 
+{
+    /**
+     * List of types which are valid for timestamp
+     *
+     * @var array
+     */
+    public static $validTypes = array(
+        'date',
+        'time',
+        'datetime',
+        'timestamp',
+        'zenddate'
+    );
+
+
+    public static function validateField(ClassMetadataInfo $meta, $field)
+    {
+        $fieldMapping = $meta->getFieldMapping($field);
+
+        if (!in_array($fieldMapping['type'], self::$validTypes)) {
+            throw new InvalidMappingException(sprintf('Field "%s" must be of one of the following types: "',
+                explode(', ', self::$validTypes)));
+        }
+    }
+}

+ 125 - 0
lib/Gedmo/SoftDeleteable/Query/TreeWalker/SoftDeleteableWalker.php

@@ -0,0 +1,125 @@
+<?php
+
+namespace Gedmo\SoftDeleteable\Query\TreeWalker;
+
+use Doctrine\ORM\Query\SqlWalker;
+use Doctrine\ORM\Query\AST\DeleteStatement;
+use Doctrine\ORM\Query\AST\DeleteClause;
+use Doctrine\ORM\Query\AST\UpdateClause;
+use Doctrine\ORM\Query\AST\UpdateItem;
+use Doctrine\ORM\Query\Exec\SingleTableDeleteUpdateExecutor;
+use Doctrine\ORM\Query\AST\PathExpression;
+use Gedmo\SoftDeleteable\SoftDeleteableListener;
+
+/**
+ * Created by Gustavo Falco <comfortablynumb84@gmail.com>
+ */
+
+class SoftDeleteableWalker extends SqlWalker
+{
+    protected $conn;
+    protected $platform;
+    protected $listener;
+    protected $alias;
+    protected $deletedAtField;
+    protected $meta;
+    
+    /**
+     * {@inheritDoc}
+     */
+    public function __construct($query, $parserResult, array $queryComponents)
+    {
+        parent::__construct($query, $parserResult, $queryComponents);
+        
+        $this->conn = $this->getConnection();
+        $this->platform = $this->conn->getDatabasePlatform();
+        $this->listener = $this->getSoftDeleteableListener();
+        $this->extractComponents($queryComponents);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getExecutor($AST)
+    {
+        if (!$AST instanceof DeleteStatement) {
+            throw new \Gedmo\Exception\UnexpectedValueException('SoftDeleteable walker should be used only on delete statement');
+        }
+        
+        return parent::getExecutor($AST);
+    }
+
+    /**
+     * Change a DELETE clause for an UPDATE clause
+     *
+     * @param DeleteClause
+     * @return string The SQL.
+     */
+    public function walkDeleteClause(DeleteClause $deleteClause)
+    {
+        $em = $this->getEntityManager();
+        $class = $em->getClassMetadata($deleteClause->abstractSchemaName);
+        $tableName = $class->getTableName();
+        $this->setSQLTableAlias($tableName, $tableName, $deleteClause->aliasIdentificationVariable);
+        $quotedTableName = $class->getQuotedTableName($this->platform);
+        $quotedColumnName = $class->getQuotedColumnName($this->deletedAtField, $this->platform);
+        
+        $sql = 'UPDATE '.$quotedTableName.' SET '.$quotedColumnName.' = "'.date('Y-m-d H:i:s').'"';
+
+        return $sql;
+    }
+
+    /**
+     * Get the currently used SoftDeleteableListener
+     *
+     * @throws \Gedmo\Exception\RuntimeException - if listener is not found
+     * @return SoftDeleteableListener
+     */
+    private function getSoftDeleteableListener()
+    {
+        if (is_null($this->listener)) {
+            $em = $this->getEntityManager();
+
+            foreach ($em->getEventManager()->getListeners() as $event => $listeners) {
+                foreach ($listeners as $hash => $listener) {
+                    if ($listener instanceof SoftDeleteableListener) {
+                        $this->listener = $listener;
+                        break;
+                    }
+                }
+                if ($this->listener) {
+                    break;
+                }
+            }
+
+            if (is_null($this->listener)) {
+                throw new \Gedmo\Exception\RuntimeException('The SoftDeleteable listener could not be found.');
+            }
+        }
+
+        return $this->listener;
+    }
+
+    /**
+     * Search for components in the delete clause
+     *
+     * @param array $queryComponents
+     * @return void
+     */
+    private function extractComponents(array $queryComponents)
+    {
+        $em = $this->getEntityManager();
+        
+        foreach ($queryComponents as $alias => $comp) {
+            if (!isset($comp['metadata'])) {
+                continue;
+            }
+            $meta = $comp['metadata'];
+            $config = $this->listener->getConfiguration($em, $meta->name);
+            if ($config && isset($config['softDeleteable']) && $config['softDeleteable']) {
+                $this->deletedAtField = $config['fieldName'];
+                $this->meta = $meta;
+            }
+        }
+    }
+}

+ 27 - 24
lib/Gedmo/SoftDeleteable/SoftDeleteableListener.php

@@ -27,50 +27,53 @@ class SoftDeleteableListener extends MappedEventSubscriber
     {
         return array(
             'loadClassMetadata',
-            'preRemove'
+            'onFlush'
         );
     }
 
     /**
-     * Mapps additional metadata
+     * If it's a SoftDeleteable object, update the "deletedAt" field
+     * and skip the removal of the object
      *
-     * @param EventArgs $eventArgs
+     * @param EventArgs $args
      * @return void
      */
-    public function loadClassMetadata(EventArgs $eventArgs)
+    public function onFlush(EventArgs $args)
     {
-        $ea = $this->getEventAdapter($eventArgs);
+        $ea = $this->getEventAdapter($args);
         $om = $ea->getObjectManager();
-        $meta = $eventArgs->getClassMetadata();
-        $this->loadMetadataForObjectClass($ea->getObjectManager(), $eventArgs->getClassMetadata());
-        $config = isset($this->configurations[$meta->name]) ? $this->configurations[$meta->name] : array();
+        $uow = $om->getUnitOfWork();
         
-        if (isset($config['softDeleteable']) && $config['softDeleteable']) {
-            if ($config['autoMap']) {
-                $meta->mapField(array(
-                     'fieldName'         => $config['fieldName'],
-                     'id'                => false,
-                     'type'              => 'datetime',
-                     'nullable'          => true
-                ));
+        foreach ($uow->getScheduledEntityDeletions() as $entity) {
+            $meta = $om->getClassMetadata(get_class($entity));
+            $config = $this->getConfiguration($om, $meta->name);
+
+            if (isset($config['softDeleteable']) && $config['softDeleteable']) {
+                $reflProp = $meta->getReflectionProperty($config['fieldName']);
+                $reflProp->setAccessible(true);
+                $date = new \DateTime();
+                $oldValue = $reflProp->getValue($entity);
+                $reflProp->setValue($entity, $date);
 
-                if ($cacheDriver = $om->getMetadataFactory()->getCacheDriver()) {
-                    $cacheDriver->save($meta->name."\$CLASSMETADATA", $meta, null);
-                }
+                $om->persist($entity);
+                $uow->propertyChanged($entity, $config['fieldName'], $oldValue, $date);
+                $uow->scheduleExtraUpdate($entity, array(
+                    $config['fieldName'] => array($oldValue, $date)
+                ));
             }
         }
     }
 
     /**
-     * If it's a SoftDeleteable object, update the "deletedAt" field
-     * and skip the removal of the object
+     * Mapps additional metadata
      *
-     * @param EventArgs $args
+     * @param EventArgs $eventArgs
      * @return void
      */
-    public function preRemove(EventArgs $args)
+    public function loadClassMetadata(EventArgs $eventArgs)
     {
-        
+        $ea = $this->getEventAdapter($eventArgs);
+        $this->loadMetadataForObjectClass($ea->getObjectManager(), $eventArgs->getClassMetadata());
     }
 
     /**

+ 22 - 3
tests/Gedmo/SoftDeleteable/Fixture/Entity/Article.php

@@ -4,10 +4,11 @@ namespace SoftDeleteable\Fixture\Entity;
 
 use Gedmo\Mapping\Annotation as Gedmo;
 use Doctrine\ORM\Mapping as ORM;
+use Doctrine\Common\Collections\ArrayCollection;
 
 /**
  * @ORM\Entity
- * @Gedmo\SoftDeleteable
+ * @Gedmo\SoftDeleteable(fieldName="deletedAt")
  */
 class Article
 {
@@ -19,13 +20,26 @@ class Article
     private $id;
 
     /**
-     * @ORM\Column(name="title", type="string", length=8)
+     * @ORM\Column(name="title", type="string")
      */
     private $title;
 
-    /** Mapped by listener */
+    /**
+     * @ORM\Column(name="deletedAt", type="datetime", nullable=true)
+     */
     private $deletedAt;
 
+    /**
+     * @ORM\OneToMany(targetEntity="Comment", mappedBy="article", cascade={"persist", "remove"})
+     */
+    private $comments;
+
+
+    public function __construct()
+    {
+        $this->comments = new ArrayCollection();
+    }
+
     public function getId()
     {
         return $this->id;
@@ -50,4 +64,9 @@ class Article
     {
         return $this->deletedAt;
     }
+
+    public function addComment(Comment $comment)
+    {
+        $this->comments[] = $comment;
+    }
 }

+ 65 - 0
tests/Gedmo/SoftDeleteable/Fixture/Entity/Comment.php

@@ -0,0 +1,65 @@
+<?php
+
+namespace SoftDeleteable\Fixture\Entity;
+
+use Gedmo\Mapping\Annotation as Gedmo;
+use Doctrine\ORM\Mapping as ORM;
+
+/**
+ * @ORM\Entity
+ * @Gedmo\SoftDeleteable(fieldName="deletedAt")
+ */
+class Comment
+{
+    /**
+     * @ORM\Column(name="id", type="integer")
+     * @ORM\Id
+     * @ORM\GeneratedValue(strategy="IDENTITY")
+     */
+    private $id;
+
+    /**
+     * @ORM\Column(name="comment", type="string")
+     */
+    private $comment;
+
+    /**
+     * @ORM\Column(name="deletedAt", type="datetime", nullable=true)
+     */
+    private $deletedAt;
+
+    /**
+     * @ORM\ManyToOne(targetEntity="Article", inversedBy="comments")
+     */
+    private $article;
+
+    public function getId()
+    {
+        return $this->id;
+    }
+
+    public function setComment($comment)
+    {
+        $this->comment = $comment;
+    }
+
+    public function getComment()
+    {
+        return $this->comment;
+    }
+
+    public function setDeletedAt($deletedAt)
+    {
+        $this->deletedAt = $deletedAt;
+    }
+
+    public function getDeletedAt()
+    {
+        return $this->deletedAt;
+    }
+
+    public function getArticle()
+    {
+        return $this->article;
+    }
+}

+ 75 - 10
tests/Gedmo/SoftDeleteable/SoftDeleteableEntityTest.php

@@ -6,6 +6,7 @@ use Tool\BaseTestCaseORM;
 use Doctrine\Common\EventManager;
 use Doctrine\Common\Util\Debug,
     SoftDeleteable\Fixture\Entity\Article,
+    SoftDeleteable\Fixture\Entity\Comment,
     Gedmo\SoftDeleteable\SoftDeleteableListener;
 
 /**
@@ -20,6 +21,8 @@ use Doctrine\Common\Util\Debug,
 class SoftDeleteableEntityTest extends BaseTestCaseORM
 {
     const ARTICLE_CLASS = 'SoftDeleteable\Fixture\Entity\Article';
+    const COMMENT_CLASS = 'SoftDeleteable\Fixture\Entity\Comment';
+    const SOFT_DELETEABLE_FILTER_NAME = 'soft-deleteable';
 
     private $softDeleteableListener;
 
@@ -30,34 +33,96 @@ class SoftDeleteableEntityTest extends BaseTestCaseORM
         $evm = new EventManager;
         $this->softDeleteableListener = new SoftDeleteableListener();
         $evm->addEventSubscriber($this->softDeleteableListener);
-
-        $this->em = $this->getMockSqliteEntityManager($evm);
+        $config = $this->getMockAnnotatedConfig();
+        $config->addFilter(self::SOFT_DELETEABLE_FILTER_NAME, 'Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter');
+        $this->em = $this->getMockSqliteEntityManager($evm, $config);
+        $this->em->getFilters()->enable(self::SOFT_DELETEABLE_FILTER_NAME);
     }
 
     public function testSoftDeleteable()
     {
-        $art0 = new Article();
-        $art0->setTitle('Title 1');
+        $repo = $this->em->getRepository(self::ARTICLE_CLASS);
+        $commentRepo = $this->em->getRepository(self::COMMENT_CLASS);
 
-        $art1 = new Article();
-        $art1->setTitle('Title 2');
+        $comment = new Comment();
+        $commentField = 'comment';
+        $commentValue = 'Comment 1';
+        $comment->setComment($commentValue);
+        $art0 = new Article();
+        $field = 'title';
+        $value = 'Title 1';
+        $art0->setTitle($value);
+        $art0->addComment($comment);
 
         $this->em->persist($art0);
-        $this->em->persist($art1);
 
         $this->em->flush();
         
-        $meta = $this->em->getClassMetadata(self::ARTICLE_CLASS);
+        $art = $repo->findOneBy(array($field => $value));
+
+        $this->assertNull($art->getDeletedAt());
+        $this->assertNull($comment->getDeletedAt());
+
+        $this->em->remove($art);
+        $this->em->flush();
+
+        $art = $repo->findOneBy(array($field => $value));
+        $this->assertNull($art);
+        $comment = $commentRepo->findOneBy(array($commentField => $commentValue));
+        $this->assertNull($comment);
+
+        // Now we deactivate the filter so we test if the entity appears in the result
+        $this->em->getFilters()->disable(self::SOFT_DELETEABLE_FILTER_NAME);
 
-        $this->assertArrayHasKey('deletedAt', $meta->fieldNames);
+        $this->em->clear();
 
+        $art = $repo->findOneBy(array($field => $value));
+        $this->assertTrue(is_object($art));
+        $this->assertTrue(is_object($art->getDeletedAt()));
+        $this->assertTrue($art->getDeletedAt() instanceof \DateTime);
+        $comment = $commentRepo->findOneBy(array($commentField => $commentValue));
+        $this->assertTrue(is_object($comment));
+        $this->assertTrue(is_object($comment->getDeletedAt()));
+        $this->assertTrue($comment->getDeletedAt() instanceof \DateTime);
+
+        $art->setDeletedAt(null);
+        $comment->setDeletedAt(null);
+        $this->em->persist($art);
+        $this->em->flush();
+
+        $this->em->createQuery('UPDATE '.self::ARTICLE_CLASS.' a SET a.deletedAt = NULL')->execute();
+
+        // Now we try with a DQL Delete query
+        $this->em->getFilters()->enable(self::SOFT_DELETEABLE_FILTER_NAME);
+        $dql = sprintf('DELETE FROM %s a WHERE a.%s = :%s',
+            self::ARTICLE_CLASS, $field, $field);
+        $query = $this->em->createQuery($dql);
+        $query->setParameter($field, $value);
+        $query->setHint(
+            \Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER,
+            'Gedmo\SoftDeleteable\Query\TreeWalker\SoftDeleteableWalker'
+        );
+        $query->execute();
+
+        $art = $repo->findOneBy(array($field => $value));
+        $this->assertNull($art);
+
+        // Now we deactivate the filter so we test if the entity appears in the result
+        $this->em->getFilters()->disable(self::SOFT_DELETEABLE_FILTER_NAME);
+        $this->em->clear();
+        
+        $art = $repo->findOneBy(array($field => $value));
         
+        $this->assertTrue(is_object($art));
+        $this->assertTrue(is_object($art->getDeletedAt()));
+        $this->assertTrue($art->getDeletedAt() instanceof \DateTime);
     }
 
     protected function getUsedEntityFixtures()
     {
         return array(
-            self::ARTICLE
+            self::ARTICLE_CLASS,
+            self::COMMENT_CLASS
         );
     }
 

+ 7 - 3
tests/Gedmo/Tool/BaseTestCaseORM.php

@@ -8,11 +8,13 @@ use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
 use Doctrine\ORM\EntityManager;
 use Doctrine\Common\EventManager;
 use Doctrine\ORM\Tools\SchemaTool;
+use Doctrine\ORM\Configuration;
 use Gedmo\Translatable\TranslatableListener;
 use Gedmo\Sluggable\SluggableListener;
 use Gedmo\Tree\TreeListener;
 use Gedmo\Timestampable\TimestampableListener;
 use Gedmo\Loggable\LoggableListener;
+use Gedmo\SoftDeleteable\SoftDeleteableListener;
 
 /**
  * Base test case contains common mock objects
@@ -53,14 +55,14 @@ abstract class BaseTestCaseORM extends \PHPUnit_Framework_TestCase
      * @param EventManager $evm
      * @return EntityManager
      */
-    protected function getMockSqliteEntityManager(EventManager $evm = null)
+    protected function getMockSqliteEntityManager(EventManager $evm = null, Configuration $config = null)
     {
         $conn = array(
             'driver' => 'pdo_sqlite',
             'memory' => true,
         );
 
-        $config = $this->getMockAnnotatedConfig();
+        $config = is_null($config) ? $this->getMockAnnotatedConfig() : $config;
         $em = EntityManager::create($conn, $config, $evm ?: $this->getEventManager());
 
         $schema = array_map(function($class) use ($em) {
@@ -195,6 +197,7 @@ abstract class BaseTestCaseORM extends \PHPUnit_Framework_TestCase
         $evm->addEventSubscriber(new LoggableListener);
         $evm->addEventSubscriber(new TranslatableListener);
         $evm->addEventSubscriber(new TimestampableListener);
+        $evm->addEventSubscriber(new SoftDeleteableListener);
         return $evm;
     }
 
@@ -203,9 +206,10 @@ abstract class BaseTestCaseORM extends \PHPUnit_Framework_TestCase
      *
      * @return Doctrine\ORM\Configuration
      */
-    private function getMockAnnotatedConfig()
+    protected function getMockAnnotatedConfig()
     {
         $config = $this->getMock('Doctrine\ORM\Configuration');
+
         $config
             ->expects($this->once())
             ->method('getProxyDir')