فهرست منبع

Merge pull request #279 from comfortablynumb/soft-deleteable

[WIP] Soft Deleteable
Gediminas Morkevicius 13 سال پیش
والد
کامیت
86fd5ecb2d
29فایلهای تغییر یافته به همراه1593 افزوده شده و 4 حذف شده
  1. 207 0
      doc/softdeleteable.md
  2. 1 0
      lib/Gedmo/Mapping/Annotation/All.php
  3. 23 0
      lib/Gedmo/Mapping/Annotation/SoftDeleteable.php
  4. 66 0
      lib/Gedmo/SoftDeleteable/Filter/SoftDeleteableFilter.php
  5. 85 0
      lib/Gedmo/SoftDeleteable/Mapping/Driver/Annotation.php
  6. 52 0
      lib/Gedmo/SoftDeleteable/Mapping/Driver/Xml.php
  7. 63 0
      lib/Gedmo/SoftDeleteable/Mapping/Driver/Yaml.php
  8. 44 0
      lib/Gedmo/SoftDeleteable/Mapping/Validator.php
  9. 51 0
      lib/Gedmo/SoftDeleteable/Query/TreeWalker/Exec/MultiTableDeleteExecutor.php
  10. 142 0
      lib/Gedmo/SoftDeleteable/Query/TreeWalker/SoftDeleteableWalker.php
  11. 30 0
      lib/Gedmo/SoftDeleteable/SoftDeleteable.php
  12. 85 0
      lib/Gedmo/SoftDeleteable/SoftDeleteableListener.php
  13. 5 0
      schemas/orm/doctrine-extensions-mapping-2-2.xsd
  14. 14 0
      tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.SoftDeleteable.dcm.xml
  15. 16 0
      tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.SoftDeleteable.dcm.yml
  16. 55 0
      tests/Gedmo/Mapping/Fixture/SoftDeleteable.php
  17. 10 0
      tests/Gedmo/Mapping/Fixture/Xml/SoftDeleteable.php
  18. 10 0
      tests/Gedmo/Mapping/Fixture/Yaml/SoftDeleteable.php
  19. 67 0
      tests/Gedmo/Mapping/SoftDeleteableMappingTest.php
  20. 67 0
      tests/Gedmo/Mapping/Xml/SoftDeleteableMappingTest.php
  21. 72 0
      tests/Gedmo/SoftDeleteable/Fixture/Entity/Article.php
  22. 65 0
      tests/Gedmo/SoftDeleteable/Fixture/Entity/Comment.php
  23. 13 0
      tests/Gedmo/SoftDeleteable/Fixture/Entity/MegaPage.php
  24. 70 0
      tests/Gedmo/SoftDeleteable/Fixture/Entity/Module.php
  25. 75 0
      tests/Gedmo/SoftDeleteable/Fixture/Entity/Page.php
  26. 179 0
      tests/Gedmo/SoftDeleteable/SoftDeleteableEntityTest.php
  27. 22 4
      tests/Gedmo/Tool/BaseTestCaseORM.php
  28. 1 0
      tests/bootstrap.php
  29. 3 0
      tests/phpunit.xml.dist

+ 207 - 0
doc/softdeleteable.md

@@ -0,0 +1,207 @@
+# 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.
+
+With SoftDeleteable there's one more step you need to do. You need to add the filter to your configuration:
+
+``` php
+
+$config = new Doctrine\ORM\Configuration;
+
+// Your configs..
+
+$config->addFilter('soft-deleteable', 'Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter');
+```
+
+And then you can access the filter from your EntityManager to enable or disable it with the following code:
+
+``` php
+// This will enable the SoftDeleteable filter, so entities which were "soft-deleted" will not appear
+// in results
+$em->getFilters()->enable('soft-deleteable');
+
+// This will disable the SoftDeleteable filter, so entities which were "soft-deleted" will appear in results
+$em->getFilters()->disable('soft-deleteable');
+```
+
+<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';

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

@@ -0,0 +1,23 @@
+<?php
+
+namespace Gedmo\Mapping\Annotation;
+
+use Doctrine\Common\Annotations\Annotation;
+
+/**
+ * Group annotation for SoftDeleteable extension
+ *
+ * @author Gustavo Falco <comfortablynumb84@gmail.com>
+ * @package Gedmo.Mapping.Annotation
+ * @subpackage SoftDeleteable
+ * @link http://www.gediminasm.org
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ *
+ * @Annotation
+ * @Target("CLASS")
+ */
+final class SoftDeleteable extends Annotation
+{
+    /** @var string */
+    public $fieldName = 'deletedAt';
+}

+ 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 ($this->configuration === null) {
+            $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 ($this->configuration === null) {
+                    break;
+                }
+            }
+
+            if ($this->configuration === null) {
+                throw new \RuntimeException('Listener "SoftDeleteableListener" was not added to the EventManager!');
+            }
+        }
+
+        return $this->configuration;
+    }
+}

+ 85 - 0
lib/Gedmo/SoftDeleteable/Mapping/Driver/Annotation.php

@@ -0,0 +1,85 @@
+<?php
+
+namespace Gedmo\SoftDeleteable\Mapping\Driver;
+
+use Gedmo\Mapping\Driver\AnnotationDriverInterface,
+    Doctrine\Common\Persistence\Mapping\ClassMetadata,
+    Gedmo\Exception\InvalidMappingException,
+    Gedmo\SoftDeleteable\Mapping\Validator;
+
+/**
+ * This is an annotation mapping driver for SoftDeleteable
+ * behavioral extension. Used for extraction of extended
+ * metadata from Annotations specificaly for SoftDeleteable
+ * extension.
+ *
+ * @author Gustavo Falco <comfortablynumb84@gmail.com>
+ * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
+ * @package Gedmo.SoftDeleteable.Mapping.Driver
+ * @subpackage Annotation
+ * @link http://www.gediminasm.org
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+class Annotation implements AnnotationDriverInterface
+{
+    /**
+     * Annotation to define that this object is loggable
+     */
+    const SOFT_DELETEABLE = 'Gedmo\\Mapping\\Annotation\\SoftDeleteable';
+
+    /**
+     * Annotation reader instance
+     *
+     * @var object
+     */
+    private $reader;
+
+    /**
+     * original driver if it is available
+     */
+    protected $_originalDriver = null;
+    /**
+     * {@inheritDoc}
+     */
+    public function setAnnotationReader($reader)
+    {
+        $this->reader = $reader;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function validateFullMetadata(ClassMetadata $meta, array $config)
+    {
+        // Nothing here for now
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    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;
+        }
+
+        $this->validateFullMetadata($meta, $config);
+    }
+
+    /**
+     * Passes in the mapping read by original driver
+     *
+     * @param $driver
+     * @return void
+     */
+    public function setOriginalDriver($driver)
+    {
+        $this->_originalDriver = $driver;
+    }
+}

+ 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($xml->{'soft-deleteable'})) {
+                $field = $this->_getAttribute($xml->{'soft-deleteable'}, '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)));
+        }
+    }
+}

+ 51 - 0
lib/Gedmo/SoftDeleteable/Query/TreeWalker/Exec/MultiTableDeleteExecutor.php

@@ -0,0 +1,51 @@
+<?php
+
+namespace Gedmo\SoftDeleteable\Query\TreeWalker\Exec;
+
+use Doctrine\ORM\Query\Exec\MultiTableDeleteExecutor as BaseMultiTableDeleteExecutor;
+use Doctrine\ORM\Query\AST\Node;
+use Doctrine\ORM\Mapping\ClassMetadataInfo;
+use Doctrine\DBAL\Platforms\AbstractPlatform;
+
+/**
+ * This class is used when a DELETE DQL query is called for entities
+ * that are part of an inheritance tree
+ *
+ * @author Gustavo Falco <comfortablynumb84@gmail.com>
+ * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
+ * @package Gedmo.Query.TreeWalker.Exec
+ * @subpackage MultiTableDeleteExecutor
+ * @link http://www.gediminasm.org
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+class MultiTableDeleteExecutor extends BaseMultiTableDeleteExecutor
+{
+    /**
+     * {@inheritDoc}
+     */
+    public function __construct(Node $AST, $sqlWalker, ClassMetadataInfo $meta, AbstractPlatform $platform, array $config)
+    {
+        parent::__construct($AST, $sqlWalker);
+
+        $reflProp = new \ReflectionProperty(get_class($this), '_sqlStatements');
+        $reflProp->setAccessible(true);
+
+        $sqlStatements = $reflProp->getValue($this);
+
+        foreach ($sqlStatements as $index => $stmt) {
+            $matches = array();
+            preg_match('/DELETE FROM (\w+) .+/', $stmt, $matches);
+
+            if (isset($matches[1]) && $meta->getQuotedTableName($platform) === $matches[1]) {
+                $sqlStatements[$index] = str_replace('DELETE FROM', 'UPDATE', $stmt);;
+                $sqlStatements[$index] = str_replace('WHERE', 'SET '.$config['fieldName'].' = "'.date('Y-m-d H:i:s').'" WHERE', $sqlStatements[$index]);
+            } else {
+                // We have to avoid the removal of registers of child entities of a SoftDeleteable entity
+                unset($sqlStatements[$index]);
+            }
+        }
+
+        $reflProp->setValue($this, $sqlStatements);
+    }
+}

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

@@ -0,0 +1,142 @@
+<?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;
+use Gedmo\SoftDeleteable\Query\TreeWalker\Exec\MultiTableDeleteExecutor;
+
+/**
+ * This SqlWalker is needed when you need to use a DELETE DQL query.
+ * It will update the "deletedAt" field with the actual date, instead
+ * of actually deleting it.
+ *
+ * @author Gustavo Falco <comfortablynumb84@gmail.com>
+ * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
+ * @package Gedmo.Query.TreeWalker
+ * @subpackage SoftDeleteableWalker
+ * @link http://www.gediminasm.org
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+class SoftDeleteableWalker extends SqlWalker
+{
+    protected $conn;
+    protected $platform;
+    protected $listener;
+    protected $configuration;
+    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)
+    {
+        switch (true) {
+            case ($AST instanceof DeleteStatement):
+                $primaryClass = $this->getEntityManager()->getClassMetadata($AST->deleteClause->abstractSchemaName);
+
+                return ($primaryClass->isInheritanceTypeJoined())
+                    ? new MultiTableDeleteExecutor($AST, $this, $this->meta, $this->platform, $this->configuration)
+                    : new SingleTableDeleteUpdateExecutor($AST, $this);
+            default:
+                throw new \Gedmo\Exception\UnexpectedValueException('SoftDeleteable walker should be used only on delete statement');
+        }
+    }
+
+    /**
+     * 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->configuration = $config;
+                $this->deletedAtField = $config['fieldName'];
+                $this->meta = $meta;
+            }
+        }
+    }
+}

+ 30 - 0
lib/Gedmo/SoftDeleteable/SoftDeleteable.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace Gedmo\SoftDeleteable;
+
+/**
+ * This interface is not necessary but can be implemented for
+ * Domain Objects which in some cases needs to be identified as
+ * SoftDeleteable
+ *
+ * @author Gustavo Falco <comfortablynumb84@gmail.com>
+ * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
+ * @package Gedmo.SoftDeleteable
+ * @subpackage SoftDeleteable
+ * @link http://www.gediminasm.org
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+interface SoftDeleteable
+{
+    // this interface is not necessary to implement
+    
+    /**
+     * @gedmo:SoftDeleteable
+     * to mark the class as SoftDeleteable use class annotation @gedmo:SoftDeleteable
+     * this object will be able to be soft deleted
+     * example:
+     * 
+     * @gedmo:SoftDeleteable
+     * class MyEntity
+     */
+}

+ 85 - 0
lib/Gedmo/SoftDeleteable/SoftDeleteableListener.php

@@ -0,0 +1,85 @@
+<?php
+
+namespace Gedmo\SoftDeleteable;
+
+use Doctrine\Common\Persistence\ObjectManager,
+    Doctrine\Common\Persistence\Mapping\ClassMetadata,
+    Gedmo\Mapping\MappedEventSubscriber,
+    Gedmo\Loggable\Mapping\Event\LoggableAdapter,
+    Doctrine\Common\EventArgs;
+
+/**
+ * SoftDeleteable listener
+ *
+ * @author Gustavo Falco <comfortablynumb84@gmail.com>
+ * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
+ * @package Gedmo.SoftDeleteable
+ * @subpackage SoftDeleteableListener
+ * @link http://www.gediminasm.org
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+class SoftDeleteableListener extends MappedEventSubscriber
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function getSubscribedEvents()
+    {
+        return array(
+            'loadClassMetadata',
+            'onFlush'
+        );
+    }
+
+    /**
+     * If it's a SoftDeleteable object, update the "deletedAt" field
+     * and skip the removal of the object
+     *
+     * @param EventArgs $args
+     * @return void
+     */
+    public function onFlush(EventArgs $args)
+    {
+        $ea = $this->getEventAdapter($args);
+        $om = $ea->getObjectManager();
+        $uow = $om->getUnitOfWork();
+        
+        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']);
+                $date = new \DateTime();
+                $oldValue = $reflProp->getValue($entity);
+                $reflProp->setValue($entity, $date);
+
+                $om->persist($entity);
+                $uow->propertyChanged($entity, $config['fieldName'], $oldValue, $date);
+                $uow->scheduleExtraUpdate($entity, array(
+                    $config['fieldName'] => array($oldValue, $date)
+                ));
+            }
+        }
+    }
+
+    /**
+     * Mapps additional metadata
+     *
+     * @param EventArgs $eventArgs
+     * @return void
+     */
+    public function loadClassMetadata(EventArgs $eventArgs)
+    {
+        $ea = $this->getEventAdapter($eventArgs);
+        $this->loadMetadataForObjectClass($ea->getObjectManager(), $eventArgs->getClassMetadata());
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected function getNamespace()
+    {
+        return __NAMESPACE__;
+    }
+}

+ 5 - 0
schemas/orm/doctrine-extensions-mapping-2-2.xsd

@@ -26,6 +26,7 @@ people to push their own additional attributes/elements into the same field elem
   <xs:element name="tree" type="gedmo:tree"/>
   <xs:element name="tree-closure" type="gedmo:tree-closure"/>
   <xs:element name="loggable" type="gedmo:loggable"/>
+  <xs:element name="soft-deleteable" type="gedmo:soft-deleteable"/>
   <!-- field -->
   <xs:element name="slug" type="gedmo:slug"/>
   <xs:element name="translatable" type="gedmo:emptyType"/>
@@ -67,6 +68,10 @@ people to push their own additional attributes/elements into the same field elem
     <xs:attribute name="separator" type="xs:string" use="optional" />
     <xs:attribute name="style" type="gedmo:slug-style" use="optional" />
   </xs:complexType>
+
+  <xs:complexType name="soft-deleteable">
+    <xs:attribute name="field-name" type="xs:string" use="required" />
+  </xs:complexType>
   
   <xs:complexType name="handler">
     <xs:sequence>

+ 14 - 0
tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.SoftDeleteable.dcm.xml

@@ -0,0 +1,14 @@
+<?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\SoftDeleteable" table="soft_deleteables">
+        <id name="id" type="integer" column="id">
+            <generator strategy="AUTO"/>
+        </id>
+
+        <field name="deletedAt" type="datetime" nullable="true"/>
+
+        <gedmo:soft-deleteable field-name="deletedAt"/>
+    </entity>
+</doctrine-mapping>

+ 16 - 0
tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.SoftDeleteable.dcm.yml

@@ -0,0 +1,16 @@
+---
+Mapping\Fixture\Yaml\SoftDeleteable:
+  type: entity
+  table: soft_deleteables
+  gedmo:
+    soft_deleteable:
+      field_name: deletedAt
+  id:
+    id:
+      type: integer
+      generator:
+        strategy: AUTO
+  fields:
+    deletedAt:
+      type: datetime
+      nullable: true

+ 55 - 0
tests/Gedmo/Mapping/Fixture/SoftDeleteable.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace Mapping\Fixture;
+
+use Gedmo\Mapping\Annotation as Gedmo;
+use Doctrine\ORM\Mapping as ORM;
+
+/**
+ * @ORM\Entity
+ * @Gedmo\SoftDeleteable(fieldName="deletedAt")
+ */
+class SoftDeleteable
+{
+    /**
+     * @ORM\Id
+     * @ORM\GeneratedValue
+     * @ORM\Column(type="integer")
+     */
+    private $id;
+
+    /**
+     * @ORM\Column(name="deleted_at", 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 setCode($code)
+    {
+        $this->code = $code;
+    }
+
+    public function getCode()
+    {
+        return $this->code;
+    }
+
+    public function getSlug()
+    {
+        return $this->slug;
+    }
+}

+ 10 - 0
tests/Gedmo/Mapping/Fixture/Xml/SoftDeleteable.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace Mapping\Fixture\Xml;
+
+class SoftDeleteable
+{
+    private $id;
+
+    private $deletedAt;
+}

+ 10 - 0
tests/Gedmo/Mapping/Fixture/Yaml/SoftDeleteable.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace Mapping\Fixture\Yaml;
+
+class SoftDeleteable
+{
+    private $id;
+
+    private $deletedAt;
+}

+ 67 - 0
tests/Gedmo/Mapping/SoftDeleteableMappingTest.php

@@ -0,0 +1,67 @@
+<?php
+
+namespace Gedmo\Mapping;
+
+use Doctrine\Common\Annotations\AnnotationReader;
+use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
+use Doctrine\Common\EventManager;
+use Doctrine\ORM\Mapping\Driver\DriverChain;
+use Doctrine\ORM\Mapping\Driver\YamlDriver;
+use Gedmo\SoftDeleteable\SoftDeleteableListener;
+use Tool\BaseTestCaseOM;
+
+/**
+ * These are mapping tests for SoftDeleteable extension
+ *
+ * @author Gustavo Falco <comfortablynumb84@gmail.com>
+ * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
+ * @package Gedmo.Mapping
+ * @link http://www.gediminasm.org
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+class SoftDeleteableMappingTest extends BaseTestCaseOM
+{
+    /**
+     * @var Doctrine\ORM\EntityManager
+     */
+    private $em;
+
+    /**
+     * @var Gedmo\SoftDeleteable\SoftDeleteableListener
+     */
+    private $softDeleteable;
+
+    public function setUp()
+    {
+        parent::setUp();
+        
+        $reader = new AnnotationReader();
+        $annotationDriver = new AnnotationDriver($reader);
+
+        $yamlDriver = new YamlDriver(__DIR__.'/Driver/Yaml');
+
+        $chain = new DriverChain;
+        $chain->addDriver($yamlDriver, 'Mapping\Fixture\Yaml');
+        $chain->addDriver($annotationDriver, 'Mapping\Fixture');
+
+        $this->softDeleteable = new SoftDeleteableListener();
+        $this->evm = new EventManager;
+        $this->evm->addEventSubscriber($this->softDeleteable);
+
+        $this->em = $this->getMockSqliteEntityManager(array(
+            'Mapping\Fixture\Yaml\SoftDeleteable',
+            'Mapping\Fixture\SoftDeleteable'
+        ), $chain);
+    }
+
+    public function testYamlMapping()
+    {
+        $meta = $this->em->getClassMetadata('Mapping\Fixture\Yaml\SoftDeleteable');
+        $config = $this->softDeleteable->getConfiguration($this->em, $meta->name);
+        
+        $this->assertArrayHasKey('softDeleteable', $config);
+        $this->assertTrue($config['softDeleteable']);
+        $this->assertArrayHasKey('fieldName', $config);
+        $this->assertEquals('deletedAt', $config['fieldName']);
+    }
+}

+ 67 - 0
tests/Gedmo/Mapping/Xml/SoftDeleteableMappingTest.php

@@ -0,0 +1,67 @@
+<?php
+
+namespace Gedmo\Mapping\Xml;
+
+use Doctrine\Common\Annotations\AnnotationReader;
+use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
+use Doctrine\Common\EventManager;
+use Doctrine\ORM\Mapping\Driver\DriverChain;
+use Doctrine\ORM\Mapping\Driver\XmlDriver;
+use Gedmo\SoftDeleteable\SoftDeleteableListener;
+use Tool\BaseTestCaseOM;
+
+/**
+ * These are mapping tests for SoftDeleteable extension
+ *
+ * @author Gustavo Falco <comfortablynumb84@gmail.com>
+ * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
+ * @package Gedmo.Mapping
+ * @link http://www.gediminasm.org
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+class SoftDeleteableMappingTest extends BaseTestCaseOM
+{
+    /**
+     * @var Doctrine\ORM\EntityManager
+     */
+    private $em;
+
+    /**
+     * @var Gedmo\SoftDeleteable\SoftDeleteableListener
+     */
+    private $softDeleteable;
+
+    public function setUp()
+    {
+        parent::setUp();
+        
+        $reader = new AnnotationReader();
+        $annotationDriver = new AnnotationDriver($reader);
+
+        $xmlDriver = new XmlDriver(__DIR__.'/../Driver/Xml');
+
+        $chain = new DriverChain;
+        $chain->addDriver($xmlDriver, 'Mapping\Fixture\Xml');
+        $chain->addDriver($annotationDriver, 'Mapping\Fixture');
+
+        $this->softDeleteable = new SoftDeleteableListener;
+        $this->evm = new EventManager;
+        $this->evm->addEventSubscriber($this->softDeleteable);
+
+        $this->em = $this->getMockSqliteEntityManager(array(
+            'Mapping\Fixture\Xml\SoftDeleteable',
+            'Mapping\Fixture\SoftDeleteable'
+        ), $chain);
+    }
+
+    public function testMetadata()
+    {
+        $meta = $this->em->getClassMetadata('Mapping\Fixture\Xml\SoftDeleteable');
+        $config = $this->softDeleteable->getConfiguration($this->em, $meta->name);
+
+        $this->assertArrayHasKey('softDeleteable', $config);
+        $this->assertTrue($config['softDeleteable']);
+        $this->assertArrayHasKey('fieldName', $config);
+        $this->assertEquals('deletedAt', $config['fieldName']);
+    }
+}

+ 72 - 0
tests/Gedmo/SoftDeleteable/Fixture/Entity/Article.php

@@ -0,0 +1,72 @@
+<?php
+
+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(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;
+
+    /**
+     * @ORM\OneToMany(targetEntity="Comment", mappedBy="article", cascade={"persist", "remove"})
+     */
+    private $comments;
+
+
+    public function __construct()
+    {
+        $this->comments = new ArrayCollection();
+    }
+
+    public function getId()
+    {
+        return $this->id;
+    }
+
+    public function setTitle($title)
+    {
+        $this->title = $title;
+    }
+
+    public function getTitle()
+    {
+        return $this->title;
+    }
+
+    public function setDeletedAt($deletedAt)
+    {
+        $this->deletedAt = $deletedAt;
+    }
+
+    public function getDeletedAt()
+    {
+        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;
+    }
+}

+ 13 - 0
tests/Gedmo/SoftDeleteable/Fixture/Entity/MegaPage.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace SoftDeleteable\Fixture\Entity;
+
+use Doctrine\ORM\Mapping as ORM;
+use Doctrine\Common\Collections\ArrayCollection;
+
+/**
+ * @ORM\Entity
+ */
+class MegaPage extends Page
+{
+}

+ 70 - 0
tests/Gedmo/SoftDeleteable/Fixture/Entity/Module.php

@@ -0,0 +1,70 @@
+<?php
+
+namespace SoftDeleteable\Fixture\Entity;
+
+use Gedmo\Mapping\Annotation as Gedmo;
+use Doctrine\ORM\Mapping as ORM;
+
+/**
+ * @ORM\Entity
+ * @Gedmo\SoftDeleteable(fieldName="deletedAt")
+ */
+class Module
+{
+    /**
+     * @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;
+
+    /**
+     * @ORM\ManyToOne(targetEntity="Page", inversedBy="modules")
+     */
+    private $page;
+
+    public function getId()
+    {
+        return $this->id;
+    }
+
+    public function setPage(Page $page)
+    {
+        $this->page = $page;
+    }
+
+    public function getPage()
+    {
+        return $this->page;
+    }
+
+    public function setDeletedAt($deletedAt)
+    {
+        $this->deletedAt = $deletedAt;
+    }
+
+    public function getDeletedAt()
+    {
+        return $this->deletedAt;
+    }
+
+    public function setTitle($title)
+    {
+        $this->title = $title;
+    }
+
+    public function getTitle()
+    {
+        return $this->title;
+    }
+}

+ 75 - 0
tests/Gedmo/SoftDeleteable/Fixture/Entity/Page.php

@@ -0,0 +1,75 @@
+<?php
+
+namespace SoftDeleteable\Fixture\Entity;
+
+use Gedmo\Mapping\Annotation as Gedmo;
+use Doctrine\ORM\Mapping as ORM;
+use Doctrine\Common\Collections\ArrayCollection;
+
+/**
+ * @ORM\Entity
+ * @ORM\InheritanceType("JOINED")
+ * @ORM\DiscriminatorColumn(name="discr", type="string")
+ * @ORM\DiscriminatorMap({"page" = "Page", "mega_page" = "MegaPage"})
+ * @Gedmo\SoftDeleteable(fieldName="deletedAt")
+ */
+class Page
+{
+    /**
+     * @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;
+
+    /**
+     * @ORM\OneToMany(targetEntity="Module", mappedBy="page", cascade={"persist", "remove"})
+     */
+    private $modules;
+
+
+    public function __construct()
+    {
+        $this->modules = new ArrayCollection();
+    }
+
+    public function getId()
+    {
+        return $this->id;
+    }
+
+    public function setTitle($title)
+    {
+        $this->title = $title;
+    }
+
+    public function getTitle()
+    {
+        return $this->title;
+    }
+
+    public function setDeletedAt($deletedAt)
+    {
+        $this->deletedAt = $deletedAt;
+    }
+
+    public function getDeletedAt()
+    {
+        return $this->deletedAt;
+    }
+
+    public function addModule(Module $module)
+    {
+        $this->module[] = $module;
+    }
+}

+ 179 - 0
tests/Gedmo/SoftDeleteable/SoftDeleteableEntityTest.php

@@ -0,0 +1,179 @@
+<?php
+
+namespace Gedmo\SoftDeleteable;
+
+use Tool\BaseTestCaseORM;
+use Doctrine\Common\EventManager;
+use Doctrine\Common\Util\Debug,
+    SoftDeleteable\Fixture\Entity\Article,
+    SoftDeleteable\Fixture\Entity\Comment,
+    SoftDeleteable\Fixture\Entity\Page,
+    SoftDeleteable\Fixture\Entity\MegaPage,
+    SoftDeleteable\Fixture\Entity\Module,
+    Gedmo\SoftDeleteable\SoftDeleteableListener;
+
+/**
+ * These are tests for SoftDeleteable behavior
+ *
+ * @author Gustavo Falco <comfortablynumb84@gmail.com>
+ * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
+ * @package Gedmo.SoftDeleteable
+ * @link http://www.gediminasm.org
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+class SoftDeleteableEntityTest extends BaseTestCaseORM
+{
+    const ARTICLE_CLASS = 'SoftDeleteable\Fixture\Entity\Article';
+    const COMMENT_CLASS = 'SoftDeleteable\Fixture\Entity\Comment';
+    const PAGE_CLASS = 'SoftDeleteable\Fixture\Entity\Page';
+    const MEGA_PAGE_CLASS = 'SoftDeleteable\Fixture\Entity\MegaPage';
+    const MODULE_CLASS = 'SoftDeleteable\Fixture\Entity\Module';
+    const SOFT_DELETEABLE_FILTER_NAME = 'soft-deleteable';
+
+    private $softDeleteableListener;
+
+    protected function setUp()
+    {
+        parent::setUp();
+
+        $evm = new EventManager;
+        $this->softDeleteableListener = new SoftDeleteableListener();
+        $evm->addEventSubscriber($this->softDeleteableListener);
+        $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()
+    {
+        $repo = $this->em->getRepository(self::ARTICLE_CLASS);
+        $commentRepo = $this->em->getRepository(self::COMMENT_CLASS);
+
+        $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->flush();
+        
+        $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->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);
+
+        $this->em->createQuery('UPDATE '.self::ARTICLE_CLASS.' a SET a.deletedAt = NULL')->execute();
+
+        $this->em->refresh($art);
+        $this->em->refresh($comment);
+
+        // 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);
+
+
+        // Inheritance tree DELETE DQL
+        $this->em->getFilters()->enable(self::SOFT_DELETEABLE_FILTER_NAME);
+        
+        $megaPageRepo = $this->em->getRepository(self::MEGA_PAGE_CLASS);
+        $module = new Module();
+        $module->setTitle('Module 1');
+        $page = new MegaPage();
+        $page->setTitle('Page 1');
+        $page->addModule($module);
+        $module->setPage($page);
+
+        $this->em->persist($page);
+        $this->em->persist($module);
+        $this->em->flush();
+        
+        $dql = sprintf('DELETE FROM %s p',
+            self::PAGE_CLASS);
+        $query = $this->em->createQuery($dql);
+        $query->setHint(
+            \Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER,
+            'Gedmo\SoftDeleteable\Query\TreeWalker\SoftDeleteableWalker'
+        );
+
+        $query->execute();
+
+        $p = $megaPageRepo->findOneBy(array('title' => 'Page 1'));
+        $this->assertNull($p);
+
+        // 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();
+
+        $p = $megaPageRepo->findOneBy(array('title' => 'Page 1'));
+
+        $this->assertTrue(is_object($p));
+        $this->assertTrue(is_object($p->getDeletedAt()));
+        $this->assertTrue($p->getDeletedAt() instanceof \DateTime);
+    }
+
+    protected function getUsedEntityFixtures()
+    {
+        return array(
+            self::ARTICLE_CLASS,
+            self::PAGE_CLASS,
+            self::MEGA_PAGE_CLASS,
+            self::MODULE_CLASS,
+            self::COMMENT_CLASS
+        );
+    }
+
+    private function populate()
+    {
+        
+    }
+}

+ 22 - 4
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,24 @@ abstract class BaseTestCaseORM extends \PHPUnit_Framework_TestCase
      *
      * @return Doctrine\ORM\Configuration
      */
-    private function getMockAnnotatedConfig()
+    protected function getMockAnnotatedConfig()
     {
-        $config = $this->getMock('Doctrine\ORM\Configuration');
+        // We need to mock every method except the ones which
+        // handle the filters
+        $configurationClass = 'Doctrine\ORM\Configuration';
+        $refl = new \ReflectionClass($configurationClass);
+        $methods = $refl->getMethods();
+
+        $mockMethods = array();
+
+        foreach ($methods as $method) {
+            if ($method->name !== 'addFilter' && $method->name !== 'getFilterClassName') {
+                $mockMethods[] = $method->name;
+            }
+        }
+        
+        $config = $this->getMock($configurationClass, $mockMethods);
+
         $config
             ->expects($this->once())
             ->method('getProxyDir')

+ 1 - 0
tests/bootstrap.php

@@ -48,6 +48,7 @@ $loader->registerNamespaces(array(
     'Sortable\\Fixture'          => __DIR__.'/Gedmo',
     'Mapping\\Fixture'           => __DIR__.'/Gedmo',
     'Loggable\\Fixture'          => __DIR__.'/Gedmo',
+    'SoftDeleteable\\Fixture'    => __DIR__.'/Gedmo',
     'Wrapper\\Fixture'           => __DIR__.'/Gedmo',
 ));
 $loader->register();

+ 3 - 0
tests/phpunit.xml.dist

@@ -41,5 +41,8 @@
         <testsuite name="Translator extension">
             <directory suffix=".php">./Gedmo/Translator/</directory>
         </testsuite>
+        <testsuite name="SoftDeleteable Extension">
+            <directory suffix=".php">./Gedmo/SoftDeleteable/</directory>
+        </testsuite>
     </testsuites>
 </phpunit>