[NFC] Comment block cleanup
[civicrm-core.git] / CRM / Core / Transaction.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
bc77d7c0 4 | Copyright CiviCRM LLC. All rights reserved. |
6a488035 5 | |
bc77d7c0
TO
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
6a488035 9 +--------------------------------------------------------------------+
d25dd0ee 10 */
6a488035
TO
11
12/**
13 *
14 * @package CRM
ca5cec67 15 * @copyright CiviCRM LLC https://civicrm.org/licensing
6a488035 16 * @copyright David Strauss <david@fourkitchens.com> (c) 2007
6a488035 17 *
5bb8417a
TO
18 * (Note: This has been considerably rewritten; the interface is preserved
19 * for backward compatibility.)
6a488035 20 *
5bb8417a
TO
21 * Transaction management in Civi is divided among three classes:
22 * - CRM_Core_Transaction: API. This binds to __construct() + __destruct()
23 * and notifies the transaction manager when it's OK to begin/end a transaction.
24 * - Civi\Core\Transaction\Manager: Tracks pending transaction-frames
25 * - Civi\Core\Transaction\Frame: A nestable transaction (e.g. based on BEGIN/COMMIT/ROLLBACK
26 * or SAVEPOINT/ROLLBACK TO SAVEPOINT).
27 *
28 * Examples:
29 *
0b882a86 30 * ```
5bb8417a
TO
31 * // Some business logic using the helper functions
32 * function my_business_logic() {
33 * CRM_Core_Transaction::create()->run(function($tx) {
34 * ...do work...
35 * if ($error) throw new Exception();
36 * });
37 * }
38 *
39 * // Some business logic which returns an error-value
40 * // and explicitly manages the transaction.
41 * function my_business_logic() {
42 * $tx = new CRM_Core_Transaction();
43 * ...do work...
44 * if ($error) {
45 * $tx->rollback();
46 * return error_value;
47 * }
48 * }
49 *
50 * // Some business logic which uses exceptions
51 * // and explicitly manages the transaction.
52 * function my_business_logic() {
53 * $tx = new CRM_Core_Transaction();
54 * try {
55 * ...do work...
56 * } catch (Exception $ex) {
57 * $tx->rollback()->commit();
58 * throw $ex;
59 * }
60 * }
61 *
0b882a86 62 * ```
5bb8417a
TO
63 *
64 * Note: As of 4.6, the transaction manager supports both reference-counting and nested
65 * transactions (SAVEPOINTs). In the past, it only supported reference-counting. The two cases
66 * may exhibit different systemic effects with respect to unhandled exceptions.
6a488035
TO
67 */
68class CRM_Core_Transaction {
69
70 /**
66f9e52b 71 * These constants represent phases at which callbacks can be invoked.
6a488035 72 */
7da04cde
TO
73 const PHASE_PRE_COMMIT = 1;
74 const PHASE_POST_COMMIT = 2;
75 const PHASE_PRE_ROLLBACK = 4;
76 const PHASE_POST_ROLLBACK = 8;
6a488035
TO
77
78 /**
5bb8417a
TO
79 * Whether commit() has been called on this instance
80 * of CRM_Core_Transaction
518fa0ee 81 * @var bool
6a488035 82 */
5bb8417a 83 private $_pseudoCommitted = FALSE;
6a488035
TO
84
85 /**
66f9e52b 86 * Ensure that an SQL transaction is started.
6a488035 87 *
5bb8417a 88 * This is a thin wrapper around __construct() which allows more fluent coding.
6a488035 89 *
6a0b768e
TO
90 * @param bool $nest
91 * Determines what to do if there's currently an active transaction:.
5bb8417a
TO
92 * - If true, then make a new nested transaction ("SAVEPOINT")
93 * - If false, then attach to the existing transaction
94 * @return \CRM_Core_Transaction
6a488035 95 */
5bb8417a
TO
96 public static function create($nest = FALSE) {
97 return new self($nest);
98 }
a0ee3941
EM
99
100 /**
66f9e52b 101 * Ensure that an SQL transaction is started.
a0ee3941 102 *
6a0b768e
TO
103 * @param bool $nest
104 * Determines what to do if there's currently an active transaction:.
5bb8417a
TO
105 * - If true, then make a new nested transaction ("SAVEPOINT")
106 * - If false, then attach to the existing transaction
a0ee3941 107 */
00be9182 108 public function __construct($nest = FALSE) {
5bb8417a 109 \Civi\Core\Transaction\Manager::singleton()->inc($nest);
6a488035
TO
110 }
111
00be9182 112 public function __destruct() {
6a488035
TO
113 $this->commit();
114 }
115
5bb8417a 116 /**
66f9e52b 117 * Immediately commit or rollback.
5bb8417a
TO
118 *
119 * (Note: Prior to 4.6, return void)
120 *
121 * @return \CRM_Core_Exception this
122 */
00be9182 123 public function commit() {
5bb8417a 124 if (!$this->_pseudoCommitted) {
6a488035 125 $this->_pseudoCommitted = TRUE;
5bb8417a 126 \Civi\Core\Transaction\Manager::singleton()->dec();
6a488035 127 }
5bb8417a 128 return $this;
6a488035
TO
129 }
130
a0ee3941
EM
131 /**
132 * @param $flag
133 */
518fa0ee 134 public static function rollbackIfFalse($flag) {
5bb8417a
TO
135 $frame = \Civi\Core\Transaction\Manager::singleton()->getFrame();
136 if ($flag === FALSE && $frame !== NULL) {
137 $frame->setRollbackOnly();
6a488035
TO
138 }
139 }
140
5bb8417a
TO
141 /**
142 * Mark the transaction for rollback.
143 *
144 * (Note: Prior to 4.6, return void)
ba3228d1 145 * @return \CRM_Core_Transaction
5bb8417a 146 */
6a488035 147 public function rollback() {
5bb8417a
TO
148 $frame = \Civi\Core\Transaction\Manager::singleton()->getFrame();
149 if ($frame !== NULL) {
150 $frame->setRollbackOnly();
151 }
152 return $this;
153 }
154
155 /**
156 * Execute a function ($callable) within the scope of a transaction. If
157 * $callable encounters an unhandled exception, then rollback the transaction.
158 *
159 * After calling run(), the CRM_Core_Transaction object is "used up"; do not
160 * use it again.
161 *
6a0b768e 162 * @param string $callable
b44e3f84 163 * Should exception one parameter (CRM_Core_Transaction $tx).
ba3228d1 164 * @return CRM_Core_Transaction
5bb8417a
TO
165 * @throws Exception
166 */
167 public function run($callable) {
168 try {
169 $callable($this);
0db6c3e1
TO
170 }
171 catch (Exception $ex) {
5bb8417a
TO
172 $this->rollback()->commit();
173 throw $ex;
174 }
175 $this->commit();
176 return $this;
6a488035
TO
177 }
178
179 /**
180 * Force an immediate rollback, regardless of how many any
181 * CRM_Core_Transaction objects are waiting for
182 * pseudo-commits.
183 *
184 * Only rollback if the transaction API has been called.
185 *
186 * This is only appropriate when it is _certain_ that the
187 * callstack will not wind-down normally -- e.g. before
188 * a call to exit().
189 */
518fa0ee 190 public static function forceRollbackIfEnabled() {
5bb8417a
TO
191 if (\Civi\Core\Transaction\Manager::singleton()->getFrame() !== NULL) {
192 \Civi\Core\Transaction\Manager::singleton()->forceRollback();
6a488035
TO
193 }
194 }
195
a0ee3941
EM
196 /**
197 * @return bool
198 */
518fa0ee 199 public static function willCommit() {
5bb8417a
TO
200 $frame = \Civi\Core\Transaction\Manager::singleton()->getFrame();
201 return ($frame === NULL) ? TRUE : !$frame->isRollbackOnly();
6a488035
TO
202 }
203
204 /**
66f9e52b 205 * Determine whether there is a pending transaction.
6a488035 206 */
518fa0ee 207 public static function isActive() {
5bb8417a
TO
208 $frame = \Civi\Core\Transaction\Manager::singleton()->getFrame();
209 return ($frame !== NULL);
6a488035
TO
210 }
211
212 /**
66f9e52b 213 * Add a transaction callback.
6a488035 214 *
5bb8417a
TO
215 * Note: It's conceivable to add callbacks to the main/overall transaction
216 * (aka $manager->getBaseFrame()) or to the innermost nested transaction
217 * (aka $manager->getFrame()). addCallback() has been used in the past to
218 * work-around deadlocks. This may or may not be necessary now -- but it
219 * seems more consistent (for b/c purposes) to attach callbacks to the
220 * main/overall transaction.
221 *
6a488035
TO
222 * Pre-condition: isActive()
223 *
6a0b768e
TO
224 * @param int $phase
225 * A constant; one of: self::PHASE_{PRE,POST}_{COMMIT,ROLLBACK}.
226 * @param string $callback
227 * A PHP callback.
228 * @param mixed $params
229 * Optional values to pass to callback.
6a488035 230 * See php manual call_user_func_array for details.
ba3228d1 231 * @param int $id
6a488035 232 */
518fa0ee 233 public static function addCallback($phase, $callback, $params = NULL, $id = NULL) {
5bb8417a
TO
234 $frame = \Civi\Core\Transaction\Manager::singleton()->getBaseFrame();
235 $frame->addCallback($phase, $callback, $params, $id);
6a488035 236 }
96025800 237
74effac4
TO
238 /**
239 * Whenever hook_civicrm_post fires, schedule an equivalent
240 * call to hook_civicrm_postCommit.
241 *
242 * @param \Civi\Core\Event\PostEvent $e
243 * @see CRM_Utils_Hook::post
244 */
245 public static function addPostCommit($e) {
246 // Do we want to dedupe post-commit hooks for the same txn? Setting an ID
247 // would allow this.
248 // $id = $e->entity . chr(0) . $e->action . chr(0) . $e->id;
249 $frame = \Civi\Core\Transaction\Manager::singleton()->getBaseFrame();
250 if ($frame) {
251 $frame->addCallback(self::PHASE_POST_COMMIT, ['CRM_Utils_Hook', 'postCommit'], [$e->action, $e->entity, $e->id, $e->object]);
252 }
253 else {
254 \CRM_Utils_Hook::postCommit($e->action, $e->entity, $e->id, $e->object);
255 }
256 }
257
6a488035 258}