A couple of days ago I blogged about how Doctrine's SoftDelete behavior can keep other listeners' preDelete() hooks from firing; after a bit of coding this morning, I believe I have a solution.

In my initial setup, I had applied the SoftDelete behavior in the model itself via the following YAML config:

Post:
  actAs:
    SoftDelete:

Then, in my Zend_Application bootstrap, I was assigning my ACL listener like so:

<?php
$listener = new Blahg_Plugin_Doctrine_Record_Listener_Acl($acl);
$table->addRecordListener($listener);

Since behaviors are assigned very early on in the process (during the setTableDefinition() method), this chain of events resulted in the following order of listeners:

  • SoftDelete
  • Acl

Unfortunately, since SoftDelete changes the nature of the event from DELETE to UPDATE, this meant that by the time the Acl listener fired, the event looked like an update, and the update hooks got fired instead of the delete hooks.

The solution? Change the order in which the listeners are registered by implementing my ACL listener as part of an assignable behavior. That way I could assign both listeners in the actAs part of the YAML schema, like so:

Post:
  actAs:
    Blahg_Plugin_Doctrine_Template_Aclable:
    SoftDelete:

As long as SoftDelete is the last behavior attached, the ACL listeners will fire correctly.

This requires a couple of modifications to the original code. For starters, we need to modify the constructor of our listener to accept an array of options instead of just a Zend_Acl object:

<?php
class Blahg_Plugin_Doctrine_Record_Listener_Acl 
    extends Doctrine_Record_Listener
{
    protected $_options = array();
 
    public function __construct(array $options)
    {
        $this->_options = $options;
    }
}

Since we're no longer forcing constructor injection of the ACL object, we also need to allow the listener to go and find one from the application bootstrap if necessary. As a result, we modify getAcl() and setAcl() accordingly:

<?php
    public function getAcl()
    {
        if (!array_key_exists('acl', $this->_options) || null === $this->_options['acl']) {
            $fc = Zend_Controller_Front::getInstance();
            $bootstrap = $fc->getParam('Bootstrap');
            $bootstrap->bootstrap('acl');
            $this->setAcl($bootstrap->getResource('acl'));
        }
        return $this->_options['acl'];
    }
 
    public function setAcl(Zend_Acl $acl)
    {   
        $this->_options['acl'] = $acl;
        return $this;
    }

This still allows us to inject the ACL as necessary for tests and such, while allowing for graceful failover to the bootstrap object if one has not been provided (caveat: you have to register your bootstrap with your front controller instance for this to work).

In order to use this modified listener as a behavior, we also need to define a (very) simple Doctrine_Template implementation. All it'll need to do is register the listener during setTableDefinition():

<?php
class Blahg_Plugin_Doctrine_Template_Aclable extends Doctrine_Template
{
    protected $_options = array(
        'acl' => null,
    );  
 
    public function setTableDefinition()
    {       
        $this->addListener(new Blahg_Plugin_Doctrine_Record_Listener_Acl($this->_options));
    }   
}

And that's it! From here, just add the new Aclable behavior to your schema file, regenerate your model classes, and everything should happen in the right order from then on. Problem solved.

Categories: