ManagesTransactions.php 6.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  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 ($result) {
  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. * @throws \Exception
  77. */
  78. public function beginTransaction()
  79. {
  80. $this->createTransaction();
  81. $this->transactions++;
  82. $this->fireConnectionEvent('beganTransaction');
  83. }
  84. /**
  85. * Create a transaction within the database.
  86. *
  87. * @return void
  88. */
  89. protected function createTransaction()
  90. {
  91. if ($this->transactions == 0) {
  92. try {
  93. $this->getPdo()->beginTransaction();
  94. } catch (Exception $e) {
  95. $this->handleBeginTransactionException($e);
  96. }
  97. } elseif ($this->transactions >= 1 && $this->queryGrammar->supportsSavepoints()) {
  98. $this->createSavepoint();
  99. }
  100. }
  101. /**
  102. * Create a save point within the database.
  103. *
  104. * @return void
  105. */
  106. protected function createSavepoint()
  107. {
  108. $this->getPdo()->exec(
  109. $this->queryGrammar->compileSavepoint('trans'.($this->transactions + 1))
  110. );
  111. }
  112. /**
  113. * Handle an exception from a transaction beginning.
  114. *
  115. * @param \Throwable $e
  116. * @return void
  117. *
  118. * @throws \Exception
  119. */
  120. protected function handleBeginTransactionException($e)
  121. {
  122. if ($this->causedByLostConnection($e)) {
  123. $this->reconnect();
  124. $this->pdo->beginTransaction();
  125. } else {
  126. throw $e;
  127. }
  128. }
  129. /**
  130. * Commit the active database transaction.
  131. *
  132. * @return void
  133. */
  134. public function commit()
  135. {
  136. if ($this->transactions == 1) {
  137. $this->getPdo()->commit();
  138. }
  139. $this->transactions = max(0, $this->transactions - 1);
  140. $this->fireConnectionEvent('committed');
  141. }
  142. /**
  143. * Rollback the active database transaction.
  144. *
  145. * @param int|null $toLevel
  146. * @return void
  147. */
  148. public function rollBack($toLevel = null)
  149. {
  150. // We allow developers to rollback to a certain transaction level. We will verify
  151. // that this given transaction level is valid before attempting to rollback to
  152. // that level. If it's not we will just return out and not attempt anything.
  153. $toLevel = is_null($toLevel)
  154. ? $this->transactions - 1
  155. : $toLevel;
  156. if ($toLevel < 0 || $toLevel >= $this->transactions) {
  157. return;
  158. }
  159. // Next, we will actually perform this rollback within this database and fire the
  160. // rollback event. We will also set the current transaction level to the given
  161. // level that was passed into this method so it will be right from here out.
  162. $this->performRollBack($toLevel);
  163. $this->transactions = $toLevel;
  164. $this->fireConnectionEvent('rollingBack');
  165. }
  166. /**
  167. * Perform a rollback within the database.
  168. *
  169. * @param int $toLevel
  170. * @return void
  171. */
  172. protected function performRollBack($toLevel)
  173. {
  174. if ($toLevel == 0) {
  175. $this->getPdo()->rollBack();
  176. } elseif ($this->queryGrammar->supportsSavepoints()) {
  177. $this->getPdo()->exec(
  178. $this->queryGrammar->compileSavepointRollBack('trans'.($toLevel + 1))
  179. );
  180. }
  181. }
  182. /**
  183. * Get the number of active transactions.
  184. *
  185. * @return int
  186. */
  187. public function transactionLevel()
  188. {
  189. return $this->transactions;
  190. }
  191. }