diff --git a/.github/workflows/build-ci-atlas.yml b/.github/workflows/build-ci-atlas.yml
index 30b4b06b1..339f8fc38 100644
--- a/.github/workflows/build-ci-atlas.yml
+++ b/.github/workflows/build-ci-atlas.yml
@@ -4,11 +4,15 @@ on:
     push:
     pull_request:
 
+env:
+  MONGODB_EXT_V1: mongodb-1.21.0
+  MONGODB_EXT_V2: mongodb-mongodb/mongo-php-driver@v2.x
+
 jobs:
     build:
         runs-on: "${{ matrix.os }}"
 
-        name: "PHP ${{ matrix.php }} Laravel ${{ matrix.laravel }} Atlas"
+        name: "PHP/${{ matrix.php }} Laravel/${{ matrix.laravel }} Driver/${{ matrix.driver }}"
 
         strategy:
             matrix:
@@ -21,6 +25,13 @@ jobs:
                 laravel:
                     - "11.*"
                     - "12.*"
+                driver:
+                  - 1
+                include:
+                    -   php: "8.4"
+                        laravel: "12.*"
+                        os: "ubuntu-latest"
+                        driver: 2
 
         steps:
             -   uses: "actions/checkout@v4"
@@ -39,11 +50,19 @@ jobs:
                 run: |
                     docker exec --tty mongodb mongosh --eval "db.runCommand({ serverStatus: 1 })"
 
+            -   name: Setup cache environment
+                id: extcache
+                uses: shivammathur/cache-extensions@v1
+                with:
+                    php-version: ${{ matrix.php }}
+                    extensions: ${{ matrix.driver == 1 && env.MONGODB_EXT_V1 || env.MONGODB_EXT_V2 }}
+                    key: "extcache-v1"
+
             -   name: "Installing php"
                 uses: "shivammathur/setup-php@v2"
                 with:
                     php-version: ${{ matrix.php }}
-                    extensions: "curl,mbstring,xdebug"
+                    extensions: "curl,mbstring,xdebug,${{ matrix.driver == 1 && env.MONGODB_EXT_V1 || env.MONGODB_EXT_V2 }}"
                     coverage: "xdebug"
                     tools: "composer"
 
diff --git a/.github/workflows/build-ci.yml b/.github/workflows/build-ci.yml
index 659c316d3..bc799c70e 100644
--- a/.github/workflows/build-ci.yml
+++ b/.github/workflows/build-ci.yml
@@ -4,11 +4,15 @@ on:
     push:
     pull_request:
 
+env:
+  MONGODB_EXT_V1: mongodb-1.21.0
+  MONGODB_EXT_V2: mongodb-mongodb/mongo-php-driver@v2.x
+
 jobs:
     build:
         runs-on: "${{ matrix.os }}"
 
-        name: "PHP ${{ matrix.php }} Laravel ${{ matrix.laravel }} MongoDB ${{ matrix.mongodb }} ${{ matrix.mode }}"
+        name: "PHP/${{ matrix.php }} Laravel/${{ matrix.laravel }} Driver/${{ matrix.driver }} Server/${{ matrix.mongodb }} ${{ matrix.mode }}"
 
         strategy:
             matrix:
@@ -29,12 +33,21 @@ jobs:
                     - "10.*"
                     - "11.*"
                     - "12.*"
+                driver:
+                    - 1
                 include:
                     - php: "8.1"
                       laravel: "10.*"
                       mongodb: "5.0"
                       mode: "low-deps"
                       os: "ubuntu-latest"
+                      driver: 1.x
+                      driver_version: "1.21.0"
+                    - php: "8.4"
+                      laravel: "12.*"
+                      mongodb: "8.0"
+                      os: "ubuntu-latest"
+                      driver: 2
                 exclude:
                     - php: "8.1"
                       laravel: "11.*"
@@ -59,11 +72,19 @@ jobs:
                     if [ "${{ matrix.mongodb }}" = "4.4" ]; then MONGOSH_BIN="mongo"; else MONGOSH_BIN="mongosh"; fi
                     docker exec --tty mongodb $MONGOSH_BIN --eval "db.runCommand({ serverStatus: 1 })"
 
+            -   name: Setup cache environment
+                id: extcache
+                uses: shivammathur/cache-extensions@v1
+                with:
+                    php-version: ${{ matrix.php }}
+                    extensions: ${{ matrix.driver == 1 && env.MONGODB_EXT_V1 || env.MONGODB_EXT_V2 }}
+                    key: "extcache-v1"
+
             -   name: "Installing php"
                 uses: "shivammathur/setup-php@v2"
                 with:
                     php-version: ${{ matrix.php }}
-                    extensions: "curl,mbstring,xdebug"
+                    extensions: "curl,mbstring,xdebug,${{ matrix.driver == 1 && env.MONGODB_EXT_V1 || env.MONGODB_EXT_V2 }}"
                     coverage: "xdebug"
                     tools: "composer"
 
diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml
index 24d397294..946e84971 100644
--- a/.github/workflows/coding-standards.yml
+++ b/.github/workflows/coding-standards.yml
@@ -5,7 +5,7 @@ on:
   pull_request:
 
 env:
-  PHP_VERSION: "8.2"
+  PHP_VERSION: "8.4"
   DRIVER_VERSION: "stable"
 
 jobs:
diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml
index a66100d93..e0c907953 100644
--- a/.github/workflows/static-analysis.yml
+++ b/.github/workflows/static-analysis.yml
@@ -13,9 +13,12 @@ on:
 env:
   PHP_VERSION: "8.2"
   DRIVER_VERSION: "stable"
+  MONGODB_EXT_V1: mongodb-1.21.0
+  MONGODB_EXT_V2: mongodb-mongodb/mongo-php-driver@v2.x
 
 jobs:
   phpstan:
+    name: "PHP/${{ matrix.php }} Driver/${{ matrix.driver }}"
     runs-on: "ubuntu-22.04"
     continue-on-error: true
     strategy:
@@ -24,6 +27,10 @@ jobs:
           - '8.1'
           - '8.2'
           - '8.3'
+          - '8.4'
+        driver:
+          - 1
+          - 2
     steps:
       - name: Checkout
         uses: actions/checkout@v4
@@ -35,11 +42,19 @@ jobs:
         run: |
           echo CHECKED_OUT_SHA=$(git rev-parse HEAD) >> $GITHUB_ENV
 
+      - name: Setup cache environment
+        id: extcache
+        uses: shivammathur/cache-extensions@v1
+        with:
+          php-version: ${{ matrix.php }}
+          extensions: ${{ matrix.driver == 1 && env.MONGODB_EXT_V1 || env.MONGODB_EXT_V2 }}
+          key: "extcache-v1"
+
       - name: Setup PHP
         uses: shivammathur/setup-php@v2
         with:
           php-version: ${{ matrix.php }}
-          extensions: curl, mbstring
+          extensions: "curl,mbstring,${{ matrix.driver == 1 && env.MONGODB_EXT_V1 || env.MONGODB_EXT_V2 }}"
           tools: composer:v2
           coverage: none
 
diff --git a/composer.json b/composer.json
index a6f5470aa..2542b51bb 100644
--- a/composer.json
+++ b/composer.json
@@ -23,14 +23,14 @@
     "license": "MIT",
     "require": {
         "php": "^8.1",
-        "ext-mongodb": "^1.21",
+        "ext-mongodb": "^1.21|^2",
         "composer-runtime-api": "^2.0.0",
         "illuminate/cache": "^10.36|^11|^12",
         "illuminate/container": "^10.0|^11|^12",
         "illuminate/database": "^10.30|^11|^12",
         "illuminate/events": "^10.0|^11|^12",
         "illuminate/support": "^10.0|^11|^12",
-        "mongodb/mongodb": "^1.21",
+        "mongodb/mongodb": "^1.21|^2",
         "symfony/http-foundation": "^6.4|^7"
     },
     "require-dev": {
diff --git a/src/Eloquent/Builder.php b/src/Eloquent/Builder.php
index eedbe8712..f85570575 100644
--- a/src/Eloquent/Builder.php
+++ b/src/Eloquent/Builder.php
@@ -11,7 +11,7 @@
 use MongoDB\Builder\Type\QueryInterface;
 use MongoDB\Builder\Type\SearchOperatorInterface;
 use MongoDB\Driver\CursorInterface;
-use MongoDB\Driver\Exception\WriteException;
+use MongoDB\Driver\Exception\BulkWriteException;
 use MongoDB\Laravel\Connection;
 use MongoDB\Laravel\Helpers\QueriesRelationships;
 use MongoDB\Laravel\Query\AggregationBuilder;
@@ -285,7 +285,7 @@ public function createOrFirst(array $attributes = [], array $values = [])
 
         try {
             return $this->create(array_merge($attributes, $values));
-        } catch (WriteException $e) {
+        } catch (BulkWriteException $e) {
             if ($e->getCode() === self::DUPLICATE_KEY_ERROR) {
                 return $this->where($attributes)->first() ?? throw $e;
             }
diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php
index 9592bbe7c..46beebab1 100644
--- a/tests/QueryBuilderTest.php
+++ b/tests/QueryBuilderTest.php
@@ -161,7 +161,7 @@ public function testFindWithTimeout()
         $id = DB::table('users')->insertGetId(['name' => 'John Doe']);
 
         $subscriber = new class implements CommandSubscriber {
-            public function commandStarted(CommandStartedEvent $event)
+            public function commandStarted(CommandStartedEvent $event): void
             {
                 if ($event->getCommandName() !== 'find') {
                     return;
@@ -171,11 +171,11 @@ public function commandStarted(CommandStartedEvent $event)
                 Assert::assertSame(1000, $event->getCommand()->maxTimeMS);
             }
 
-            public function commandFailed(CommandFailedEvent $event)
+            public function commandFailed(CommandFailedEvent $event): void
             {
             }
 
-            public function commandSucceeded(CommandSucceededEvent $event)
+            public function commandSucceeded(CommandSucceededEvent $event): void
             {
             }
         };
diff --git a/tests/Scout/ScoutEngineTest.php b/tests/Scout/ScoutEngineTest.php
index 40d943ffb..7b254ec9c 100644
--- a/tests/Scout/ScoutEngineTest.php
+++ b/tests/Scout/ScoutEngineTest.php
@@ -11,13 +11,11 @@
 use Laravel\Scout\Builder;
 use Laravel\Scout\Jobs\RemoveFromSearch;
 use LogicException;
-use Mockery as m;
 use MongoDB\BSON\Document;
 use MongoDB\BSON\UTCDateTime;
 use MongoDB\Collection;
 use MongoDB\Database;
 use MongoDB\Driver\CursorInterface;
-use MongoDB\Laravel\Eloquent\Model;
 use MongoDB\Laravel\Scout\ScoutEngine;
 use MongoDB\Laravel\Tests\Scout\Models\ScoutUser;
 use MongoDB\Laravel\Tests\Scout\Models\SearchableModel;
@@ -36,7 +34,7 @@ class ScoutEngineTest extends TestCase
 
     public function testCreateIndexInvalidDefinition(): void
     {
-        $database = m::mock(Database::class);
+        $database = $this->createMock(Database::class);
         $engine = new ScoutEngine($database, false, ['collection_invalid' => ['foo' => 'bar']]);
 
         $this->expectException(LogicException::class);
@@ -53,21 +51,22 @@ public function testCreateIndex(): void
             ],
         ];
 
-        $database = m::mock(Database::class);
-        $collection = m::mock(Collection::class);
-        $database->shouldReceive('createCollection')
-            ->once()
+        $database = $this->createMock(Database::class);
+        $collection = $this->createMock(Collection::class);
+        $database->expects($this->once())
+            ->method('createCollection')
             ->with($collectionName);
-        $database->shouldReceive('selectCollection')
+        $database->expects($this->once())
+            ->method('selectCollection')
             ->with($collectionName)
-            ->andReturn($collection);
-        $collection->shouldReceive('createSearchIndex')
-            ->once()
+            ->willReturn($collection);
+        $collection->expects($this->once())
+            ->method('createSearchIndex')
             ->with($expectedDefinition, ['name' => 'scout']);
-        $collection->shouldReceive('listSearchIndexes')
-            ->once()
+        $collection->expects($this->once())
+            ->method('listSearchIndexes')
             ->with(['name' => 'scout', 'typeMap' => ['root' => 'bson']])
-            ->andReturn(new ArrayIterator([Document::fromPHP(['name' => 'scout', 'status' => 'READY'])]));
+            ->willReturn(new ArrayIterator([Document::fromPHP(['name' => 'scout', 'status' => 'READY'])]));
 
         $engine = new ScoutEngine($database, false, []);
         $engine->createIndex($collectionName);
@@ -90,21 +89,22 @@ public function testCreateIndexCustomDefinition(): void
             ],
         ];
 
-        $database = m::mock(Database::class);
-        $collection = m::mock(Collection::class);
-        $database->shouldReceive('createCollection')
-            ->once()
+        $database = $this->createMock(Database::class);
+        $collection = $this->createMock(Collection::class);
+        $database->expects($this->once())
+            ->method('createCollection')
             ->with($collectionName);
-        $database->shouldReceive('selectCollection')
+        $database->expects($this->once())
+            ->method('selectCollection')
             ->with($collectionName)
-            ->andReturn($collection);
-        $collection->shouldReceive('createSearchIndex')
-            ->once()
+            ->willReturn($collection);
+        $collection->expects($this->once())
+            ->method('createSearchIndex')
             ->with($expectedDefinition, ['name' => 'scout']);
-        $collection->shouldReceive('listSearchIndexes')
-            ->once()
+        $collection->expects($this->once())
+            ->method('listSearchIndexes')
             ->with(['name' => 'scout', 'typeMap' => ['root' => 'bson']])
-            ->andReturn(new ArrayIterator([Document::fromPHP(['name' => 'scout', 'status' => 'READY'])]));
+            ->willReturn(new ArrayIterator([Document::fromPHP(['name' => 'scout', 'status' => 'READY'])]));
 
         $engine = new ScoutEngine($database, false, [$collectionName => $expectedDefinition]);
         $engine->createIndex($collectionName);
@@ -115,26 +115,28 @@ public function testCreateIndexCustomDefinition(): void
     public function testSearch(Closure $builder, array $expectedPipeline): void
     {
         $data = [['_id' => 'key_1', '__count' => 15], ['_id' => 'key_2', '__count' => 15]];
-        $database = m::mock(Database::class);
-        $collection = m::mock(Collection::class);
-        $database->shouldReceive('selectCollection')
+        $database = $this->createMock(Database::class);
+        $collection = $this->createMock(Collection::class);
+        $database->expects($this->once())
+            ->method('selectCollection')
             ->with('collection_searchable')
-            ->andReturn($collection);
-        $cursor = m::mock(CursorInterface::class);
-        $cursor->shouldReceive('setTypeMap')->once()->with(self::EXPECTED_TYPEMAP);
-        $cursor->shouldReceive('toArray')->once()->with()->andReturn($data);
-
-        $collection->shouldReceive('getCollectionName')
-            ->zeroOrMoreTimes()
-            ->andReturn('collection_searchable');
-        $collection->shouldReceive('aggregate')
-            ->once()
-            ->withArgs(function ($pipeline) use ($expectedPipeline) {
-                self::assertEquals($expectedPipeline, $pipeline);
-
-                return true;
-            })
-            ->andReturn($cursor);
+            ->willReturn($collection);
+        $cursor = $this->createMock(CursorInterface::class);
+        $cursor->expects($this->once())
+            ->method('setTypeMap')
+            ->with(self::EXPECTED_TYPEMAP);
+        $cursor->expects($this->once())
+            ->method('toArray')
+            ->with()
+            ->willReturn($data);
+
+        $collection->expects($this->any())
+            ->method('getCollectionName')
+            ->willReturn('collection_searchable');
+        $collection->expects($this->once())
+            ->method('aggregate')
+            ->with($expectedPipeline)
+            ->willReturn($cursor);
 
         $engine = new ScoutEngine($database, softDelete: false);
         $result = $engine->search($builder());
@@ -414,15 +416,15 @@ public function testPaginate()
         $perPage = 5;
         $page = 3;
 
-        $database = m::mock(Database::class);
-        $collection = m::mock(Collection::class);
-        $cursor = m::mock(CursorInterface::class);
-        $database->shouldReceive('selectCollection')
+        $database = $this->createMock(Database::class);
+        $collection = $this->createMock(Collection::class);
+        $cursor = $this->createMock(CursorInterface::class);
+        $database->method('selectCollection')
             ->with('collection_searchable')
-            ->andReturn($collection);
-        $collection->shouldReceive('aggregate')
-            ->once()
-            ->withArgs(function (...$args) {
+            ->willReturn($collection);
+        $collection->expects($this->once())
+            ->method('aggregate')
+            ->willReturnCallback(function (...$args) use ($cursor) {
                 self::assertSame([
                     [
                         '$search' => [
@@ -468,14 +470,11 @@ public function testPaginate()
                     ],
                 ], $args[0]);
 
-                return true;
-            })
-            ->andReturn($cursor);
-        $cursor->shouldReceive('setTypeMap')->once()->with(self::EXPECTED_TYPEMAP);
-        $cursor->shouldReceive('toArray')
-            ->once()
-            ->with()
-            ->andReturn([['_id' => 'key_1', '__count' => 17], ['_id' => 'key_2', '__count' => 17]]);
+                return $cursor;
+            });
+        $cursor->expects($this->once())->method('setTypeMap')->with(self::EXPECTED_TYPEMAP);
+        $cursor->expects($this->once())->method('toArray')->with()
+            ->willReturn([['_id' => 'key_1', '__count' => 17], ['_id' => 'key_2', '__count' => 17]]);
 
         $engine = new ScoutEngine($database, softDelete: false);
         $builder = new Builder(new SearchableModel(), 'mustang');
@@ -485,20 +484,27 @@ public function testPaginate()
 
     public function testMapMethodRespectsOrder()
     {
-        $database = m::mock(Database::class);
+        $database = $this->createMock(Database::class);
+        $query = $this->createMock(Builder::class);
         $engine = new ScoutEngine($database, false);
 
-        $model = m::mock(Model::class);
-        $model->shouldReceive(['getScoutKeyName' => 'id']);
-        $model->shouldReceive('queryScoutModelsByIds->get')
-            ->andReturn(LaravelCollection::make([
+        $model = $this->createMock(SearchableModel::class);
+        $model->expects($this->any())
+            ->method('getScoutKeyName')
+            ->willReturn('id');
+        $model->expects($this->once())
+            ->method('queryScoutModelsByIds')
+            ->willReturn($query);
+        $query->expects($this->once())
+            ->method('get')
+            ->willReturn(LaravelCollection::make([
                 new ScoutUser(['id' => 1]),
                 new ScoutUser(['id' => 2]),
                 new ScoutUser(['id' => 3]),
                 new ScoutUser(['id' => 4]),
             ]));
 
-        $builder = m::mock(Builder::class);
+        $builder = $this->createMock(Builder::class);
 
         $results = $engine->map($builder, [
             ['_id' => 1, '__count' => 4],
@@ -518,21 +524,27 @@ public function testMapMethodRespectsOrder()
 
     public function testLazyMapMethodRespectsOrder()
     {
-        $lazy = false;
-        $database = m::mock(Database::class);
+        $database = $this->createMock(Database::class);
+        $query = $this->createMock(Builder::class);
         $engine = new ScoutEngine($database, false);
 
-        $model = m::mock(Model::class);
-        $model->shouldReceive(['getScoutKeyName' => 'id']);
-        $model->shouldReceive('queryScoutModelsByIds->cursor')
-            ->andReturn(LazyCollection::make([
+        $model = $this->createMock(SearchableModel::class);
+        $model->expects($this->any())
+            ->method('getScoutKeyName')
+            ->willReturn('id');
+        $model->expects($this->once())
+            ->method('queryScoutModelsByIds')
+            ->willReturn($query);
+        $query->expects($this->once())
+            ->method('cursor')
+            ->willReturn(LazyCollection::make([
                 new ScoutUser(['id' => 1]),
                 new ScoutUser(['id' => 2]),
                 new ScoutUser(['id' => 3]),
                 new ScoutUser(['id' => 4]),
             ]));
 
-        $builder = m::mock(Builder::class);
+        $builder = $this->createMock(Builder::class);
 
         $results = $engine->lazyMap($builder, [
             ['_id' => 1, '__count' => 4],
@@ -553,13 +565,14 @@ public function testLazyMapMethodRespectsOrder()
     public function testUpdate(): void
     {
         $date = new DateTimeImmutable('2000-01-02 03:04:05');
-        $database = m::mock(Database::class);
-        $collection = m::mock(Collection::class);
-        $database->shouldReceive('selectCollection')
+        $database = $this->createMock(Database::class);
+        $collection = $this->createMock(Collection::class);
+        $database->expects($this->once())
+            ->method('selectCollection')
             ->with('collection_indexable')
-            ->andReturn($collection);
-        $collection->shouldReceive('bulkWrite')
-            ->once()
+            ->willReturn($collection);
+        $collection->expects($this->once())
+            ->method('bulkWrite')
             ->with([
                 [
                     'updateOne' => [
@@ -592,26 +605,23 @@ public function testUpdate(): void
     public function testUpdateWithSoftDelete(): void
     {
         $date = new DateTimeImmutable('2000-01-02 03:04:05');
-        $database = m::mock(Database::class);
-        $collection = m::mock(Collection::class);
-        $database->shouldReceive('selectCollection')
+        $database = $this->createMock(Database::class);
+        $collection = $this->createMock(Collection::class);
+        $database->expects($this->once())
+            ->method('selectCollection')
             ->with('collection_indexable')
-            ->andReturn($collection);
-        $collection->shouldReceive('bulkWrite')
-            ->once()
-            ->withArgs(function ($pipeline) {
-                $this->assertSame([
-                    [
-                        'updateOne' => [
-                            ['_id' => 'key_1'],
-                            ['$set' => ['id' => 1, '__soft_deleted' => false]],
-                            ['upsert' => true],
-                        ],
+            ->willReturn($collection);
+        $collection->expects($this->once())
+            ->method('bulkWrite')
+            ->with([
+                [
+                    'updateOne' => [
+                        ['_id' => 'key_1'],
+                        ['$set' => ['id' => 1, '__soft_deleted' => false]],
+                        ['upsert' => true],
                     ],
-                ], $pipeline);
-
-                return true;
-            });
+                ],
+            ]);
 
         $model = new SearchableModel(['id' => 1]);
         $model->delete();
@@ -622,13 +632,14 @@ public function testUpdateWithSoftDelete(): void
 
     public function testDelete(): void
     {
-        $database = m::mock(Database::class);
-        $collection = m::mock(Collection::class);
-        $database->shouldReceive('selectCollection')
+        $database = $this->createMock(Database::class);
+        $collection = $this->createMock(Collection::class);
+        $database->expects($this->once())
+            ->method('selectCollection')
             ->with('collection_indexable')
-            ->andReturn($collection);
-        $collection->shouldReceive('deleteMany')
-            ->once()
+            ->willReturn($collection);
+        $collection->expects($this->once())
+            ->method('deleteMany')
             ->with(['_id' => ['$in' => ['key_1', 'key_2']]]);
 
         $engine = new ScoutEngine($database, softDelete: false);
@@ -646,13 +657,14 @@ public function testDeleteWithRemoveableScoutCollection(): void
 
         $job = unserialize(serialize($job));
 
-        $database = m::mock(Database::class);
-        $collection = m::mock(Collection::class);
-        $database->shouldReceive('selectCollection')
+        $database = $this->createMock(Database::class);
+        $collection = $this->createMock(Collection::class);
+        $database->expects($this->once())
+            ->method('selectCollection')
             ->with('collection_indexable')
-            ->andReturn($collection);
-        $collection->shouldReceive('deleteMany')
-            ->once()
+            ->willReturn($collection);
+        $collection->expects($this->once())
+            ->method('deleteMany')
             ->with(['_id' => ['$in' => ['key_5']]]);
 
         $engine = new ScoutEngine($database, softDelete: false);