Search code examples
phpsymfonysoft-deletestofdoctrineextensions

Coherent logic behind the use of SoftDeleteable


By every is know that SoftDeleteable is a Doctrine Extension:

That allows behavior to "soft delete" objects, filtering them at SELECT time by marking them with a timestamp as, but not Explicitly removing them from the database.

Now considering this, which would be consistent logic when inserting new rows in the table and taking the above marked as deleted but physically being there?

The point is that I have recently been forced to make use of this behavior in an application but when I am inserting new records, and logically, when one exists then I got a error like this:

An exception occurred while executing "INSERT INTO fos_user (username, username_canonical, email, email_canonical, enabled, salt, password, last_login, locked, expired, expires_at, confirmation_token, password_requested_at, roles, credentials_expired, credentials_expire_at, deletedAt, createdAt, updatedAt) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) 'with params ["admin1" "admin1" "admin", "admin", 1, "8ycal2x0eewwg0gw0o0gcw884ooossg", "886mLvLTi1yPdSBTR9Cfi + + a3lvideQ4pw89ZHDOWVz86kJqXjx7C1 1ZIwTvzET7N1Fk \ / yHsw10z3Cjm9k + m \ / g ==", null, 0, 0, null, null, null, 'a: 2: {i: 0; s: 16: \ "ROLE_PROFILE_ONE \" i: 1, s: 16: \ "ROLE_PROFILE_TWO \";} ", 0, null, null," 09/12/2014 18:16:01 ""9/12/2014 18:16:01"]:

SQLSTATE [23000]: Integrity constraint Violation: 1062 Duplicate entry 'admin1' for key 'UNIQ_957A647992FC23A8'

My question is, how you handle SoftdDeleteable to enter new records? An example of what yours do or fewer ideas would come to me well and would help.


Solution

  • If you are wanting to keep the original then you would need to find someway to make sure the unique field hadn't been used before. I think the easiest way t do that would be to use a custom repository for your user field and disable the softdeleteable filter before the search.

    By default the UniqueEntity uses findBy and the repository set for the class but it would make sense to create your own method with the filter disabled by default so as to avoid having to mess around with the constraint while leaving the regular method intact.

    As you are using FOSUserBundle (or so it seems from the table name fos_user) you can set the repositoryClass in your mapping (the XML is a big load of mess but you can see it here)...

    // Annotation
    @ORM\Entity(repositoryClass="Acme\StoreBundle\Entity\ProductRepository")
    
    //YAML
    Acme\UserBundle\Entity\User:
        type: entity
        repositoryClass: Acme\UserBundle\Entity\UserRepository
    

    And then in your UserRepository just add you findIncludingSoftdeletedBy method disabling the softdeleteable filter like...

    namespace Acme\UserBundle\Entity;
    
    use Doctrine\ORM\EntityRepository;
    
    class UserRepository extends EntityRepository
    {
        /**
         * Finds users by a set of criteria including sofdeleted.
         *
         * @param array      $criteria
         * @param array|null $orderBy
         * @param int|null   $limit
         * @param int|null   $offset
         *
         * @return array The objects.
         */
        public function findIncludingSoftdeletedBy(
            array $criteria, array $orderBy = null, $limit = null, $offset = null
        )
        {
            // Get array of enabled filters
            $enabledFilters = $this->em->getFilters()->getEnabledFilters();
    
            // If softdeleteable (or soft-deleteable depending on config) 
            // is in array of enabled filters disable it
            if (array_key_exists('softdeleteable', $endabledFilters)) {
                // disabled softdeleteable filter ($this->em being entity manager)
                $this->_em->getFilters()->disable('softdeleteable');
            }
    
            // return regular "findBy" including softdeleted users
            return $this->findBy($criteria, $orderBy, $limit, $offset);
        }
    }
    

    Update

    I forgot this bit.

    You would then need to create your own validation file that would reference this new validation constraint. (For FOSUserBundle and in YAML (I prefer YAML, XML looks like a physics book has been sick on my screen)).

    Acme\UserBundle\Entity\User:
        constraints:
            - Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity:
                fields: usernameCanonical
                errorPath: username
                message: fos_user.username.already_used
                // Your method instead of the default "findBy"
                method: findIncludingSoftdeletedBy
                groups: [ Registration, Profile ]
            - Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity:
                fields: emailCanonical
                errorPath: email
                message: fos_user.email.already_used
                // Your method instead of the default "findBy"
                method: findIncludingSoftdeletedBy
                groups: [ Registration, Profile ]
    

    For more information on the UniqueEntity constraint see the docs, specifically..

    fields
    type: array | string [default option]

    This required option is the field (or list of fields) on which this entity should be unique. For example, if you specified both the email and name field in a single UniqueEntity constraint, then it would enforce that the combination value where unique (e.g. two users could have the same email, as long as they don't have the same name also).

    If you need to require two fields to be individually unique (e.g. a unique email and a unique username), you use two UniqueEntity entries, each with a single field.