ManagesTransactions.php 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. <?php
  2. namespace Illuminate\Database\Concerns;
  3. use Closure;
  4. use Illuminate\Database\DeadlockException;
  5. use RuntimeException;
  6. use Throwable;
  7. trait ManagesTransactions
  8. {
  9. /**
  10. * Execute a Closure within a transaction.
  11. *
  12. * @param \Closure $callback
  13. * @param int $attempts
  14. * @return mixed
  15. *
  16. * @throws \Throwable
  17. */
  18. public function transaction(Closure $callback, $attempts = 1)
  19. {
  20. for ($currentAttempt = 1; $currentAttempt <= $attempts; $currentAttempt++) {
  21. $this->beginTransaction();
  22. // We'll simply execute the given callback within a try / catch block and if we
  23. // catch any exception we can rollback this transaction so that none of this
  24. // gets actually persisted to a database or stored in a permanent fashion.
  25. try {
  26. $callbackResult = $callback($this);
  27. }
  28. // If we catch an exception we'll rollback this transaction and try again if we
  29. // are not out of attempts. If we are out of attempts we will just throw the
  30. // exception back out, and let the developer handle an uncaught exception.
  31. catch (Throwable $e) {
  32. $this->handleTransactionException(
  33. $e, $currentAttempt, $attempts
  34. );
  35. continue;
  36. }
  37. $levelBeingCommitted = $this->transactions;
  38. try {
  39. if ($this->transactions == 1) {
  40. $this->fireConnectionEvent('committing');
  41. $this->getPdo()->commit();
  42. }
  43. $this->transactions = max(0, $this->transactions - 1);
  44. } catch (Throwable $e) {
  45. $this->handleCommitTransactionException(
  46. $e, $currentAttempt, $attempts
  47. );
  48. continue;
  49. }
  50. $this->transactionsManager?->commit(
  51. $this->getName(),
  52. $levelBeingCommitted,
  53. $this->transactions
  54. );
  55. $this->fireConnectionEvent('committed');
  56. return $callbackResult;
  57. }
  58. }
  59. /**
  60. * Handle an exception encountered when running a transacted statement.
  61. *
  62. * @param \Throwable $e
  63. * @param int $currentAttempt
  64. * @param int $maxAttempts
  65. * @return void
  66. *
  67. * @throws \Throwable
  68. */
  69. protected function handleTransactionException(Throwable $e, $currentAttempt, $maxAttempts)
  70. {
  71. // On a deadlock, MySQL rolls back the entire transaction so we can't just
  72. // retry the query. We have to throw this exception all the way out and
  73. // let the developer handle it in another way. We will decrement too.
  74. if ($this->causedByConcurrencyError($e) &&
  75. $this->transactions > 1) {
  76. $this->transactions--;
  77. $this->transactionsManager?->rollback(
  78. $this->getName(), $this->transactions
  79. );
  80. throw new DeadlockException($e->getMessage(), is_int($e->getCode()) ? $e->getCode() : 0, $e);
  81. }
  82. // If there was an exception we will rollback this transaction and then we
  83. // can check if we have exceeded the maximum attempt count for this and
  84. // if we haven't we will return and try this query again in our loop.
  85. $this->rollBack();
  86. if ($this->causedByConcurrencyError($e) &&
  87. $currentAttempt < $maxAttempts) {
  88. return;
  89. }
  90. throw $e;
  91. }
  92. /**
  93. * Start a new database transaction.
  94. *
  95. * @return void
  96. *
  97. * @throws \Throwable
  98. */
  99. public function beginTransaction()
  100. {
  101. foreach ($this->beforeStartingTransaction as $callback) {
  102. $callback($this);
  103. }
  104. $this->createTransaction();
  105. $this->transactions++;
  106. $this->transactionsManager?->begin(
  107. $this->getName(), $this->transactions
  108. );
  109. $this->fireConnectionEvent('beganTransaction');
  110. }
  111. /**
  112. * Create a transaction within the database.
  113. *
  114. * @return void
  115. *
  116. * @throws \Throwable
  117. */
  118. protected function createTransaction()
  119. {
  120. if ($this->transactions == 0) {
  121. $this->reconnectIfMissingConnection();
  122. try {
  123. $this->getPdo()->beginTransaction();
  124. } catch (Throwable $e) {
  125. $this->handleBeginTransactionException($e);
  126. }
  127. } elseif ($this->transactions >= 1 && $this->queryGrammar->supportsSavepoints()) {
  128. $this->createSavepoint();
  129. }
  130. }
  131. /**
  132. * Create a save point within the database.
  133. *
  134. * @return void
  135. *
  136. * @throws \Throwable
  137. */
  138. protected function createSavepoint()
  139. {
  140. $this->getPdo()->exec(
  141. $this->queryGrammar->compileSavepoint('trans'.($this->transactions + 1))
  142. );
  143. }
  144. /**
  145. * Handle an exception from a transaction beginning.
  146. *
  147. * @param \Throwable $e
  148. * @return void
  149. *
  150. * @throws \Throwable
  151. */
  152. protected function handleBeginTransactionException(Throwable $e)
  153. {
  154. if ($this->causedByLostConnection($e)) {
  155. $this->reconnect();
  156. $this->getPdo()->beginTransaction();
  157. } else {
  158. throw $e;
  159. }
  160. }
  161. /**
  162. * Commit the active database transaction.
  163. *
  164. * @return void
  165. *
  166. * @throws \Throwable
  167. */
  168. public function commit()
  169. {
  170. if ($this->transactionLevel() == 1) {
  171. $this->fireConnectionEvent('committing');
  172. $this->getPdo()->commit();
  173. }
  174. [$levelBeingCommitted, $this->transactions] = [
  175. $this->transactions,
  176. max(0, $this->transactions - 1),
  177. ];
  178. $this->transactionsManager?->commit(
  179. $this->getName(), $levelBeingCommitted, $this->transactions
  180. );
  181. $this->fireConnectionEvent('committed');
  182. }
  183. /**
  184. * Handle an exception encountered when committing a transaction.
  185. *
  186. * @param \Throwable $e
  187. * @param int $currentAttempt
  188. * @param int $maxAttempts
  189. * @return void
  190. *
  191. * @throws \Throwable
  192. */
  193. protected function handleCommitTransactionException(Throwable $e, $currentAttempt, $maxAttempts)
  194. {
  195. $this->transactions = max(0, $this->transactions - 1);
  196. if ($this->causedByConcurrencyError($e) && $currentAttempt < $maxAttempts) {
  197. return;
  198. }
  199. if ($this->causedByLostConnection($e)) {
  200. $this->transactions = 0;
  201. }
  202. throw $e;
  203. }
  204. /**
  205. * Rollback the active database transaction.
  206. *
  207. * @param int|null $toLevel
  208. * @return void
  209. *
  210. * @throws \Throwable
  211. */
  212. public function rollBack($toLevel = null)
  213. {
  214. // We allow developers to rollback to a certain transaction level. We will verify
  215. // that this given transaction level is valid before attempting to rollback to
  216. // that level. If it's not we will just return out and not attempt anything.
  217. $toLevel = is_null($toLevel)
  218. ? $this->transactions - 1
  219. : $toLevel;
  220. if ($toLevel < 0 || $toLevel >= $this->transactions) {
  221. return;
  222. }
  223. // Next, we will actually perform this rollback within this database and fire the
  224. // rollback event. We will also set the current transaction level to the given
  225. // level that was passed into this method so it will be right from here out.
  226. try {
  227. $this->performRollBack($toLevel);
  228. } catch (Throwable $e) {
  229. $this->handleRollBackException($e);
  230. }
  231. $this->transactions = $toLevel;
  232. $this->transactionsManager?->rollback(
  233. $this->getName(), $this->transactions
  234. );
  235. $this->fireConnectionEvent('rollingBack');
  236. }
  237. /**
  238. * Perform a rollback within the database.
  239. *
  240. * @param int $toLevel
  241. * @return void
  242. *
  243. * @throws \Throwable
  244. */
  245. protected function performRollBack($toLevel)
  246. {
  247. if ($toLevel == 0) {
  248. $pdo = $this->getPdo();
  249. if ($pdo->inTransaction()) {
  250. $pdo->rollBack();
  251. }
  252. } elseif ($this->queryGrammar->supportsSavepoints()) {
  253. $this->getPdo()->exec(
  254. $this->queryGrammar->compileSavepointRollBack('trans'.($toLevel + 1))
  255. );
  256. }
  257. }
  258. /**
  259. * Handle an exception from a rollback.
  260. *
  261. * @param \Throwable $e
  262. * @return void
  263. *
  264. * @throws \Throwable
  265. */
  266. protected function handleRollBackException(Throwable $e)
  267. {
  268. if ($this->causedByLostConnection($e)) {
  269. $this->transactions = 0;
  270. $this->transactionsManager?->rollback(
  271. $this->getName(), $this->transactions
  272. );
  273. }
  274. throw $e;
  275. }
  276. /**
  277. * Get the number of active transactions.
  278. *
  279. * @return int
  280. */
  281. public function transactionLevel()
  282. {
  283. return $this->transactions;
  284. }
  285. /**
  286. * Execute the callback after a transaction commits.
  287. *
  288. * @param callable $callback
  289. * @return void
  290. *
  291. * @throws \RuntimeException
  292. */
  293. public function afterCommit($callback)
  294. {
  295. if ($this->transactionsManager) {
  296. return $this->transactionsManager->addCallback($callback);
  297. }
  298. throw new RuntimeException('Transactions Manager has not been set.');
  299. }
  300. }