In my recent post on using Zend_Acl with Doctrine record listeners, I described a way to automate a Doctrine-based application's access control logic based on certain event hooks in Doctrine's record listener system. I still think it's a fairly elegant approach, but as I've been working with it, I discovered one behavior I didn't quite expect.

As it happened, one of the models on which I was using this technique also implemented Doctrine's core SoftDelete behavior. With SoftDelete enabled, calling $record->delete() doesn't actually remove the record from the database; instead, it provides and sets a deleted_at column and then adjusts all your other queries to treat any record with a deleted_at value as though it isn't there. In other words, all SQL DELETEs become UPDATEs, and all SQL SELECTs get an extra WHERE clause that ensures no "deleted" records are ever returned unless you explicitly ask for them. Pretty ingenious, really; it's nice if you think you'll ever need to recover from accidental deletions (though fortunately I haven't had to use it yet).

However, I recently discovered something I probably should have expected in the first place: when I called my SoftDelete-powered record's delete() method, my record listener's preDelete() hook wasn't firing; after some further research I discovered that it was firing preUpdate() instead.

As it turns out, since SoftDelete turns what would have been a SQL DELETE operation into a SQL UPDATE, the pre- and postDelete hooks are overridden with their *Update equivalents (at least after SoftDelete's own delete hooks have finished up). The unfortunate side effect? Since the preUpdate hook allows users with the "update" permission to proceed, users who had "update" could delete records, even if they didn't have the "delete" permission. Not a great setup, all things considered.

Now, I do have other protections in place. For one, I'm not only checking permissions at the model layer; my controllers do still have a few remaining ACL checks to avoid showing the user interfaces they won't actually be able to use. That said, I'd love to find a workaround for this, especially if I ever release any of this code for public use.

At the moment the only thing I can think to try is to figure out a way to ensure that my record listener is registered earlier in the stack than SoftDelete is. I'm not sure this is possible with how Doctrine behaviors are registered, but I figure it's worth some experimentation. I'll let you know how it goes.