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

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.