# Database Transaction Helper

## RunDbTransaction

`RunDbTransaction` is a fluent helper for running **safe and clean database transactions** in Laravel.  
It handles edge cases around **rollback levels**, especially when using custom exceptions like `DefaultException` (common in our codebase).

### <span aria-expanded="false" class="emojiContainer__75abc emojiContainerClickable__75abc" role="button" tabindex="0">❔</span> Why This Matters

When a `DefaultException` is thrown (one that already performs a rollback), using `DB::beginTransaction()` with a try-catch around it can lead to **double rollback errors** or broken transaction levels.

That's the main problem `RunDbTransaction` solves:

- It **detects if rollback has already happened** (e.g. inside `DefaultException`)
- Prevents **Laravel’s transaction level** from going out of sync
- Lets you **catch and handle exceptions** cleanly — even continue without rollback if needed

It also works great with regular exceptions that don’t trigger rollback on their own.

### ⚠️ try - catch - rollback issue

```
public function run()
{
    DB::beginTransaction();
    try {
        $this->transactionBody();
        DB::commit();
    } catch (\Exception $e) {
        DB::rollback();  //ISSUE: in case of DefaultException this causes over rollback!!!
        ...
    }
}

private function transactionBody($value)
{
	if (!$value) {
    	throw new DefaultException("Missing value!");
    }
    return $value
}
```

When `DefaultException` is thrown rollback is triggered by itself and `DB::rollback()` inside `catch() {...}`over rollback the transaction and rolling back all your current work! While rollback is still needed when normal exceptions are thrown!

#### `RunDbTransaction` is here to solve that !

---

### ✅ Basic Usage

```php
$successful = RunDbTransaction::create()
    ->transaction(fn() => $this->mainTransaction())
    ->run();
```

- Always call `run()` at the end.
- Returns `true` if transaction was **committed**.
- Returns `false` if it was **rolled back**.

---

### ⚙️ Methods

#### → `transaction(callable $callback)`

Defines the main code block that runs inside the transaction.

```php
->transaction(fn() => $this->doSomething())
```

---

#### → `intercept(callable $handler)`

Optional. Runs **only if an exception was thrown and rollback hasn’t yet happened**.

This is where you can handle something like `DefaultException`, and still choose to **commit** the transaction.

```
->intercept(fn($e) => $this->handleBeforeRollback($e))
```

<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary" id="bkmrk--3"></div>- Return **truthy** value → **Commit happens**
- Return **falsy** value → Rollback proceeds

---

#### → `catch(callable $handler)`

Optional. Runs **after rollback**. Even if `intercept` ran but returned false.

```
->catch(fn($e) => $this->handleAfterRollback($e))
```

---

### 🚀 Full Example with All Handlers

<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary" id="bkmrk--6"><div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary"><div class="flex items-center text-token-text-secondary px-4 py-2 text-xs font-sans justify-between h-9 bg-token-sidebar-surface-primary select-none rounded-t-2xl">  
</div></div></div>```
$successful = RunDbTransaction::create()
    ->transaction(fn() => $this->mainTransaction())
    ->intercept(fn($e) => $this->interceptException($e))
    ->catch(fn($e) => $this->catchException($e))
    ->run();
```

---

### 🧪 Force Rollback Manually

You can force a rollback by returning a special flag:

```
$committed = RunDbTransaction::create()
    ->transaction(function () {
        $this->doSomething();

        return RunDbTransaction::DO_ROLLBACK;
    })
    ->run();
```

- Returns `false`
- No exception handlers are triggered
- Transaction is rolled back cleanly

---

### 🔁 Shortcuts

#### `RunDbTransaction::runTransaction(fn() => ...)`

Basic one-liner with safe rollback handling:

```
$success = RunDbTransaction::runTransaction(fn() => $this->mainTransaction());
```

<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary" id="bkmrk-shorthand-for%3A"><div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary"><div class="overflow-y-auto p-4" dir="ltr">shorthand for:</div><div class="overflow-y-auto p-4" dir="ltr">  
</div></div></div>```
$success = RunDbTransaction::create()
	->transaction(fn() => $this->mainTransaction())
    ->run();
```

<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary" id="bkmrk-php-copyedit-%24ok-%3D-r"><div class="overflow-y-auto p-4" dir="ltr">Where no handler is needed just transaction and boolean `Success / Fail` result.</div>---

</div>#### `RunDbTransaction::runOrFail(fn() => ...)`

Same as above, but rethrows the exception if one happens and allows it to propagate further. When you only need managed transaction block:

```
RunDbTransaction::runOrFail(fn() => $this->mainTransaction());
```

Also works with `DO_ROLLBACK` – in that case it rolls back without throwing.  
Also shorthand for:

```
$successful = RunDbTransaction::create()
    ->transaction(fn() => $this->mainTransaction())
    //This shape is required when intercept handler is needed!
    ->intercept(fn($e) => $this->handleBeforeRollback($e))
    ->catch(function ($e) {
    	//handle catch here
    	throw $e;  //rethrow exception to propagate further
    })
    ->run();
```

Use this shorthand when you want error to propagate further and only need save transaction block.

---

### 💡 Summary Table

<div class="_tableContainer_16hzy_1" id="bkmrk-method-description-t"><div class="_tableContainer_16hzy_1"><div class="_tableContainer_16hzy_1"><div class="_tableContainer_16hzy_1"><div class="_tableWrapper_16hzy_14 group flex w-fit flex-col-reverse" tabindex="-1"><table class="w-fit min-w-(--thread-content-width)" data-end="4002" data-start="3040"><thead data-end="3146" data-start="3040"><tr data-end="3146" data-start="3040"><th data-col-size="sm" data-end="3067" data-start="3040">Method</th><th data-col-size="md" data-end="3146" data-start="3067">Description</th></tr></thead><tbody data-end="4002" data-start="3254"><tr data-end="3360" data-start="3254"><td data-col-size="sm" data-end="3281" data-start="3254">`transaction()`</td><td data-col-size="md" data-end="3360" data-start="3281">Required. Code block to wrap in a transaction</td></tr><tr data-end="3467" data-start="3361"><td data-col-size="sm" data-end="3388" data-start="3361">`intercept()`</td><td data-col-size="md" data-end="3467" data-start="3388">Optional. Runs if exception is thrown **before rollback**</td></tr><tr data-end="3574" data-start="3468"><td data-col-size="sm" data-end="3495" data-start="3468">`catch()`</td><td data-col-size="md" data-end="3574" data-start="3495">Optional. Runs **after rollback**, always gets the exception</td></tr><tr data-end="3681" data-start="3575"><td data-col-size="sm" data-end="3602" data-start="3575">`run()`</td><td data-col-size="md" data-end="3681" data-start="3602">Executes the transaction logic</td></tr><tr data-end="3788" data-start="3682"><td data-col-size="sm" data-end="3709" data-start="3682">`runTransaction()`</td><td data-col-size="md" data-end="3788" data-start="3709">Shorthand with rollback detection</td></tr><tr data-end="3895" data-start="3789"><td data-col-size="sm" data-end="3816" data-start="3789">`runOrFail()`</td><td data-col-size="md" data-end="3895" data-start="3816">Shorthand that rethrows exception</td></tr><tr data-end="4002" data-start="3896"><td data-col-size="sm" data-end="3923" data-start="3896">`DO_ROLLBACK`</td><td data-col-size="md" data-end="4002" data-start="3923">Special return value to force rollback without exception</td></tr></tbody></table>

</div></div></div></div></div>---


### 🧱 What About Nested Transactions?

`RunDbTransaction` also handles **multiple levels of transactions** safely — something that can easily go wrong if you're using raw `DB::beginTransaction()` and `DB::commit()` in deeply nested code.

Laravel supports **nested transactions** using savepoints, but if you're not careful, breaking out early or not committing the right number of levels can mess up the whole DB state.

```
function transactionBody() {
    $startLevel = DB::transactionLevel(); // e.g., 1
    DB::beginTransaction(); // level 2

    if ($something) {
        DB::beginTransaction(); // level 3

        // do some work...

        return; // who commits both levels? RunDbTransaction DOES !!!
    }
    ...
}

```

If you would fin yourself in such scenarion in the first place.

Its smarter to nest `RunDbTransaction` blocks

<div class="_tableContainer_16hzy_1" id="bkmrk--12"></div>