From: Tim Otten Date: Mon, 10 Nov 2014 22:54:48 +0000 (-0800) Subject: CRM-15578 - APIv3 - options.force_rollback X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=e29a1b0393d349c5bcd0b041fa035fbadf347f52;p=civicrm-core.git CRM-15578 - APIv3 - options.force_rollback --- diff --git a/Civi/API/Subscriber/TransactionSubscriber.php b/Civi/API/Subscriber/TransactionSubscriber.php index 2dd33da514..ca16b7d7bd 100644 --- a/Civi/API/Subscriber/TransactionSubscriber.php +++ b/Civi/API/Subscriber/TransactionSubscriber.php @@ -31,6 +31,12 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** * Class TransactionSubscriber + * + * Implement transaction management for API calls. Two API options are accepted: + * - is_transactional: bool|'nest' - if true, then all work is done inside a transaction. by default, true for mutator actions (C-UD) + * 'nest' will force creation of a nested transaction; otherwise, the default is to re-use any existing transactions + * - options.force_rollback: bool - if true, all work is done in a nested transaction which will be rolled back + * * @package Civi\API\Subscriber */ class TransactionSubscriber implements EventSubscriberInterface { @@ -50,6 +56,14 @@ class TransactionSubscriber implements EventSubscriberInterface { */ private $transactions = array(); + /** + * @var array (scalar $apiRequestId => bool) + * + * A list of requests which should be forcibly rolled back to + * their save points. + */ + private $forceRollback = array(); + /** * Determine if an API request should be treated as transactional * @@ -58,12 +72,50 @@ class TransactionSubscriber implements EventSubscriberInterface { * @return bool */ public function isTransactional($apiProvider, $apiRequest) { + if ($this->isForceRollback($apiProvider, $apiRequest)) { + return true; + } if (isset($apiRequest['params']['is_transactional'])) { - return \CRM_Utils_String::strtobool($apiRequest['params']['is_transactional']); + return \CRM_Utils_String::strtobool($apiRequest['params']['is_transactional']) || $apiRequest['params']['is_transactional'] == 'nest'; } return strtolower($apiRequest['action']) == 'create' || strtolower($apiRequest['action']) == 'delete' || strtolower($apiRequest['action']) == 'submit'; } + /** + * Determine if caller wants us to *always* rollback. + * + * @param \Civi\API\Provider\ProviderInterface $apiProvider + * @param array $apiRequest + * @return bool + */ + public function isForceRollback($apiProvider, $apiRequest) { + // FIXME: When APIv3 uses better parsing, the [params][options][force_rollback] check will be redundant + if (isset($apiRequest['params']['options']['force_rollback'])) { + return \CRM_Utils_String::strtobool($apiRequest['params']['options']['force_rollback']); + } + if (isset($apiRequest['options']['force_rollback'])) { + return \CRM_Utils_String::strtobool($apiRequest['options']['force_rollback']); + } + return FALSE; + } + + /** + * Determine if caller wants a nested transaction or a re-used transaction. + * + * @param \Civi\API\Provider\ProviderInterface $apiProvider + * @param array $apiRequest + * @return bool True if a new nested transaction is required; false if active tx may be used + */ + public function isNested($apiProvider, $apiRequest) { + if ($this->isForceRollback($apiProvider, $apiRequest)) { + return TRUE; + } + if (isset($apiRequest['params']['is_transactional']) && $apiRequest['params']['is_transactional'] === 'nest') { + return TRUE; + } + return FALSE; + } + /** * Open a new transaction instance (if appropriate in the current policy) * @@ -72,7 +124,10 @@ class TransactionSubscriber implements EventSubscriberInterface { function onApiPrepare(\Civi\API\Event\PrepareEvent $event) { $apiRequest = $event->getApiRequest(); if ($this->isTransactional($event->getApiProvider(), $apiRequest)) { - $this->transactions[$apiRequest['id']] = new \CRM_Core_Transaction(); + $this->transactions[$apiRequest['id']] = new \CRM_Core_Transaction($this->isNested($event->getApiProvider(), $apiRequest)); + } + if ($this->isForceRollback($event->getApiProvider(), $apiRequest)) { + $this->transactions[$apiRequest['id']]->rollback(); } } diff --git a/tests/phpunit/Civi/API/Subscriber/TransactionSubscriberTest.php b/tests/phpunit/Civi/API/Subscriber/TransactionSubscriberTest.php new file mode 100644 index 0000000000..e56b05ae72 --- /dev/null +++ b/tests/phpunit/Civi/API/Subscriber/TransactionSubscriberTest.php @@ -0,0 +1,78 @@ + TRUE), TRUE, FALSE, FALSE); + $r[] = array(3, 'Widget', 'get', array('is_transactional' => FALSE), FALSE, FALSE, FALSE); + $r[] = array(3, 'Widget', 'get', array('is_transactional' => 'nest'), TRUE, FALSE, TRUE); + + $r[] = array(3, 'Widget', 'create', array('is_transactional' => TRUE), TRUE, FALSE, FALSE); + $r[] = array(3, 'Widget', 'create', array('is_transactional' => FALSE), FALSE, FALSE, FALSE); + $r[] = array(3, 'Widget', 'create', array('is_transactional' => 'nest'), TRUE, FALSE, TRUE); + + $r[] = array(3, 'Widget', 'create', array('options' => array('force_rollback' => TRUE)), TRUE, TRUE, TRUE); + $r[] = array(3, 'Widget', 'create', array('options' => array('force_rollback' => FALSE)), TRUE, FALSE, FALSE); + + $r[] = array(3, 'Widget', 'create', array('is_transactional' => TRUE, 'options' => array('force_rollback' => TRUE)), TRUE, TRUE, TRUE); + $r[] = array(3, 'Widget', 'create', array('is_transactional' => TRUE, 'options' => array('force_rollback' => FALSE)), TRUE, FALSE, FALSE); + $r[] = array(3, 'Widget', 'create', array('is_transactional' => FALSE, 'options' => array('force_rollback' => TRUE)), TRUE, TRUE, TRUE); + $r[] = array(3, 'Widget', 'create', array('is_transactional' => FALSE, 'options' => array('force_rollback' => FALSE)), FALSE, FALSE, FALSE); + + $r[] = array(4, 'Widget', 'get', array(), FALSE, FALSE, FALSE); + $r[] = array(4, 'Widget', 'create', array(), TRUE, FALSE, FALSE); + + $r[] = array(4, 'Widget', 'create', array('is_transactional' => TRUE), TRUE, FALSE, FALSE); + $r[] = array(4, 'Widget', 'create', array('is_transactional' => FALSE), FALSE, FALSE, FALSE); + $r[] = array(4, 'Widget', 'create', array('is_transactional' => 'nest'), TRUE, FALSE, TRUE); + + $r[] = array(4, 'Widget', 'create', array('options' => array('force_rollback' => TRUE)), TRUE, TRUE, TRUE); + $r[] = array(4, 'Widget', 'create', array('options' => array('force_rollback' => FALSE)), TRUE, FALSE, FALSE); + + return $r; + } + + /** + * Ensure that API parameters "is_transactional" and "force_rollback" are parsed correctly + * @dataProvider transactionOptions + */ + function testTransactionOptions($version, $entity, $action, $params, $isTransactional, $isForceRollback, $isNested) { + $txs = new TransactionSubscriber(); + $apiProvider = NULL; + + $params['version'] = $version; + $apiRequest = \Civi\API\Request::create($entity, $action, $params, array()); + + $this->assertEquals($isTransactional, $txs->isTransactional($apiProvider, $apiRequest), 'check isTransactional'); + $this->assertEquals($isForceRollback, $txs->isForceRollback($apiProvider, $apiRequest), 'check isForceRollback'); + $this->assertEquals($isNested, $txs->isNested($apiProvider, $apiRequest), 'check isNested'); + } + + function testForceRollback() { + $result = $this->callAPISuccess('contact', 'create', array( + 'contact_type' => 'Individual', + 'first_name' => 'Me', + 'last_name' => 'Myself', + 'options' => array( + 'force_rollback' => TRUE + ), + )); + $this->assertTrue(is_numeric($result['id'])); + $this->assertDBQuery(0, 'SELECT count(*) FROM civicrm_contact WHERE id = %1', array( + 1 => array($result['id'], 'Integer'), + )); + } +}