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);