Merge pull request #15856 from agileware/CIVICRM-1368
[civicrm-core.git] / Civi / Core / Transaction / Manager.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
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 |
9 +--------------------------------------------------------------------+
10 */
11
12 namespace Civi\Core\Transaction;
13
14 /**
15 *
16 * @package Civi
17 * @copyright CiviCRM LLC https://civicrm.org/licensing
18 */
19 class Manager {
20
21 private static $singleton = NULL;
22
23 /**
24 * @var \CRM_Core_DAO
25 */
26 private $dao;
27
28 /**
29 * Stack of SQL transactions/savepoints.
30 *
31 * @var \Civi\Core\Transaction\Frame[]
32 */
33 private $frames = [];
34
35 /**
36 * @var int
37 */
38 private $savePointCount = 0;
39
40 /**
41 * @param bool $fresh
42 * @return Manager
43 */
44 public static function singleton($fresh = FALSE) {
45 if (NULL === self::$singleton || $fresh) {
46 self::$singleton = new Manager(new \CRM_Core_DAO());
47 }
48 return self::$singleton;
49 }
50
51 /**
52 * @param \CRM_Core_DAO $dao
53 * Handle for the DB connection that will execute transaction statements.
54 * (all we really care about is the query() function)
55 */
56 public function __construct($dao) {
57 $this->dao = $dao;
58 }
59
60 /**
61 * Increment the transaction count / add a new transaction level
62 *
63 * @param bool $nest
64 * Determines what to do if there's currently an active transaction:.
65 * - If true, then make a new nested transaction ("SAVEPOINT")
66 * - If false, then attach to the existing transaction
67 */
68 public function inc($nest = FALSE) {
69 if (!isset($this->frames[0])) {
70 $frame = $this->createBaseFrame();
71 array_unshift($this->frames, $frame);
72 $frame->inc();
73 $frame->begin();
74 }
75 elseif ($nest) {
76 $frame = $this->createSavePoint();
77 array_unshift($this->frames, $frame);
78 $frame->inc();
79 $frame->begin();
80 }
81 else {
82 $this->frames[0]->inc();
83 }
84 }
85
86 /**
87 * Decrement the transaction count / close out a transaction level
88 *
89 * @throws \CRM_Core_Exception
90 */
91 public function dec() {
92 if (!isset($this->frames[0]) || $this->frames[0]->isEmpty()) {
93 throw new \CRM_Core_Exception('Transaction integrity error: Expected to find active frame');
94 }
95
96 $this->frames[0]->dec();
97
98 if ($this->frames[0]->isEmpty()) {
99 // Callbacks may cause additional work (such as new transactions),
100 // and it would be confusing if the old frame was still active.
101 // De-register it before calling finish().
102 $oldFrame = array_shift($this->frames);
103 $oldFrame->finish();
104 }
105 }
106
107 /**
108 * Force an immediate rollback, regardless of how many
109 * transaction or frame objects exist.
110 *
111 * This is only appropriate when it is _certain_ that the
112 * callstack will not wind-down normally -- e.g. before
113 * a call to exit().
114 */
115 public function forceRollback() {
116 // we take the long-way-round (rolling back each frame) so that the
117 // internal state of each frame is consistent with its outcome
118
119 $oldFrames = $this->frames;
120 $this->frames = [];
121 foreach ($oldFrames as $oldFrame) {
122 $oldFrame->forceRollback();
123 }
124 }
125
126 /**
127 * Get the (innermost) SQL transaction.
128 *
129 * @return \Civi\Core\Transaction\Frame
130 */
131 public function getFrame() {
132 return isset($this->frames[0]) ? $this->frames[0] : NULL;
133 }
134
135 /**
136 * Get the (outermost) SQL transaction (i.e. the one
137 * demarcated by BEGIN/COMMIT/ROLLBACK)
138 *
139 * @return \Civi\Core\Transaction\Frame
140 */
141 public function getBaseFrame() {
142 if (empty($this->frames)) {
143 return NULL;
144 }
145 return $this->frames[count($this->frames) - 1];
146 }
147
148 /**
149 * @return \Civi\Core\Transaction\Frame
150 */
151 protected function createBaseFrame() {
152 return new Frame($this->dao, 'BEGIN', 'COMMIT', 'ROLLBACK');
153 }
154
155 /**
156 * @return \Civi\Core\Transaction\Frame
157 */
158 protected function createSavePoint() {
159 $spId = $this->savePointCount++;
160 return new Frame($this->dao, "SAVEPOINT civi_{$spId}", NULL, "ROLLBACK TO SAVEPOINT civi_{$spId}");
161 }
162
163 }