Skip to content

Commit

Permalink
Better transaction manager object design (#49103)
Browse files Browse the repository at this point in the history
* better object design

* Apply fixes from StyleCI

* use method for testing

* add or

* fix condition

---------

Co-authored-by: StyleCI Bot <bot@styleci.io>
  • Loading branch information
taylorotwell and StyleCIBot committed Nov 23, 2023
1 parent 99d21f3 commit 0b475bb
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 88 deletions.
40 changes: 17 additions & 23 deletions src/Illuminate/Database/Concerns/ManagesTransactions.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,16 @@ public function transaction(Closure $callback, $attempts = 1)
$this->getPdo()->commit();
}

$this->transactionsManager?->stageTransactions($this->getName(), $this->transactions);

$this->transactions = max(0, $this->transactions - 1);

if ($this->afterCommitCallbacksShouldBeExecuted()) {
$this->transactionsManager?->commit($this->getName());
}
[$levelBeingCommitted, $this->transactions] = [
$this->transactions,
max(0, $this->transactions - 1),
];

$this->transactionsManager?->commit(
$this->getName(),
$levelBeingCommitted,
$this->transactions
);
} catch (Throwable $e) {
$this->handleCommitTransactionException(
$e, $currentAttempt, $attempts
Expand Down Expand Up @@ -196,27 +199,18 @@ public function commit()
$this->getPdo()->commit();
}

$this->transactionsManager?->stageTransactions($this->getName(), $this->transactions);
[$levelBeingCommitted, $this->transactions] = [
$this->transactions,
max(0, $this->transactions - 1),
];

$this->transactions = max(0, $this->transactions - 1);

if ($this->afterCommitCallbacksShouldBeExecuted()) {
$this->transactionsManager?->commit($this->getName());
}
$this->transactionsManager?->commit(
$this->getName(), $levelBeingCommitted, $this->transactions
);

$this->fireConnectionEvent('committed');
}

/**
* Determine if after commit callbacks should be executed.
*
* @return bool
*/
protected function afterCommitCallbacksShouldBeExecuted()
{
return $this->transactionsManager?->afterCommitCallbacksShouldBeExecuted($this->transactions) || $this->transactions == 0;
}

/**
* Handle an exception encountered when committing a transaction.
*
Expand Down
57 changes: 34 additions & 23 deletions src/Illuminate/Database/DatabaseTransactionsManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,39 +59,26 @@ public function begin($connection, $level)
}

/**
* Move relevant pending transactions to a committed state.
* Commit the root database transaction and execute callbacks.
*
* @param string $connection
* @param int $levelBeingCommitted
* @return void
* @param int $newTransactionLevel
* @return array
*/
public function stageTransactions($connection, $levelBeingCommitted)
public function commit($connection, $levelBeingCommitted, $newTransactionLevel)
{
$this->committedTransactions = $this->committedTransactions->merge(
$this->pendingTransactions->filter(
fn ($transaction) => $transaction->connection === $connection &&
$transaction->level >= $levelBeingCommitted
)
);

$this->pendingTransactions = $this->pendingTransactions->reject(
fn ($transaction) => $transaction->connection === $connection &&
$transaction->level >= $levelBeingCommitted
);
$this->stageTransactions($connection, $levelBeingCommitted);

if (isset($this->currentTransaction[$connection])) {
$this->currentTransaction[$connection] = $this->currentTransaction[$connection]->parent;
}
}

/**
* Commit the root database transaction and execute callbacks.
*
* @param string $connection
* @return void
*/
public function commit($connection)
{
if (! $this->afterCommitCallbacksShouldBeExecuted($newTransactionLevel) &&
$newTransactionLevel !== 0) {
return [];
}

// This method is only called when the root database transaction is committed so there
// shouldn't be any pending transactions, but going to clear them here anyways just
// in case. This method could be refactored to receive a level in the future too.
Expand All @@ -106,6 +93,30 @@ public function commit($connection)
$this->committedTransactions = $forOtherConnections->values();

$forThisConnection->map->executeCallbacks();

return $forThisConnection;
}

/**
* Move relevant pending transactions to a committed state.
*
* @param string $connection
* @param int $levelBeingCommitted
* @return void
*/
public function stageTransactions($connection, $levelBeingCommitted)
{
$this->committedTransactions = $this->committedTransactions->merge(
$this->pendingTransactions->filter(
fn ($transaction) => $transaction->connection === $connection &&
$transaction->level >= $levelBeingCommitted
)
);

$this->pendingTransactions = $this->pendingTransactions->reject(
fn ($transaction) => $transaction->connection === $connection &&
$transaction->level >= $levelBeingCommitted
);
}

/**
Expand Down
15 changes: 0 additions & 15 deletions tests/Database/DatabaseConnectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
use Exception;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Connection;
use Illuminate\Database\DatabaseTransactionsManager;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Database\Events\TransactionBeginning;
use Illuminate\Database\Events\TransactionCommitted;
Expand Down Expand Up @@ -290,20 +289,6 @@ public function testCommittingFiresEventsIfSet()
$connection->commit();
}

public function testAfterCommitIsExecutedOnFinalCommit()
{
$pdo = $this->getMockBuilder(DatabaseConnectionTestMockPDO::class)->onlyMethods(['beginTransaction', 'commit'])->getMock();
$transactionsManager = $this->getMockBuilder(DatabaseTransactionsManager::class)->onlyMethods(['afterCommitCallbacksShouldBeExecuted'])->getMock();
$transactionsManager->expects($this->once())->method('afterCommitCallbacksShouldBeExecuted')->with(0)->willReturn(true);

$connection = $this->getMockConnection([], $pdo);
$connection->setTransactionManager($transactionsManager);

$connection->transaction(function () {
// do nothing
});
}

public function testRollBackedFiresEventsIfSet()
{
$pdo = $this->createMock(DatabaseConnectionTestMockPDO::class);
Expand Down
32 changes: 20 additions & 12 deletions tests/Database/DatabaseTransactionsManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,24 @@ public function testCommittingTransactions()
$manager->begin('default', 1);
$manager->begin('default', 2);
$manager->begin('admin', 1);
$manager->begin('admin', 2);

$manager->stageTransactions('default', 1);
$manager->stageTransactions('admin', 1);
$manager->commit('default');
$manager->commit('default', 2, 1);
$executedTransactions = $manager->commit('default', 1, 0);

$this->assertCount(0, $manager->getPendingTransactions());
$this->assertCount(1, $manager->getCommittedTransactions());
$executedAdminTransactions = $manager->commit('admin', 2, 1);

$this->assertCount(1, $manager->getPendingTransactions()); // One pending "admin" transaction left...
$this->assertCount(2, $executedTransactions); // Two committed tranasctions on "default"
$this->assertCount(0, $executedAdminTransactions); // Zero executed committed tranasctions on "default"

// Level 2 "admin" callback has been staged...
$this->assertSame('admin', $manager->getCommittedTransactions()[0]->connection);
$this->assertEquals(1, $manager->getCommittedTransactions()[0]->level);
$this->assertEquals(2, $manager->getCommittedTransactions()[0]->level);

// Level 1 "admin" callback still pending...
$this->assertSame('admin', $manager->getPendingTransactions()[0]->connection);
$this->assertEquals(1, $manager->getPendingTransactions()[0]->level);
}

public function testCallbacksAreAddedToTheCurrentTransaction()
Expand Down Expand Up @@ -121,12 +129,12 @@ public function testCommittingTransactionsExecutesCallbacks()

$manager->begin('admin', 1);

$manager->stageTransactions('default', 1);
$manager->commit('default');
$manager->commit('default', 2, 1);
$manager->commit('default', 1, 0);

$this->assertCount(2, $callbacks);
$this->assertEquals(['default', 1], $callbacks[0]);
$this->assertEquals(['default', 2], $callbacks[1]);
$this->assertEquals(['default', 2], $callbacks[0]);
$this->assertEquals(['default', 1], $callbacks[1]);
}

public function testCommittingExecutesOnlyCallbacksOfTheConnection()
Expand All @@ -148,8 +156,8 @@ public function testCommittingExecutesOnlyCallbacksOfTheConnection()
$callbacks[] = ['admin', 1];
});

$manager->stageTransactions('default', 1);
$manager->commit('default');
$manager->commit('default', 2, 1);
$manager->commit('default', 1, 0);

$this->assertCount(1, $callbacks);
$this->assertEquals(['default', 1], $callbacks[0]);
Expand Down
21 changes: 8 additions & 13 deletions tests/Database/DatabaseTransactionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,7 @@ public function testTransactionIsRecordedAndCommitted()
{
$transactionManager = m::mock(new DatabaseTransactionsManager);
$transactionManager->shouldReceive('begin')->once()->with('default', 1);
$transactionManager->shouldReceive('stageTransactions')->once()->with('default', 1);
$transactionManager->shouldReceive('commit')->once()->with('default');
$transactionManager->shouldReceive('commit')->once()->with('default', 1, 0);

$this->connection()->setTransactionManager($transactionManager);

Expand All @@ -84,8 +83,7 @@ public function testTransactionIsRecordedAndCommittedUsingTheSeparateMethods()
{
$transactionManager = m::mock(new DatabaseTransactionsManager);
$transactionManager->shouldReceive('begin')->once()->with('default', 1);
$transactionManager->shouldReceive('stageTransactions')->once()->with('default', 1);
$transactionManager->shouldReceive('commit')->once()->with('default');
$transactionManager->shouldReceive('commit')->once()->with('default', 1, 0);

$this->connection()->setTransactionManager($transactionManager);

Expand All @@ -105,9 +103,8 @@ public function testNestedTransactionIsRecordedAndCommitted()
$transactionManager = m::mock(new DatabaseTransactionsManager);
$transactionManager->shouldReceive('begin')->once()->with('default', 1);
$transactionManager->shouldReceive('begin')->once()->with('default', 2);
$transactionManager->shouldReceive('stageTransactions')->once()->with('default', 1);
$transactionManager->shouldReceive('stageTransactions')->once()->with('default', 2);
$transactionManager->shouldReceive('commit')->once()->with('default');
$transactionManager->shouldReceive('commit')->once()->with('default', 2, 1);
$transactionManager->shouldReceive('commit')->once()->with('default', 1, 0);

$this->connection()->setTransactionManager($transactionManager);

Expand All @@ -134,11 +131,9 @@ public function testNestedTransactionIsRecordeForDifferentConnectionsdAndCommitt
$transactionManager->shouldReceive('begin')->once()->with('default', 1);
$transactionManager->shouldReceive('begin')->once()->with('second_connection', 1);
$transactionManager->shouldReceive('begin')->once()->with('second_connection', 2);
$transactionManager->shouldReceive('stageTransactions')->once()->with('default', 1);
$transactionManager->shouldReceive('stageTransactions')->once()->with('second_connection', 1);
$transactionManager->shouldReceive('stageTransactions')->once()->with('second_connection', 2);
$transactionManager->shouldReceive('commit')->once()->with('default');
$transactionManager->shouldReceive('commit')->once()->with('second_connection');
$transactionManager->shouldReceive('commit')->once()->with('default', 1, 0);
$transactionManager->shouldReceive('commit')->once()->with('second_connection', 2, 1);
$transactionManager->shouldReceive('commit')->once()->with('second_connection', 1, 0);

$this->connection()->setTransactionManager($transactionManager);
$this->connection('second_connection')->setTransactionManager($transactionManager);
Expand Down Expand Up @@ -196,7 +191,7 @@ public function testTransactionIsRolledBackUsingSeparateMethods()
$transactionManager = m::mock(new DatabaseTransactionsManager);
$transactionManager->shouldReceive('begin')->once()->with('default', 1);
$transactionManager->shouldReceive('rollback')->once()->with('default', 0);
$transactionManager->shouldNotReceive('commit');
$transactionManager->shouldNotReceive('commit', 1, 0);

$this->connection()->setTransactionManager($transactionManager);

Expand Down
4 changes: 2 additions & 2 deletions tests/Foundation/Testing/DatabaseTransactionsManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ public function testItExecutesCallbacksForTheSecondTransaction()

$this->assertFalse($testObject->ran);

$manager->stageTransactions('foo', 1);
$manager->commit('foo');
$manager->commit('foo', 2, 1);
$manager->commit('foo', 1, 0);
$this->assertTrue($testObject->ran);
$this->assertEquals(1, $testObject->runs);
}
Expand Down

0 comments on commit 0b475bb

Please sign in to comment.