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