/ Transactions

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 shortLong transactions lock database resources
Use run() when possibleAutomatic error handling is safer
Don't nest too deepKeep savepoint nesting to 2-3 levels max
Handle exceptionsAlways catch and log transaction failures
Avoid external callsDon'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();
}