448191 Posted July 8, 2007 Share Posted July 8, 2007 I've noticed some odd behaviour when testing a Domain Object-Data Mapper/Unit of Work setup with respect to destructors... When I manually call commit() to finalize a transaction, everything works as expected. But if I rely on __destruct() of the Data Mapper (and Unit of Work), the destructor throws an exception. This only happens in tests where Domain Objects are loaded from store, and they in their turn have references to the mapper/unit of work, so I'm guessing this has something to do with circular referencing... Some code fragments: Concrete Data Mapper/Unit of Work <?php require_once 'Backbone/DataMapper.php'; class Cms_User_DataMapper_Xml extends Backbone_DataMapper { private static $fileDirty = false; private static $xml; public function __construct(){ if(self::$xml === null){ self::$xml = new DOMDocument(); self::$xml->validateOnParse = true; self::$xml->registerNodeClass('DOMElement', 'Backbone_Xml_Element_Dom'); self::$xml->load(Backbone_Settings::USER_FILE); } } public function doCreate(Backbone_DomainObject $user){ if(!$user instanceof Cms_User){ throw new Backbone_DataMapper_Exception('Object must be of type Cms_User'); } if($this->isStored($user->getId())){ throw new Backbone_DataMapper_Exception('User id "'.$user->getId().'" already used in store, create impossible.'); } $element = self::$xml->documentElement->appendElement('user'); $element->setAttribute('id', 'ID'.$user->getId()); $element->setAttribute('lastlogin', $user->getLastLogin()); $element->setAttribute('password', $user->getPassword()); $element->setAttribute('status', $user->getStatus()); $element->setAttribute('failedloginattempts', $user->getFailedLoginAttempts()); $this->setFileDirty(true); } .... public function setFileDirty($bool){ if(!is_bool($bool)){ throw new Backbone_DataMapper_Exception('Boolean dirty flag expected.'); } self::$fileDirty = $bool; } public function fileIsDirty(){ return self::$fileDirty; } public function commit(){ parent::commit(); if($this->fileIsDirty()){ $this->save(); $this->setFileDirty(false); } } public function save(){ self::$xml->save(Backbone_Settings::USER_FILE); } } ?> Abstract Data Mapper/Unit of Work <?php abstract class Backbone_DataMapper { private static $flags = array('new', 'clean', 'dirty', 'deleted'); private $objects = array(); public $autoCommit = false; public static function factory($classPrefix, $method, $args = array()){ $class = $classPrefix.'_DataMapper_'.$method; if(!class_exists($class, true)){ throw new Backbone_DataMapper_Exception('Invalid class name: '.$class); } $refl = new ReflectionClass($class); return $refl->newInstance($args); } ... final public function __destruct(){ if($this->autoCommit){ $this->commit(); } } /** * Write changes to storage * */ public function commit(){ foreach($this->objects as $object){ switch($object->getDataFlag()){ case 'new'; $this->create($object); break; case 'dirty'; $this->update($object); break; case 'deleted'; $this->delete($object); break; case 'clean'; break; default: throw new Backbone_DataMapper_Exception('Invalid flag.'); } } } } ?> Unit test (the one that throws an exception) <?php public function testAutoCommit(){ include_once 'Cms\User.php'; $mapper = Backbone_DataMapper::factory('Cms_User', 'Xml'); $mapper->autoCommit = true; $users = $mapper->loadAll(); foreach($users as $user){ $this->assertType('Cms_User', $user); $user->setLastLogin('100'); } unset($mapper); $mapper = Backbone_DataMapper::factory('Cms_User', 'Xml'); $users = $mapper->loadAll(); foreach($users as $user){ $this->assertSame('100', $user->getLastLogin(), 'auto commit failed'); $user->setLastLogin('200'); } $this->assertTrue($mapper->documentIsValid(), 'document corrupted'); $this->assertFalse($mapper->fileIsDirty(),'dirty flag should be FALSE'); } ?> Concrete Domain Object <?php require_once 'Backbone\DomainObject.php'; class Cms_User implements Backbone_DomainObject { private $status = null; private $id; private $lastLogin; private $psw; private $failedLoginAttempts; private $mapper; private $dataFlag; public function __construct($id, $psw, $lastLogin, $failedLoginAttempts, Backbone_DataMapper $mapper){ $this->id = $id; $this->lastLogin = $lastLogin; $this->psw = $psw; $this->failedLoginAttempts = $failedLoginAttempts; $this->mapper = $mapper; } ... public function resetFailedLoginAttempts(){ $this->failedloginattempts = 0; $this->mapper->registerDirty($this); } } ?> Quote Link to comment Share on other sites More sharing options...
Jenk Posted July 9, 2007 Share Posted July 9, 2007 You can't throw exceptions/instantiate new objects within a __destruct(). It is aimed at literally clean ups only, not processing or application flow. Quote Link to comment Share on other sites More sharing options...
448191 Posted July 9, 2007 Author Share Posted July 9, 2007 I know that, obviously. The problem is using __destruct() CAUSES an exception. This only occurs when Cms_User and Backbone_DataMapper are EACHOTHERS aggregate objects. Normally an aggregate object is destructed after the containing object, but in this scenario there is not one definite whole, not one aggregate. I can see how the engine may not know which object to destruct first. I would like it to destruct the Data Mapper first, obviously, otherwise there will be no Domain Objects to store. One solution would be to avoid the Domain Object having a stored reference to the mapper, but that would mean having to pass the mapper object whenever it needs to perform an action that potentially marks it dirty. This adds extra responsibility to the client code, something I wish to avoid. Note that I've combined Unit of Work and Data Mapper, in this case it's mostly the Unit of Work (and the business of letting Domain Objects mark itself dirty) that's causing issues. I'd like to at least use the destructor as a saveguard, to ensure that changes are always committed. But I can't even do that, because the destructor is not executed. :-\ Quote Link to comment Share on other sites More sharing options...
Jenk Posted July 11, 2007 Share Posted July 11, 2007 Tried using register_shutdown_function()? Quote Link to comment Share on other sites More sharing options...
448191 Posted July 12, 2007 Author Share Posted July 12, 2007 Tbh, I hadn't thought of that. I should be able to make that work. Thanks! Quote Link to comment Share on other sites More sharing options...
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.