Transactions
Miko ORM provides a clean API for database transactions. Ensure data integrity with automatic commit and rollback support.
Transaction Methods Summary
| Method | Description |
|---|---|
Transaction::run() | Execute callback in transaction |
Transaction::tryRun() | Execute and return bool (no exception) |
Transaction::begin() | Start transaction manually |
Transaction::commit() | Commit transaction |
Transaction::rollback() | Rollback transaction |
Transaction::savepoint() | Create savepoint for nested transactions |
Transaction::inTransaction() | Check if in transaction |
Transaction::getLevel() | Get nesting level |
Basic Usage
Using run() - Recommended
The run() method automatically handles commit and rollback.
use Miko\Database\ORM\Transaction;
// Simple transaction
Transaction::run(function() {
$user = User::create([
'Name' => 'John Doe',
'Email' => 'john@example.com'
]);
Order::create([
'UserId' => $user->Id,
'TotalAmount' => 100.00
]);
});
// Auto-commits on success, auto-rollbacks on exception
With Return Value
$order = Transaction::run(function() {
$user = User::create(['Name' => 'John']);
$order = Order::create([
'UserId' => $user->Id,
'OrderNumber' => 'ORD-001'
]);
return $order;
});
echo $order->OrderNumber; // "ORD-001"
tryRun() - Boolean Result
Returns true on success, false on failure (no exception thrown).
$success = Transaction::tryRun(function() {
$user = User::create(['Name' => 'John']);
$order = Order::create(['UserId' => $user->Id]);
});
if ($success) {
echo "Transaction completed successfully";
} else {
echo "Transaction failed";
}
With Exception Output
$exception = null;
$success = Transaction::tryRun(function() {
// Operations that might fail
$user = User::create(['Name' => 'John']);
if (!$user->Id) {
throw new Exception("Failed to create user");
}
}, $exception);
if (!$success) {
Logger::error("Transaction failed: " . $exception->getMessage());
}
Manual Transaction Control
For more control, use manual begin/commit/rollback.
Transaction::begin();
try {
$user = new User(['Name' => 'John']);
$user->save();
$profile = new Profile(['UserId' => $user->Id]);
$profile->save();
$order = new Order(['UserId' => $user->Id]);
$order->save();
Transaction::commit();
} catch (Exception $e) {
Transaction::rollback();
throw $e;
}
Nested Transactions (Savepoints)
Use savepoints for partial rollback in nested operations.
Transaction::run(function() {
// Main transaction
$user = User::create(['Name' => 'John']);
// Nested savepoint
Transaction::savepoint('create_orders', function() use ($user) {
Order::create(['UserId' => $user->Id, 'Total' => 100]);
Order::create(['UserId' => $user->Id, 'Total' => 200]);
});
// Another savepoint
Transaction::savepoint('create_profile', function() use ($user) {
Profile::create(['UserId' => $user->Id]);
});
});
Savepoint with Error Handling
Transaction::run(function() {
$user = User::create(['Name' => 'John']);
try {
Transaction::savepoint('optional_operation', function() use ($user) {
// This might fail
ExternalService::sync($user);
});
} catch (Exception $e) {
// Savepoint rolled back, but main transaction continues
Logger::warning("Optional operation failed: " . $e->getMessage());
}
// Continue with main transaction
$user->Status = 'active';
$user->save();
});
Transaction State
Check If In Transaction
if (Transaction::inTransaction()) {
echo "Currently in a transaction";
}
Get Nesting Level
echo Transaction::getLevel(); // 0 = not in transaction
Transaction::begin();
echo Transaction::getLevel(); // 1
Transaction::begin(); // Nested
echo Transaction::getLevel(); // 2
Transaction::commit();
echo Transaction::getLevel(); // 1
Transaction::commit();
echo Transaction::getLevel(); // 0
Practical Examples
E-Commerce Order Processing
function processOrder(array $cartItems, int $userId): Order
{
return Transaction::run(function() use ($cartItems, $userId) {
// Create order
$order = Order::create([
'UserId' => $userId,
'OrderNumber' => 'ORD-' . time(),
'Status' => 'pending',
'TotalAmount' => 0
]);
$total = 0;
// Create order items and update stock
foreach ($cartItems as $item) {
$product = Product::findOrFail($item['product_id']);
// Check stock
if ($product->Stock < $item['quantity']) {
throw new Exception("Insufficient stock for: {$product->Name}");
}
// Create order item
OrderItem::create([
'OrderId' => $order->Id,
'ProductId' => $product->Id,
'Quantity' => $item['quantity'],
'UnitPrice' => $product->Price,
'TotalPrice' => $product->Price * $item['quantity']
]);
// Decrease stock
$product->decrement('Stock', $item['quantity']);
$total += $product->Price * $item['quantity'];
}
// Update order total
$order->TotalAmount = $total;
$order->save();
// Clear user's cart
CartItem::where('UserId', $userId)->delete();
return $order;
});
}
// Usage
try {
$order = processOrder($cartItems, $userId);
echo "Order created: " . $order->OrderNumber;
} catch (Exception $e) {
echo "Order failed: " . $e->getMessage();
}
Bank Transfer
function transferMoney(int $fromAccountId, int $toAccountId, float $amount): bool
{
return Transaction::tryRun(function() use ($fromAccountId, $toAccountId, $amount) {
$fromAccount = Account::findOrFail($fromAccountId);
$toAccount = Account::findOrFail($toAccountId);
// Validate
if ($fromAccount->Balance < $amount) {
throw new Exception("Insufficient balance");
}
// Debit from source
$fromAccount->decrement('Balance', $amount);
// Credit to destination
$toAccount->increment('Balance', $amount);
// Log transactions
TransactionLog::create([
'AccountId' => $fromAccountId,
'Type' => 'debit',
'Amount' => $amount,
'Description' => "Transfer to account #$toAccountId"
]);
TransactionLog::create([
'AccountId' => $toAccountId,
'Type' => 'credit',
'Amount' => $amount,
'Description' => "Transfer from account #$fromAccountId"
]);
});
}
// Usage
if (transferMoney(1, 2, 500.00)) {
echo "Transfer successful";
} else {
echo "Transfer failed";
}
User Registration with Profile
function registerUser(array $userData): User
{
return Transaction::run(function() use ($userData) {
// Create user
$user = User::create([
'Name' => $userData['name'],
'Email' => $userData['email'],
'Password' => password_hash($userData['password'], PASSWORD_DEFAULT)
]);
// Create profile
Profile::create([
'UserId' => $user->Id,
'Bio' => $userData['bio'] ?? '',
'Avatar' => 'default.png'
]);
// Assign default role
$user->roles()->attach(Role::where('Name', 'user')->first()->Id);
// Create welcome notification
Notification::create([
'UserId' => $user->Id,
'Title' => 'Welcome!',
'Message' => 'Thanks for joining us.'
]);
return $user;
});
}
Best Practices
| Practice | Description |
|---|---|
| Keep transactions short | Long transactions lock database resources |
| Use run() when possible | Automatic error handling is safer |
| Don't nest too deep | Keep savepoint nesting to 2-3 levels max |
| Handle exceptions | Always catch and log transaction failures |
| Avoid external calls | Don't make API calls inside transactions |
// BAD - API call inside transaction
Transaction::run(function() {
$order = Order::create([...]);
PaymentGateway::charge($order); // External API - BAD!
});
// GOOD - API call outside transaction
$order = Transaction::run(function() {
return Order::create([...]);
});
try {
PaymentGateway::charge($order);
$order->Status = 'paid';
$order->save();
} catch (Exception $e) {
$order->Status = 'payment_failed';
$order->save();
}