From 8ed457c77c59b34bcd57f93dff66c2bab802e472 Mon Sep 17 00:00:00 2001 From: Pauline Vos Date: Fri, 29 Aug 2025 12:05:58 +0200 Subject: [PATCH 1/6] Improve change log categories (#3442) So that anything labeled `feature` is added to the Features category and docs changes are listed as a separate category --- .github/release.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/release.yml b/.github/release.yml index aabd8e4f2..0ca62014a 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -12,10 +12,14 @@ changelog: - title: New Features labels: - enhancement + - feature - title: Fixed labels: - bug - fixed + - title: Documentation Changes + labels: + - docs - title: Other Changes labels: - "*" From 3a8772634341c595d7742adfc586155aa38a4e06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 09:53:00 +0200 Subject: [PATCH 2/6] Bump actions/labeler from 5 to 6 (#3445) Bumps [actions/labeler](https://github.com/actions/labeler) from 5 to 6. - [Release notes](https://github.com/actions/labeler/releases) - [Commits](https://github.com/actions/labeler/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/labeler dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/labeler.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 52474c6a6..cb28413c1 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -9,4 +9,4 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@v5 + - uses: actions/labeler@v6 From cee14c4b7604a33e9fdde541eb286d9d7b88c3c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 11 Sep 2025 13:46:18 +0200 Subject: [PATCH 3/6] Remove explicit dependency to symfony/http-kernel as it's not used directly (#3446) --- composer.json | 3 +-- src/Session/MongoDbSessionHandler.php | 9 --------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/composer.json b/composer.json index 6edd8d484..658e7f112 100644 --- a/composer.json +++ b/composer.json @@ -31,8 +31,7 @@ "illuminate/database": "^10.30|^11|^12", "illuminate/events": "^10.0|^11|^12", "illuminate/support": "^10.0|^11|^12", - "mongodb/mongodb": "^1.21|^2", - "symfony/http-foundation": "^6.4|^7" + "mongodb/mongodb": "^1.21|^2" }, "require-dev": { "laravel/scout": "^10.3", diff --git a/src/Session/MongoDbSessionHandler.php b/src/Session/MongoDbSessionHandler.php index 3677ea758..b6082b6b9 100644 --- a/src/Session/MongoDbSessionHandler.php +++ b/src/Session/MongoDbSessionHandler.php @@ -1,14 +1,5 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace MongoDB\Laravel\Session; use Illuminate\Session\DatabaseSessionHandler; From dee170a2b822c209edd30ed8c1206e245584e14a Mon Sep 17 00:00:00 2001 From: Pauline Vos Date: Mon, 15 Sep 2025 18:38:29 +0200 Subject: [PATCH 4/6] Use DB tx manager in `ManagesTransactions` trait (#3443) --- src/Concerns/ManagesTransactions.php | 78 +++++++- tests/Ticket/GH3328Test.php | 266 +++++++++++++++++++++++++++ 2 files changed, 341 insertions(+), 3 deletions(-) create mode 100644 tests/Ticket/GH3328Test.php diff --git a/src/Concerns/ManagesTransactions.php b/src/Concerns/ManagesTransactions.php index 6403cc45d..534e1548e 100644 --- a/src/Concerns/ManagesTransactions.php +++ b/src/Concerns/ManagesTransactions.php @@ -8,9 +8,12 @@ use MongoDB\Client; use MongoDB\Driver\Exception\RuntimeException; use MongoDB\Driver\Session; +use MongoDB\Laravel\Connection; use Throwable; +use function max; use function MongoDB\with_transaction; +use function property_exists; /** * @internal @@ -55,8 +58,23 @@ private function getSessionOrThrow(): Session */ public function beginTransaction(array $options = []): void { + $this->runCallbacksBeforeTransaction(); + $this->getSessionOrCreate()->startTransaction($options); + + $this->handleInitialTransactionState(); + } + + private function handleInitialTransactionState(): void + { $this->transactions = 1; + + $this->transactionsManager?->begin( + $this->getName(), + $this->transactions, + ); + + $this->fireConnectionEvent('beganTransaction'); } /** @@ -64,8 +82,26 @@ public function beginTransaction(array $options = []): void */ public function commit(): void { + $this->fireConnectionEvent('committing'); $this->getSessionOrThrow()->commitTransaction(); - $this->transactions = 0; + + $this->handleCommitState(); + } + + private function handleCommitState(): void + { + [$levelBeingCommitted, $this->transactions] = [ + $this->transactions, + max(0, $this->transactions - 1), + ]; + + $this->transactionsManager?->commit( + $this->getName(), + $levelBeingCommitted, + $this->transactions, + ); + + $this->fireConnectionEvent('committed'); } /** @@ -73,14 +109,42 @@ public function commit(): void */ public function rollBack($toLevel = null): void { - $this->getSessionOrThrow()->abortTransaction(); + $session = $this->getSessionOrThrow(); + if ($session->isInTransaction()) { + $session->abortTransaction(); + } + + $this->handleRollbackState(); + } + + private function handleRollbackState(): void + { $this->transactions = 0; + + $this->transactionsManager?->rollback( + $this->getName(), + $this->transactions, + ); + + $this->fireConnectionEvent('rollingBack'); + } + + private function runCallbacksBeforeTransaction(): void + { + // ToDo: remove conditional once we stop supporting Laravel 10.x + if (property_exists(Connection::class, 'beforeStartingTransaction')) { + foreach ($this->beforeStartingTransaction as $beforeTransactionCallback) { + $beforeTransactionCallback($this); + } + } } /** * Static transaction function realize the with_transaction functionality provided by MongoDB. * - * @param int $attempts + * @param int $attempts + * + * @throws Throwable */ public function transaction(Closure $callback, $attempts = 1, array $options = []): mixed { @@ -93,15 +157,20 @@ public function transaction(Closure $callback, $attempts = 1, array $options = [ if ($attemptsLeft < 0) { $session->abortTransaction(); + $this->handleRollbackState(); return; } + $this->runCallbacksBeforeTransaction(); + $this->handleInitialTransactionState(); + // 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); + $this->fireConnectionEvent('committing'); } catch (Throwable $throwable) { throw $throwable; } @@ -110,9 +179,12 @@ public function transaction(Closure $callback, $attempts = 1, array $options = [ with_transaction($this->getSessionOrCreate(), $callbackFunction, $options); if ($attemptsLeft < 0 && $throwable) { + $this->handleRollbackState(); throw $throwable; } + $this->handleCommitState(); + return $callbackResult; } } diff --git a/tests/Ticket/GH3328Test.php b/tests/Ticket/GH3328Test.php new file mode 100644 index 000000000..6b25426f1 --- /dev/null +++ b/tests/Ticket/GH3328Test.php @@ -0,0 +1,266 @@ +beforeStartingTransactionIsSupported()) { + Event::assertDispatchedTimes(BeforeTransactionEvent::class); + } + + Event::assertDispatchedTimes(RegularEvent::class); + Event::assertDispatchedTimes(AfterCommitEvent::class); + + Event::assertDispatched(TransactionBeginning::class); + Event::assertDispatched(TransactionCommitting::class); + Event::assertDispatched(TransactionCommitted::class); + }; + + $this->assertTransactionCallbackResult($callback, $assert); + } + + public function testAfterCommitOnFailedTransaction(): void + { + $callback = static function (): void { + event(new RegularEvent()); + event(new AfterCommitEvent()); + + // Transaction failed; after commit event should not be dispatched + throw new Fake(); + }; + + $assert = function (): void { + if ($this->beforeStartingTransactionIsSupported()) { + Event::assertDispatchedTimes(BeforeTransactionEvent::class, 3); + } + + Event::assertDispatchedTimes(RegularEvent::class, 3); + + Event::assertDispatchedTimes(TransactionBeginning::class, 3); + Event::assertDispatched(TransactionRolledBack::class); + Event::assertNotDispatched(TransactionCommitting::class); + Event::assertNotDispatched(TransactionCommitted::class); + }; + + $this->assertCallbackResultForConnection( + DB::connection('mongodb'), + $callback, + $assert, + 3, + ); + + if (! interface_exists(ConcurrencyErrorDetector::class)) { + // Earlier versions of Laravel use a trait instead of DI to detect concurrency errors + // That would increase the scope of this comparison dramatically and is probably not worth it. + return; + } + + $this->app->bind(ConcurrencyErrorDetector::class, FakeConcurrencyErrorDetector::class); + + $this->assertCallbackResultForConnection( + DB::connection('sqlite'), + $callback, + $assert, + 3, + ); + } + + public function testAfterCommitOnSuccessfulManualTransaction(): void + { + $callback = function (): void { + event(new RegularEvent()); + event(new AfterCommitEvent()); + }; + + $assert = function (): void { + if ($this->beforeStartingTransactionIsSupported()) { + Event::assertDispatchedTimes(BeforeTransactionEvent::class); + } + + Event::assertDispatchedTimes(RegularEvent::class); + Event::assertDispatchedTimes(AfterCommitEvent::class); + + Event::assertDispatched(TransactionBeginning::class); + Event::assertNotDispatched(TransactionRolledBack::class); + Event::assertDispatched(TransactionCommitting::class); + Event::assertDispatched(TransactionCommitted::class); + }; + + $this->assertTransactionResult($callback, $assert); + } + + public function testAfterCommitOnFailedManualTransaction(): void + { + $callback = function (): void { + event(new RegularEvent()); + event(new AfterCommitEvent()); + + throw new Fake(); + }; + + $assert = function (): void { + if ($this->beforeStartingTransactionIsSupported()) { + Event::assertDispatchedTimes(BeforeTransactionEvent::class); + } + + Event::assertDispatchedTimes(RegularEvent::class); + Event::assertNotDispatched(AfterCommitEvent::class); + + Event::assertDispatched(TransactionBeginning::class); + Event::assertDispatched(TransactionRolledBack::class); + Event::assertNotDispatched(TransactionCommitting::class); + Event::assertNotDispatched(TransactionCommitted::class); + }; + + $this->assertTransactionResult($callback, $assert); + } + + private function assertTransactionCallbackResult(Closure $callback, Closure $assert, ?int $attempts = 1): void + { + $this->assertCallbackResultForConnection( + DB::connection('sqlite'), + $callback, + $assert, + $attempts, + ); + + $this->assertCallbackResultForConnection( + DB::connection('mongodb'), + $callback, + $assert, + $attempts, + ); + } + + /** + * Ensure equal transaction behavior between SQLite (handled by Laravel) and MongoDB + */ + private function assertCallbackResultForConnection(Connection $connection, Closure $callback, Closure $assertions, int $attempts): void + { + $fake = Event::fake(); + $connection->setEventDispatcher($fake); + + if ($this->beforeStartingTransactionIsSupported()) { + $connection->beforeStartingTransaction(function () { + event(new BeforeTransactionEvent()); + }); + } + + try { + $connection->transaction($callback, $attempts); + } catch (Exception) { + } + + $assertions(); + } + + private function assertTransactionResult(Closure $callback, Closure $assert): void + { + $this->assertManualResultForConnection( + DB::connection('sqlite'), + $callback, + $assert, + ); + + $this->assertManualResultForConnection( + DB::connection('mongodb'), + $callback, + $assert, + ); + } + + /** + * Ensure equal transaction behavior between SQLite (handled by Laravel) and MongoDB + */ + private function assertManualResultForConnection(Connection $connection, Closure $callback, Closure $assert): void + { + $fake = Event::fake(); + $connection->setEventDispatcher($fake); + + if ($this->beforeStartingTransactionIsSupported()) { + $connection->beforeStartingTransaction(function () { + event(new BeforeTransactionEvent()); + }); + } + + $connection->beginTransaction(); + + try { + $callback(); + $connection->commit(); + } catch (Exception) { + $connection->rollBack(); + } + + $assert(); + } + + private function beforeStartingTransactionIsSupported(): bool + { + return property_exists(ManagesTransactions::class, 'beforeStartingTransaction'); + } +} + +class AfterCommitEvent implements ShouldDispatchAfterCommit +{ + use Dispatchable; +} + +class BeforeTransactionEvent +{ + use Dispatchable; +} +class RegularEvent +{ + use Dispatchable; +} +class Fake extends RuntimeException +{ + public function __construct() + { + $this->errorLabels = ['TransientTransactionError']; + } +} + +if (interface_exists(ConcurrencyErrorDetector::class)) { + class FakeConcurrencyErrorDetector implements ConcurrencyErrorDetector + { + public function causedByConcurrencyError(Throwable $e): bool + { + return true; + } + } +} From e4672c17757026a3a31cf0d883eb4a0d8febfe21 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Sep 2025 19:46:48 +0000 Subject: [PATCH 5/6] Bump mongodb-labs/drivers-github-tools from 2 to 3 (#3452) Bumps [mongodb-labs/drivers-github-tools](https://github.com/mongodb-labs/drivers-github-tools) from 2 to 3. - [Release notes](https://github.com/mongodb-labs/drivers-github-tools/releases) - [Commits](https://github.com/mongodb-labs/drivers-github-tools/compare/v2...v3) --- updated-dependencies: - dependency-name: mongodb-labs/drivers-github-tools dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bc60a79cc..8b1b6b9b7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: run: echo '🎬 Release process for version ${{ inputs.version }} started by @${{ github.triggering_actor }}' >> $GITHUB_STEP_SUMMARY - name: "Generate token and checkout repository" - uses: mongodb-labs/drivers-github-tools/secure-checkout@v2 + uses: mongodb-labs/drivers-github-tools/secure-checkout@v3 with: app_id: ${{ vars.APP_ID }} private_key: ${{ secrets.APP_PRIVATE_KEY }} @@ -71,7 +71,7 @@ jobs: # - name: "Set up drivers-github-tools" - uses: mongodb-labs/drivers-github-tools/setup@v2 + uses: mongodb-labs/drivers-github-tools/setup@v3 with: aws_role_arn: ${{ secrets.AWS_ROLE_ARN }} aws_region_name: ${{ vars.AWS_REGION_NAME }} @@ -82,7 +82,7 @@ jobs: run: echo "RELEASE_URL=$(gh release create ${{ inputs.version }} --target ${{ github.ref_name }} --title "${{ inputs.version }}" --generate-notes --draft)" >> "$GITHUB_ENV" - name: "Create release tag" - uses: mongodb-labs/drivers-github-tools/tag-version@v2 + uses: mongodb-labs/drivers-github-tools/tag-version@v3 with: version: ${{ inputs.version }} tag_message_template: 'Release ${VERSION}' @@ -121,7 +121,7 @@ jobs: steps: - name: "Generate token and checkout repository" - uses: mongodb-labs/drivers-github-tools/secure-checkout@v2 + uses: mongodb-labs/drivers-github-tools/secure-checkout@v3 with: app_id: ${{ vars.APP_ID }} private_key: ${{ secrets.APP_PRIVATE_KEY }} @@ -129,14 +129,14 @@ jobs: # Sets the S3_ASSETS environment variable used later - name: "Set up drivers-github-tools" - uses: mongodb-labs/drivers-github-tools/setup@v2 + uses: mongodb-labs/drivers-github-tools/setup@v3 with: aws_role_arn: ${{ secrets.AWS_ROLE_ARN }} aws_region_name: ${{ vars.AWS_REGION_NAME }} aws_secret_id: ${{ secrets.AWS_SECRET_ID }} - name: "Generate SSDLC Reports" - uses: mongodb-labs/drivers-github-tools/full-report@v2 + uses: mongodb-labs/drivers-github-tools/full-report@v3 with: product_name: "MongoDB Laravel Integration" release_version: ${{ inputs.version }} @@ -147,7 +147,7 @@ jobs: continue-on-error: true - name: Upload S3 assets - uses: mongodb-labs/drivers-github-tools/upload-s3-assets@v2 + uses: mongodb-labs/drivers-github-tools/upload-s3-assets@v3 with: version: ${{ inputs.version }} product_name: laravel-mongodb From a948fd6408618c7ff145a6d3eb504fc6ef962329 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 21:27:01 +0200 Subject: [PATCH 6/6] Bump github/codeql-action from 3 to 4 (#3455) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v3...v4) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/static-analysis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index fe76fb466..7249dd739 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -89,13 +89,13 @@ jobs: - name: "Upload SARIF report" if: ${{ github.event_name != 'workflow_dispatch' }} - uses: "github/codeql-action/upload-sarif@v3" + uses: "github/codeql-action/upload-sarif@v4" with: sarif_file: phpstan.sarif - name: "Upload SARIF report" if: ${{ github.event_name == 'workflow_dispatch' }} - uses: "github/codeql-action/upload-sarif@v3" + uses: "github/codeql-action/upload-sarif@v4" with: sarif_file: phpstan.sarif ref: ${{ inputs.ref }}