Skip to content
This repository has been archived by the owner on Jan 21, 2020. It is now read-only.

Revert model creation if their "callbacks" failed #2561

Merged
merged 5 commits into from Feb 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
78 changes: 50 additions & 28 deletions library/CM/Model/Abstract.php
@@ -1,5 +1,7 @@
<?php

use CM\Transactions\Transaction;

abstract class CM_Model_Abstract extends CM_Class_Abstract
implements CM_Comparable, CM_ArrayConvertible, JsonSerializable, CM_Cacheable, Serializable, CM_Typed, CM_Debug_DebugInfoInterface {

Expand Down Expand Up @@ -53,9 +55,7 @@ final protected function _construct(array $id = null, array $data = null) {
/**
* @param bool|null $useReplace
* @throws CM_Exception_Invalid
* @throws CM_Exception_InvalidParam
* @throws CM_Exception_Nonexistent
* @throws CM_Exception_NotImplemented
* @throws Exception
*/
public function commit($useReplace = null) {
$useReplace = (boolean) $useReplace;
Expand All @@ -71,38 +71,50 @@ public function commit($useReplace = null) {
if (!empty($dataSchema)) {
$persistence->save($type, $this->getIdRaw(), $dataSchema);
}

if ($cache = $this->_getCache()) {
$cache->save($type, $this->getIdRaw(), $this->_getData());
}
$this->_onChange();
} else {
$this->_validateFields($this->_getData(), true);
if ($useReplace) {
if (!$persistence instanceof CM_Model_StorageAdapter_ReplaceableInterface) {
$adapterName = get_class($persistence);
throw new CM_Exception_NotImplemented('Param `useReplace` is not allowed with adapter', null, ['adapterName' => $adapterName]);
$transaction = new Transaction();
try {
$this->_validateFields($this->_getData(), true);
if ($useReplace) {
if (!$persistence instanceof CM_Model_StorageAdapter_ReplaceableInterface) {
$adapterName = get_class($persistence);
throw new CM_Exception_NotImplemented('Param `useReplace` is not allowed with adapter', null, ['adapterName' => $adapterName]);
}
$idRaw = $persistence->replace($type, $dataSchema);
} else {
$idRaw = $persistence->create($type, $dataSchema);
$transaction->addRollback(function () use ($persistence, $type, $idRaw) {
$persistence->delete($type, $idRaw);
});
}
$idRaw = $persistence->replace($type, $dataSchema);
} else {
$idRaw = $persistence->create($type, $dataSchema);
}

$this->_id = self::_castIdRaw($idRaw);
$this->_id = self::_castIdRaw($idRaw);

if ($cache = $this->_getCache()) {
$this->_loadAssets(true);
$cache->save($type, $this->getIdRaw(), $this->_getData());
}
$this->_changeContainingCacheables();
$this->_onCreate();
if ($useReplace) {
$this->_onChange();
if ($cache = $this->_getCache()) {
$this->_loadAssets(true);
$cache->save($type, $this->getIdRaw(), $this->_getData());
$transaction->addRollback(function () use ($cache, $type) {
$cache->delete($type, $this->getIdRaw());
});
}
$this->_changeContainingCacheables();
$this->_onCreate();
if ($useReplace) {
$this->_onChange();
}
} catch (Exception $e) {
$transaction->rollback();
throw $e;
}
}
$this->_autoCommit = true;
}


public function delete() {
$containingCacheables = $this->_getContainingCacheables();
$this->_onDeleteBefore();
Expand Down Expand Up @@ -495,15 +507,25 @@ protected function _getPersistence() {
/**
* @param array|null $data
* @return static
* @throws Exception
*/
final public static function createStatic(array $data = null) {
if ($data === null) {
$data = array();
$transaction = new Transaction();
try {
if ($data === null) {
$data = [];
}
$model = static::_createStatic($data);
$transaction->addRollback(function() use ($model) {
$model->delete();
});
$model->_changeContainingCacheables();
$model->_onCreate();
return $model;
} catch (Exception $e) {
$transaction->rollback();
throw $e;
}
$model = static::_createStatic($data);
$model->_changeContainingCacheables();
$model->_onCreate();
return $model;
}

/**
Expand Down
31 changes: 31 additions & 0 deletions tests/library/CM/Model/AbstractTest.php
Expand Up @@ -1185,6 +1185,37 @@ public function testCommitWithReplace() {
$this->assertSame('Param `useReplace` is not allowed with adapter', $exception->getMessage());
$this->assertSame(['adapterName' => get_class($persistence)], $exception->getMetaInfo());
}

public function testCommitCreateTransactionRollback() {
$model = $this->mockClass(CM_Model_Abstract::class)->newInstanceWithoutConstructor();
$model->mockMethod('_getSchemaData')->set([]);
$model->mockMethod('getType')->set(1);
$model->mockMethod('_validateFields');
$model->mockMethod('_getData')->set([]);

$persistence = $this->mockObject(CM_Model_StorageAdapter_AbstractAdapter::class);
$persistence->mockMethod('create')->set([]);
$persistenceDelete = $persistence->mockMethod('delete');
$model->mockMethod('_getPersistence')->set($persistence);

$cache = $this->mockObject(CM_Model_StorageAdapter_AbstractAdapter::class);
$cacheDelete = $cache->mockMethod('delete');
$model->mockMethod('_getCache')->set($cache);

$exception = new Exception('Cannot perform on create callback');
$model->mockMethod('_onCreate')->set(function() use ($exception) {
throw $exception;
});

try {
/** @var CM_Model_Abstract $model */
$model->commit();
} catch (Exception $e) {
$this->assertSame($e, $exception);
$this->assertSame(1, $persistenceDelete->getCallCount());
$this->assertSame(1, $cacheDelete->getCallCount());
}
}
}

class CM_ModelMock extends CM_Model_Abstract {
Expand Down