diff --git a/.github/workflows/build-ci.yml b/.github/workflows/build-ci.yml
index 6f57f015d..f081e3273 100644
--- a/.github/workflows/build-ci.yml
+++ b/.github/workflows/build-ci.yml
@@ -44,10 +44,6 @@ jobs:
- '8.0'
- '8.1'
services:
- mongo:
- image: mongo:${{ matrix.mongodb }}
- ports:
- - 27017:27017
mysql:
image: mysql:5.7
ports:
@@ -59,6 +55,16 @@ jobs:
steps:
- uses: actions/checkout@v2
+ - name: Create MongoDB Replica Set
+ run: |
+ docker run --name mongodb -p 27017:27017 -e MONGO_INITDB_DATABASE=unittest --detach mongo:${{ matrix.mongodb }} mongod --replSet rs --setParameter transactionLifetimeLimitSeconds=5
+ until docker exec --tty mongodb mongo 127.0.0.1:27017 --eval "db.runCommand({ ping: 1 })"; do
+ sleep 1
+ done
+ sudo docker exec --tty mongodb mongo 127.0.0.1:27017 --eval "rs.initiate({\"_id\":\"rs\",\"members\":[{\"_id\":0,\"host\":\"127.0.0.1:27017\" }]})"
+ - name: Show MongoDB server status
+ run: |
+ docker exec --tty mongodb mongo 127.0.0.1:27017 --eval "db.runCommand({ serverStatus: 1 })"
- name: "Installing php"
uses: shivammathur/setup-php@v2
with:
@@ -88,7 +94,7 @@ jobs:
run: |
./vendor/bin/phpunit --coverage-clover coverage.xml
env:
- MONGODB_URI: 'mongodb://127.0.0.1/'
+ MONGODB_URI: 'mongodb://127.0.0.1/?replicaSet=rs'
MYSQL_HOST: 0.0.0.0
MYSQL_PORT: 3307
- uses: codecov/codecov-action@v1
diff --git a/README.md b/README.md
index 06531dcb1..0c07e7288 100644
--- a/README.md
+++ b/README.md
@@ -37,6 +37,7 @@ This package adds functionalities to the Eloquent model and Query builder for Mo
- [Query Builder](#query-builder)
- [Basic Usage](#basic-usage-2)
- [Available operations](#available-operations)
+ - [Transactions](#transactions)
- [Schema](#schema)
- [Basic Usage](#basic-usage-3)
- [Geospatial indexes](#geospatial-indexes)
@@ -968,6 +969,52 @@ If you are familiar with [Eloquent Queries](http://laravel.com/docs/queries), th
### Available operations
To see the available operations, check the [Eloquent](#eloquent) section.
+Transactions
+------------
+Transactions require MongoDB version ^4.0 as well as deployment of replica set or sharded clusters. You can find more information [in the MongoDB docs](https://docs.mongodb.com/manual/core/transactions/)
+
+### Basic Usage
+
+```php
+DB::transaction(function () {
+ User::create(['name' => 'john', 'age' => 19, 'title' => 'admin', 'email' => 'john@example.com']);
+ DB::collection('users')->where('name', 'john')->update(['age' => 20]);
+ DB::collection('users')->where('name', 'john')->delete();
+});
+```
+
+```php
+// begin a transaction
+DB::beginTransaction();
+User::create(['name' => 'john', 'age' => 19, 'title' => 'admin', 'email' => 'john@example.com']);
+DB::collection('users')->where('name', 'john')->update(['age' => 20]);
+DB::collection('users')->where('name', 'john')->delete();
+
+// commit changes
+DB::commit();
+```
+
+To abort a transaction, call the `rollBack` method at any point during the transaction:
+```php
+DB::beginTransaction();
+User::create(['name' => 'john', 'age' => 19, 'title' => 'admin', 'email' => 'john@example.com']);
+
+// Abort the transaction, discarding any data created as part of it
+DB::rollBack();
+```
+
+**NOTE:** Transactions in MongoDB cannot be nested. DB::beginTransaction() function will start new transactions in a new created or existing session and will raise the RuntimeException when transactions already exist. See more in MongoDB official docs [Transactions and Sessions](https://www.mongodb.com/docs/manual/core/transactions/#transactions-and-sessions)
+```php
+DB::beginTransaction();
+User::create(['name' => 'john', 'age' => 20, 'title' => 'admin']);
+
+// This call to start a nested transaction will raise a RuntimeException
+DB::beginTransaction();
+DB::collection('users')->where('name', 'john')->update(['age' => 20]);
+DB::commit();
+DB::rollBack();
+```
+
Schema
------
The database driver also has (limited) schema builder support. You can easily manipulate collections and set indexes.
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 15601b8dc..9aebe0c0a 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -19,6 +19,9 @@
tests/QueryBuilderTest.php
tests/QueryTest.php
+
+ tests/TransactionTest.php
+
tests/ModelTest.php
tests/RelationsTest.php
@@ -36,7 +39,7 @@
-
+
diff --git a/src/Concerns/ManagesTransactions.php b/src/Concerns/ManagesTransactions.php
new file mode 100644
index 000000000..d3344f919
--- /dev/null
+++ b/src/Concerns/ManagesTransactions.php
@@ -0,0 +1,116 @@
+session;
+ }
+
+ private function getSessionOrCreate(): Session
+ {
+ if ($this->session === null) {
+ $this->session = $this->getMongoClient()->startSession();
+ }
+
+ return $this->session;
+ }
+
+ private function getSessionOrThrow(): Session
+ {
+ $session = $this->getSession();
+
+ if ($session === null) {
+ throw new RuntimeException('There is no active session.');
+ }
+
+ return $session;
+ }
+
+ /**
+ * Starts a transaction on the active session. An active session will be created if none exists.
+ */
+ public function beginTransaction(array $options = []): void
+ {
+ $this->getSessionOrCreate()->startTransaction($options);
+ $this->transactions = 1;
+ }
+
+ /**
+ * Commit transaction in this session.
+ */
+ public function commit(): void
+ {
+ $this->getSessionOrThrow()->commitTransaction();
+ $this->transactions = 0;
+ }
+
+ /**
+ * Abort transaction in this session.
+ */
+ public function rollBack($toLevel = null): void
+ {
+ $this->getSessionOrThrow()->abortTransaction();
+ $this->transactions = 0;
+ }
+
+ /**
+ * Static transaction function realize the with_transaction functionality provided by MongoDB.
+ *
+ * @param int $attempts
+ */
+ public function transaction(Closure $callback, $attempts = 1, array $options = []): mixed
+ {
+ $attemptsLeft = $attempts;
+ $callbackResult = null;
+ $throwable = null;
+
+ $callbackFunction = function (Session $session) use ($callback, &$attemptsLeft, &$callbackResult, &$throwable) {
+ $attemptsLeft--;
+
+ if ($attemptsLeft < 0) {
+ $session->abortTransaction();
+
+ return;
+ }
+
+ // Catch, store, and re-throw any exception thrown during execution
+ // of the callable. The last exception is re-thrown if the transaction
+ // was aborted because the number of callback attempts has been exceeded.
+ try {
+ $callbackResult = $callback($this);
+ } catch (Throwable $throwable) {
+ throw $throwable;
+ }
+ };
+
+ with_transaction($this->getSessionOrCreate(), $callbackFunction, $options);
+
+ if ($attemptsLeft < 0 && $throwable) {
+ throw $throwable;
+ }
+
+ return $callbackResult;
+ }
+}
diff --git a/src/Connection.php b/src/Connection.php
index b65b40ca3..c78ac95c1 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -5,11 +5,14 @@
use Illuminate\Database\Connection as BaseConnection;
use Illuminate\Support\Arr;
use InvalidArgumentException;
+use Jenssegers\Mongodb\Concerns\ManagesTransactions;
use MongoDB\Client;
use MongoDB\Database;
class Connection extends BaseConnection
{
+ use ManagesTransactions;
+
/**
* The MongoDB database handler.
*
diff --git a/src/Query/Builder.php b/src/Query/Builder.php
index 631e64950..066412734 100644
--- a/src/Query/Builder.php
+++ b/src/Query/Builder.php
@@ -346,6 +346,8 @@ public function getFresh($columns = [], $returnLazy = false)
$options = array_merge($options, $this->options);
}
+ $options = $this->inheritConnectionOptions($options);
+
// Execute aggregation
$results = iterator_to_array($this->collection->aggregate($pipeline, $options));
@@ -356,12 +358,10 @@ public function getFresh($columns = [], $returnLazy = false)
// Return distinct results directly
$column = isset($this->columns[0]) ? $this->columns[0] : '_id';
+ $options = $this->inheritConnectionOptions();
+
// Execute distinct
- if ($wheres) {
- $result = $this->collection->distinct($column, $wheres);
- } else {
- $result = $this->collection->distinct($column);
- }
+ $result = $this->collection->distinct($column, $wheres ?: [], $options);
return new Collection($result);
} // Normal query
@@ -407,6 +407,8 @@ public function getFresh($columns = [], $returnLazy = false)
$options = array_merge($options, $this->options);
}
+ $options = $this->inheritConnectionOptions($options);
+
// Execute query and get MongoCursor
$cursor = $this->collection->find($wheres, $options);
@@ -581,8 +583,9 @@ public function insert(array $values)
$values = [$values];
}
- // Batch insert
- $result = $this->collection->insertMany($values);
+ $options = $this->inheritConnectionOptions();
+
+ $result = $this->collection->insertMany($values, $options);
return 1 == (int) $result->isAcknowledged();
}
@@ -592,7 +595,9 @@ public function insert(array $values)
*/
public function insertGetId(array $values, $sequence = null)
{
- $result = $this->collection->insertOne($values);
+ $options = $this->inheritConnectionOptions();
+
+ $result = $this->collection->insertOne($values, $options);
if (1 == (int) $result->isAcknowledged()) {
if ($sequence === null) {
@@ -614,6 +619,8 @@ public function update(array $values, array $options = [])
$values = ['$set' => $values];
}
+ $options = $this->inheritConnectionOptions($options);
+
return $this->performUpdate($values, $options);
}
@@ -635,6 +642,8 @@ public function increment($column, $amount = 1, array $extra = [], array $option
$query->orWhereNotNull($column);
});
+ $options = $this->inheritConnectionOptions($options);
+
return $this->performUpdate($query, $options);
}
@@ -696,7 +705,10 @@ public function delete($id = null)
}
$wheres = $this->compileWheres();
- $result = $this->collection->DeleteMany($wheres);
+ $options = $this->inheritConnectionOptions();
+
+ $result = $this->collection->deleteMany($wheres, $options);
+
if (1 == (int) $result->isAcknowledged()) {
return $result->getDeletedCount();
}
@@ -721,7 +733,8 @@ public function from($collection, $as = null)
*/
public function truncate(): bool
{
- $result = $this->collection->deleteMany([]);
+ $options = $this->inheritConnectionOptions();
+ $result = $this->collection->deleteMany([], $options);
return 1 === (int) $result->isAcknowledged();
}
@@ -855,6 +868,8 @@ protected function performUpdate($query, array $options = [])
$options['multiple'] = true;
}
+ $options = $this->inheritConnectionOptions($options);
+
$wheres = $this->compileWheres();
$result = $this->collection->UpdateMany($wheres, $query, $options);
if (1 == (int) $result->isAcknowledged()) {
@@ -1249,6 +1264,18 @@ public function options(array $options)
return $this;
}
+ /**
+ * Apply the connection's session to options if it's not already specified.
+ */
+ private function inheritConnectionOptions(array $options = []): array
+ {
+ if (! isset($options['session']) && ($session = $this->connection->getSession())) {
+ $options['session'] = $session;
+ }
+
+ return $options;
+ }
+
/**
* @inheritdoc
*/
diff --git a/tests/TransactionTest.php b/tests/TransactionTest.php
new file mode 100644
index 000000000..52ce422a7
--- /dev/null
+++ b/tests/TransactionTest.php
@@ -0,0 +1,448 @@
+getPrimaryServerType() === Server::TYPE_STANDALONE) {
+ $this->markTestSkipped('Transactions are not supported on standalone servers');
+ }
+
+ User::truncate();
+ }
+
+ public function tearDown(): void
+ {
+ User::truncate();
+
+ parent::tearDown();
+ }
+
+ public function testCreateWithCommit(): void
+ {
+ DB::beginTransaction();
+ /** @var User $klinson */
+ $klinson = User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
+ DB::commit();
+
+ $this->assertInstanceOf(Model::class, $klinson);
+ $this->assertTrue($klinson->exists);
+ $this->assertEquals('klinson', $klinson->name);
+
+ $check = User::find($klinson->_id);
+ $this->assertInstanceOf(User::class, $check);
+ $this->assertEquals($klinson->name, $check->name);
+ }
+
+ public function testCreateRollBack(): void
+ {
+ DB::beginTransaction();
+ /** @var User $klinson */
+ $klinson = User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
+ DB::rollBack();
+
+ $this->assertInstanceOf(Model::class, $klinson);
+ $this->assertTrue($klinson->exists);
+ $this->assertEquals('klinson', $klinson->name);
+
+ $this->assertFalse(User::where('_id', $klinson->_id)->exists());
+ }
+
+ public function testInsertWithCommit(): void
+ {
+ DB::beginTransaction();
+ DB::collection('users')->insert(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
+ DB::commit();
+
+ $this->assertTrue(DB::collection('users')->where('name', 'klinson')->exists());
+ }
+
+ public function testInsertWithRollBack(): void
+ {
+ DB::beginTransaction();
+ DB::collection('users')->insert(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
+ DB::rollBack();
+
+ $this->assertFalse(DB::collection('users')->where('name', 'klinson')->exists());
+ }
+
+ public function testEloquentCreateWithCommit(): void
+ {
+ DB::beginTransaction();
+ /** @var User $klinson */
+ $klinson = User::getModel();
+ $klinson->name = 'klinson';
+ $klinson->save();
+ DB::commit();
+
+ $this->assertTrue($klinson->exists);
+ $this->assertNotNull($klinson->getIdAttribute());
+
+ $check = User::find($klinson->_id);
+ $this->assertInstanceOf(User::class, $check);
+ $this->assertEquals($check->name, $klinson->name);
+ }
+
+ public function testEloquentCreateWithRollBack(): void
+ {
+ DB::beginTransaction();
+ /** @var User $klinson */
+ $klinson = User::getModel();
+ $klinson->name = 'klinson';
+ $klinson->save();
+ DB::rollBack();
+
+ $this->assertTrue($klinson->exists);
+ $this->assertNotNull($klinson->getIdAttribute());
+
+ $this->assertFalse(User::where('_id', $klinson->_id)->exists());
+ }
+
+ public function testInsertGetIdWithCommit(): void
+ {
+ DB::beginTransaction();
+ $userId = DB::collection('users')->insertGetId(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
+ DB::commit();
+
+ $this->assertInstanceOf(ObjectId::class, $userId);
+
+ $user = DB::collection('users')->find((string) $userId);
+ $this->assertEquals('klinson', $user['name']);
+ }
+
+ public function testInsertGetIdWithRollBack(): void
+ {
+ DB::beginTransaction();
+ $userId = DB::collection('users')->insertGetId(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
+ DB::rollBack();
+
+ $this->assertInstanceOf(ObjectId::class, $userId);
+ $this->assertFalse(DB::collection('users')->where('_id', (string) $userId)->exists());
+ }
+
+ public function testUpdateWithCommit(): void
+ {
+ User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
+
+ DB::beginTransaction();
+ $updated = DB::collection('users')->where('name', 'klinson')->update(['age' => 21]);
+ DB::commit();
+
+ $this->assertEquals(1, $updated);
+ $this->assertTrue(DB::collection('users')->where('name', 'klinson')->where('age', 21)->exists());
+ }
+
+ public function testUpdateWithRollback(): void
+ {
+ User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
+
+ DB::beginTransaction();
+ $updated = DB::collection('users')->where('name', 'klinson')->update(['age' => 21]);
+ DB::rollBack();
+
+ $this->assertEquals(1, $updated);
+ $this->assertFalse(DB::collection('users')->where('name', 'klinson')->where('age', 21)->exists());
+ }
+
+ public function testEloquentUpdateWithCommit(): void
+ {
+ /** @var User $klinson */
+ $klinson = User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
+ /** @var User $alcaeus */
+ $alcaeus = User::create(['name' => 'alcaeus', 'age' => 38, 'title' => 'admin']);
+
+ DB::beginTransaction();
+ $klinson->age = 21;
+ $klinson->update();
+
+ $alcaeus->update(['age' => 39]);
+ DB::commit();
+
+ $this->assertEquals(21, $klinson->age);
+ $this->assertEquals(39, $alcaeus->age);
+
+ $this->assertTrue(User::where('_id', $klinson->_id)->where('age', 21)->exists());
+ $this->assertTrue(User::where('_id', $alcaeus->_id)->where('age', 39)->exists());
+ }
+
+ public function testEloquentUpdateWithRollBack(): void
+ {
+ /** @var User $klinson */
+ $klinson = User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
+ /** @var User $alcaeus */
+ $alcaeus = User::create(['name' => 'klinson', 'age' => 38, 'title' => 'admin']);
+
+ DB::beginTransaction();
+ $klinson->age = 21;
+ $klinson->update();
+
+ $alcaeus->update(['age' => 39]);
+ DB::rollBack();
+
+ $this->assertEquals(21, $klinson->age);
+ $this->assertEquals(39, $alcaeus->age);
+
+ $this->assertFalse(User::where('_id', $klinson->_id)->where('age', 21)->exists());
+ $this->assertFalse(User::where('_id', $alcaeus->_id)->where('age', 39)->exists());
+ }
+
+ public function testDeleteWithCommit(): void
+ {
+ User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
+
+ DB::beginTransaction();
+ $deleted = User::where(['name' => 'klinson'])->delete();
+ DB::commit();
+
+ $this->assertEquals(1, $deleted);
+ $this->assertFalse(User::where(['name' => 'klinson'])->exists());
+ }
+
+ public function testDeleteWithRollBack(): void
+ {
+ User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
+
+ DB::beginTransaction();
+ $deleted = User::where(['name' => 'klinson'])->delete();
+ DB::rollBack();
+
+ $this->assertEquals(1, $deleted);
+ $this->assertTrue(User::where(['name' => 'klinson'])->exists());
+ }
+
+ public function testEloquentDeleteWithCommit(): void
+ {
+ /** @var User $klinson */
+ $klinson = User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
+
+ DB::beginTransaction();
+ $klinson->delete();
+ DB::commit();
+
+ $this->assertFalse(User::where('_id', $klinson->_id)->exists());
+ }
+
+ public function testEloquentDeleteWithRollBack(): void
+ {
+ /** @var User $klinson */
+ $klinson = User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
+
+ DB::beginTransaction();
+ $klinson->delete();
+ DB::rollBack();
+
+ $this->assertTrue(User::where('_id', $klinson->_id)->exists());
+ }
+
+ public function testIncrementWithCommit(): void
+ {
+ User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
+
+ DB::beginTransaction();
+ DB::collection('users')->where('name', 'klinson')->increment('age');
+ DB::commit();
+
+ $this->assertTrue(DB::collection('users')->where('name', 'klinson')->where('age', 21)->exists());
+ }
+
+ public function testIncrementWithRollBack(): void
+ {
+ User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
+
+ DB::beginTransaction();
+ DB::collection('users')->where('name', 'klinson')->increment('age');
+ DB::rollBack();
+
+ $this->assertTrue(DB::collection('users')->where('name', 'klinson')->where('age', 20)->exists());
+ }
+
+ public function testDecrementWithCommit(): void
+ {
+ User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
+
+ DB::beginTransaction();
+ DB::collection('users')->where('name', 'klinson')->decrement('age');
+ DB::commit();
+
+ $this->assertTrue(DB::collection('users')->where('name', 'klinson')->where('age', 19)->exists());
+ }
+
+ public function testDecrementWithRollBack(): void
+ {
+ User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
+
+ DB::beginTransaction();
+ DB::collection('users')->where('name', 'klinson')->decrement('age');
+ DB::rollBack();
+
+ $this->assertTrue(DB::collection('users')->where('name', 'klinson')->where('age', 20)->exists());
+ }
+
+ public function testQuery()
+ {
+ /** rollback test */
+ DB::beginTransaction();
+ $count = DB::collection('users')->count();
+ $this->assertEquals(0, $count);
+ DB::collection('users')->insert(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
+ $count = DB::collection('users')->count();
+ $this->assertEquals(1, $count);
+ DB::rollBack();
+
+ $count = DB::collection('users')->count();
+ $this->assertEquals(0, $count);
+
+ /** commit test */
+ DB::beginTransaction();
+ $count = DB::collection('users')->count();
+ $this->assertEquals(0, $count);
+ DB::collection('users')->insert(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
+ $count = DB::collection('users')->count();
+ $this->assertEquals(1, $count);
+ DB::commit();
+
+ $count = DB::collection('users')->count();
+ $this->assertEquals(1, $count);
+ }
+
+ public function testTransaction(): void
+ {
+ User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
+
+ // The $connection parameter may be unused, but is implicitly used to
+ // test that the closure is executed with the connection as an argument.
+ DB::transaction(function (Connection $connection): void {
+ User::create(['name' => 'alcaeus', 'age' => 38, 'title' => 'admin']);
+ User::where(['name' => 'klinson'])->update(['age' => 21]);
+ });
+
+ $count = User::count();
+ $this->assertEquals(2, $count);
+
+ $this->assertTrue(User::where('alcaeus')->exists());
+ $this->assertTrue(User::where(['name' => 'klinson'])->where('age', 21)->exists());
+ }
+
+ public function testTransactionRepeatsOnTransientFailure(): void
+ {
+ User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
+
+ $timesRun = 0;
+
+ DB::transaction(function () use (&$timesRun): void {
+ $timesRun++;
+
+ // Run a query to start the transaction on the server
+ User::create(['name' => 'alcaeus', 'age' => 38, 'title' => 'admin']);
+
+ // Update user outside of the session
+ if ($timesRun == 1) {
+ DB::getCollection('users')->updateOne(['name' => 'klinson'], ['$set' => ['age' => 22]]);
+ }
+
+ // This update will create a write conflict, aborting the transaction
+ User::where(['name' => 'klinson'])->update(['age' => 21]);
+ }, 2);
+
+ $this->assertSame(2, $timesRun);
+ $this->assertTrue(User::where(['name' => 'klinson'])->where('age', 21)->exists());
+ }
+
+ public function testTransactionRespectsRepetitionLimit(): void
+ {
+ $klinson = User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
+
+ $timesRun = 0;
+
+ try {
+ DB::transaction(function () use (&$timesRun): void {
+ $timesRun++;
+
+ // Run a query to start the transaction on the server
+ User::create(['name' => 'alcaeus', 'age' => 38, 'title' => 'admin']);
+
+ // Update user outside of the session
+ DB::getCollection('users')->updateOne(['name' => 'klinson'], ['$inc' => ['age' => 2]]);
+
+ // This update will create a write conflict, aborting the transaction
+ User::where(['name' => 'klinson'])->update(['age' => 21]);
+ }, 2);
+
+ $this->fail('Expected exception during transaction');
+ } catch (BulkWriteException $e) {
+ $this->assertInstanceOf(BulkWriteException::class, $e);
+ $this->assertStringContainsString('WriteConflict', $e->getMessage());
+ }
+
+ $this->assertSame(2, $timesRun);
+
+ $check = User::find($klinson->_id);
+ $this->assertInstanceOf(User::class, $check);
+
+ // Age is expected to be 24: the callback is executed twice, incrementing age by 2 every time
+ $this->assertSame(24, $check->age);
+ }
+
+ public function testTransactionReturnsCallbackResult(): void
+ {
+ $result = DB::transaction(function (): User {
+ return User::create(['name' => 'klinson', 'age' => 20, 'title' => 'admin']);
+ });
+
+ $this->assertInstanceOf(User::class, $result);
+ $this->assertEquals($result->title, 'admin');
+ $this->assertSame(1, User::count());
+ }
+
+ public function testNestedTransactionsCauseException(): void
+ {
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('Transaction already in progress');
+
+ DB::beginTransaction();
+ DB::beginTransaction();
+ DB::commit();
+ DB::rollBack();
+ }
+
+ public function testNestingTransactionInManualTransaction()
+ {
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('Transaction already in progress');
+
+ DB::beginTransaction();
+ DB::transaction(function (): void {
+ });
+ DB::rollBack();
+ }
+
+ public function testCommitWithoutSession(): void
+ {
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('There is no active session.');
+
+ DB::commit();
+ }
+
+ public function testRollBackWithoutSession(): void
+ {
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('There is no active session.');
+
+ DB::rollback();
+ }
+
+ private function getPrimaryServerType(): int
+ {
+ return DB::getMongoClient()->getManager()->selectServer()->getType();
+ }
+}
diff --git a/tests/config/database.php b/tests/config/database.php
index 73f3d8697..498e4e7e0 100644
--- a/tests/config/database.php
+++ b/tests/config/database.php
@@ -6,7 +6,11 @@
'name' => 'mongodb',
'driver' => 'mongodb',
'dsn' => env('MONGODB_URI', 'mongodb://127.0.0.1/'),
- 'database' => env('MONGO_DATABASE', 'unittest'),
+ 'database' => env('MONGODB_DATABASE', 'unittest'),
+ 'options' => [
+ 'connectTimeoutMS' => 100,
+ 'serverSelectionTimeoutMS' => 250,
+ ],
],
'mysql' => [
diff --git a/tests/config/queue.php b/tests/config/queue.php
index 7d52487fa..d287780e9 100644
--- a/tests/config/queue.php
+++ b/tests/config/queue.php
@@ -16,7 +16,7 @@
],
'failed' => [
- 'database' => env('MONGO_DATABASE'),
+ 'database' => env('MONGODB_DATABASE'),
'driver' => 'mongodb',
'table' => 'failed_jobs',
],