How to create Doctrine 1 -style Soft-Delete in Doctrine 2

December 4, 2010 – 7:48 pm Tags:

Doctrine 1 has the concept of behaviors which you could add to your models. One of these was the soft-delete behavior, which allowed you to “delete” records without really deleting them.

Doctrine 2 does not have behaviors due to various reasons. However, I needed a way to have a model which worked like soft-delete.

Let’s see one approach to creating such behavior in Doctrine 2.

Soft-delete basics

Simply put, all we need for soft-delete is a flag in the table – the deleted flag. This will be used to tell whether a record has been deleted or not.

In practice it’s a bit more involved than that, as you need to make sure all your queries work accordingly. Queries which fetch listings must make sure to only fetch lists of things that haven’t been deleted and queries deleting things must be sure to not execute any DELETE statements.

Doctrine 2 custom repositories

The most straightforward way to create a entity type in Doctrine 2 which works in a soft-delete style manner is to create a custom repository.

The idea is that you should access your database through the repository. This is similar to other approaches for abstracting database access: The repository hides the detail of what is used to access the DB, and thus you can easily change the plumbing later if needed.

By using the repository we can also inject custom behavior in queries, such as soft-delete.

Creating a soft-delete enabled repository

Let’s say we have an example entity called… Example. Creative, don’t you think?

/**
 * @Entity(repositoryClass="ExampleRepository")
 */
class Example {
    //Let's assume there's some fields here
 
    /**
     * @Column(type="boolean")
     */
    private $deleted;
 
    public function delete() {
        $this->deleted = true;
    }
 
    public function isDeleted() {
        return $this->deleted;
    }
}

First, note we declared a custom repository class in the entity annotation. Then, we have a boolean mapping for a deleted field, a method to mark the entity deleted and a method to get the deleted value. You might also want to add a method for undeleting entities if you find it necessary.

Next, we’ll need the repository:

class ExampleRepository extends \Doctrine\ORM\EntityRepository {
    public function findBy(array $criteria) {
    }
 
    public function findOneBy(array $criteria) {
    }
 
    public function find($id, $lockMode = \Doctrine\DBAL\LockMode::NONE, $lockVersion = null) {
    }
}

The three methods in the repository will override the default finders. This will be enough to make the repository always return non-deleted entities by default.

Let’s look at how to implement the functions:

public function findBy(array $criteria) {
    return parent::findBy($this->fixCriteria($criteria));
}
 
public function findOneBy(array $criteria) {
    return parent::findOneBy($this->fixCriteria($criteria));
}
 
public function find($id, $lockMode = \Doctrine\DBAL\LockMode::NONE, $lockVersion = null) {
    return $this->findOneBy(array(
        'id' => $id
    ));
}
 
private function fixCriteria(array $criteria) {
    //Unless explicitly requested to return deleted items, we want to return non-deleted items by default
    if(!in_array('deleted', $criteria)) {
        $criteria['deleted'] = false;
    }
 
    return $criteria;
}

To achieve our requirement of returning non-deleted entities, we simply make sure that all queries include a criteria that makes the deleted flag false. As such, we always run the search criteria through the fixCriteria function, which makes sure that unless explicitly requested, we will return non-deleted items.

In closing

As you can see by utilizing a repository we can easily add custom behaviors into performing queries. However, do note a few gotchas with this approach:

  • When “deleting” entities, you should always use the methods in the entity itself and then have the EntityManager persist them
  • If you perform DQL queries, you will need to include a WHERE-clause to get non-deleted entities

The biggest benefit of this approach is received when you make sure your code always uses the repository when querying. This way you can make sure the returned results are always consistent to what is expected.

If you have any suggestions on how to improve this, or any other ideas, feel free to post them in the comments.

Share this:
  1. 4 Responses to “How to create Doctrine 1 -style Soft-Delete in Doctrine 2”

  2. I wouldn’t use a boolean column, but a datetime that’s nullable.

    It gives you the advantage that you can see when something was deleted.

    I also seem to recall having problems with joins if you used a boolean for this, but I can’t recall the details.

    By Harro on Dec 14, 2010

  3. Good idea – that should work equally well if you want to log the deletion time. So far we haven’t ran into any issues with booleans, but I would assume if there was any, it would be a D2 bug.

    By Jani Hartikainen on Dec 15, 2010

  4. I agree that being able to see when something was deleted can be useful; however, a single column is of limited use. Here are some things to noodle over:

    1 – You may want to allow delete, undelete, delete, … ad nauseam. If this is implemented, the single datetime column must be filled, then nulled, etc. Further, there is no auditing of what was done and when.

    2 – If you are already implementing a more full-featured auditing facility, then the extra datetime column is of no use and only serves to make things more confusing.

    I would suggest sticking to the boolean/integer and add true auditing. The Doctrine2 lifecycle event system works well for this.

    By Wil Moore III on Dec 20, 2010

  5. Hi,
    thank you for this how-to, but is there a way how to filter out deleted entities in relations?

    Eg:
    I have a Person, which have eg. two Addresses.
    so
    Person {id}
    Address {id,person_id}

    After that I will “delete” one of the address

    How to get the Person with only with addresses which are not deleted?

    I thought fetching the relations goes through its repository (AddressRepository in above example) and its findBy() function but it doesnt look like.

    Any help? Or should it work? Thanks a lot!

    By stefi on Oct 4, 2011

Post a Comment

You can use some HTML (a, em, strong, etc.). If you want to post code, use <pre lang="PHP">code here</pre> (you can replace PHP with the language you are posting)