ManagesTransactions.php 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. <?php
  2. namespace Illuminate\Database\Concerns;
  3. use Closure;
  4. use Exception;
  5. use Throwable;
  6. trait ManagesTransactions
  7. {
  8. /**
  9. * Execute a Closure within a transaction.
  10. *
  11. * @param \Closure $callback
  12. * @param int $attempts
  13. * @return mixed
  14. *
  15. * @throws \Exception|\Throwable
  16. */
  17. public function transaction(Closure $callback, $attempts = 1)
  18. {
  19. for ($currentAttempt = 1; $currentAttempt <= $attempts; $currentAttempt++) {
  20. $this->beginTransaction();
  21. // We'll simply execute the given callback within a try / catch block and if we
  22. // catch any exception we can rollback this transaction so that none of this
  23. // gets actually persisted to a database or stored in a permanent fashion.
  24. try {
  25. return tap($callback($this), function () {
  26. $this->commit();
  27. });
  28. }
  29. // If we catch an exception we'll rollback this transaction and try again if we
  30. // are not out of attempts. If we are out of attempts we will just throw the
  31. // exception back out and let the developer handle an uncaught exceptions.
  32. catch (Exception $e) {
  33. $this->handleTransactionException(
  34. $e, $currentAttempt, $attempts
  35. );
  36. } catch (Throwable $e) {
  37. $this->rollBack();
  38. throw $e;
  39. }
  40. }
  41. }
  42. /**
  43. * Handle an exception encountered when running a transacted statement.
  44. *
  45. * @param \Exception $e
  46. * @param int $currentAttempt
  47. * @param int $maxAttempts
  48. * @return void
  49. *
  50. * @throws \Exception
  51. */
  52. protected function handleTransactionException($e, $currentAttempt, $maxAttempts)
  53. {
  54. // On a deadlock, MySQL rolls back the entire transaction so we can't just
  55. // retry the query. We have to throw this exception all the way out and
  56. // let the developer handle it in another way. We will decrement too.
  57. if ($this->causedByDeadlock($e) &&
  58. $this->transactions > 1) {
  59. $this->transactions--;
  60. throw $e;
  61. }
  62. // If there was an exception we will rollback this transaction and then we
  63. // can check if we have exceeded the maximum attempt count for this and
  64. // if we haven't we will return and try this query again in our loop.
  65. $this->rollBack();
  66. if ($this->causedByDeadlock($e) &&
  67. $currentAttempt < $maxAttempts) {
  68. return;
  69. }
  70. throw $e;
  71. }
  72. /**
  73. * Start a new database transaction.
  74. *
  75. * @return void
  76. *
  77. * @throws \Exception
  78. */
  79. public function beginTransaction()
  80. {
  81. $this->createTransaction();
  82. $this->transactions++;
  83. $this->fireConnectionEvent('beganTransaction');
  84. }
  85. /**
  86. * Create a transaction within the database.
  87. *
  88. * @return void
  89. */
  90. protected function createTransaction()
  91. {
  92. if ($this->transactions == 0) {
  93. try {
  94. $this->getPdo()->beginTransaction();
  95. } catch (Exception $e) {
  96. $this->handleBeginTransactionException($e);
  97. }
  98. } elseif ($this->transactions >= 1 && $this->queryGrammar->supportsSavepoints()) {
  99. $this->createSavepoint();
  100. }
  101. }
  102. /**
  103. * Create a save point within the database.
  104. *
  105. * @return void
  106. */
  107. protected function createSavepoint()
  108. {
  109. $this->getPdo()->exec(
  110. $this->queryGrammar->compileSavepoint('trans'.($this->transactions + 1))
  111. );
  112. }
  113. /**
  114. * Handle an exception from a transaction beginning.
  115. *
  116. * @param \Throwable $e
  117. * @return void
  118. *
  119. * @throws \Exception
  120. */
  121. protected function handleBeginTransactionException($e)
  122. {
  123. if ($this->causedByLostConnection($e)) {
  124. $this->reconnect();
  125. $this->pdo->beginTransaction();
  126. } else {
  127. throw $e;
  128. }
  129. }
  130. /**
  131. * Commit the active database transaction.
  132. *
  133. * @return void
  134. */
  135. public function commit()
  136. {
  137. if ($this->transactions == 1) {
  138. $this->getPdo()->commit();
  139. }
  140. $this->transactions = max(0, $this->transactions - 1);
  141. $this->fireConnectionEvent('committed');
  142. }
  143. /**
  144. * Rollback the active database transaction.
  145. *
  146. * @param int|null $toLevel
  147. * @return void
  148. *
  149. * @throws \Exception
  150. */
  151. public function rollBack($toLevel = null)
  152. {
  153. // We allow developers to rollback to a certain transaction level. We will verify
  154. // that this given transaction level is valid before attempting to rollback to
  155. // that level. If it's not we will just return out and not attempt anything.
  156. $toLevel = is_null($toLevel)
  157. ? $this->transactions - 1
  158. : $toLevel;
  159. if ($toLevel < 0 || $toLevel >= $this->transactions) {
  160. return;
  161. }
  162. // Next, we will actually perform this rollback within this database and fire the
  163. // rollback event. We will also set the current transaction level to the given
  164. // level that was passed into this method so it will be right from here out.
  165. try {
  166. $this->performRollBack($toLevel);
  167. } catch (Exception $e) {
  168. $this->handleRollBackException($e);
  169. }
  170. $this->transactions = $toLevel;
  171. $this->fireConnectionEvent('rollingBack');
  172. }
  173. /**
  174. * Perform a rollback within the database.
  175. *
  176. * @param int $toLevel
  177. * @return void
  178. */
  179. protected function performRollBack($toLevel)
  180. {
  181. if ($toLevel == 0) {
  182. $this->getPdo()->rollBack();
  183. } elseif ($this->queryGrammar->supportsSavepoints()) {
  184. $this->getPdo()->exec(
  185. $this->queryGrammar->compileSavepointRollBack('trans'.($toLevel + 1))
  186. );
  187. }
  188. }
  189. /**
  190. * Handle an exception from a rollback.
  191. *
  192. * @param \Exception $e
  193. *
  194. * @throws \Exception
  195. */
  196. protected function handleRollBackException($e)
  197. {
  198. if ($this->causedByLostConnection($e)) {
  199. $this->transactions = 0;
  200. }
  201. throw $e;
  202. }
  203. /**
  204. * Get the number of active transactions.
  205. *
  206. * @return int
  207. */
  208. public function transactionLevel()
  209. {
  210. return $this->transactions;
  211. }
  212. }