diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index 9dc3a9a6a9..02dca84361 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -50,6 +50,11 @@ jobs: - name: "Install dependencies with Composer" uses: "ramsey/composer-install@v1" + - name: "Upload composer.lock as build artifact" + uses: actions/upload-artifact@v2 + with: + name: composer.lock + path: composer.lock # https://github.com/doctrine/.github/issues/3 - name: "Run PHP_CodeSniffer" diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 88546fb2cd..e495b70cc6 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -12,12 +12,10 @@ env: jobs: phpunit: name: "PHPUnit" - runs-on: "${{ matrix.os }}" + runs-on: "ubuntu-18.04" strategy: matrix: - os: - - "ubuntu-18.04" php-version: - "7.2" - "7.3" @@ -35,13 +33,11 @@ jobs: - "highest" include: - deps: "lowest" - os: "ubuntu-16.04" php-version: "7.2" mongodb-version: "3.6" driver-version: "1.5.0" topology: "server" - topology: "sharded_cluster" - os: "ubuntu-18.04" php-version: "8.0" mongodb-version: "4.4" driver-version: "stable" @@ -86,6 +82,12 @@ jobs: dependency-versions: "${{ matrix.dependencies }}" composer-options: "--prefer-dist" + - name: "Upload composer.lock as build artifact" + uses: actions/upload-artifact@v2 + with: + name: composer.lock + path: composer.lock + - id: setup-mongodb uses: mongodb-labs/drivers-evergreen-tools@master with: diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 1e9792830b..3f0325a942 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -54,6 +54,12 @@ jobs: - name: "Install dependencies with Composer" uses: "ramsey/composer-install@v1" + - name: "Upload composer.lock as build artifact" + uses: actions/upload-artifact@v2 + with: + name: composer.lock + path: composer.lock + # https://github.com/doctrine/.github/issues/3 - name: "Run PHP_CodeSniffer" run: "vendor/bin/phpbench run --report=default --revs=100 --iterations=5 --report=aggregate" diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 5109efe477..e0ad865ade 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -49,6 +49,12 @@ jobs: - name: "Install dependencies with Composer" uses: "ramsey/composer-install@v1" + - name: "Upload composer.lock as build artifact" + uses: actions/upload-artifact@v2 + with: + name: composer.lock + path: composer.lock + - name: "Run a static analysis with phpstan/phpstan" run: "vendor/bin/phpstan analyse --error-format=github" @@ -75,5 +81,11 @@ jobs: - name: "Install dependencies with Composer" uses: "ramsey/composer-install@v1" + - name: "Upload composer.lock as build artifact" + uses: actions/upload-artifact@v2 + with: + name: composer.lock + path: composer.lock + - name: "Run a static analysis with vimeo/psalm" run: "vendor/bin/psalm --show-info=false --stats --output-format=github --threads=$(nproc) --php-version=${{ matrix.php-version }}" diff --git a/lib/Doctrine/ODM/MongoDB/Persisters/PersistenceBuilder.php b/lib/Doctrine/ODM/MongoDB/Persisters/PersistenceBuilder.php index adf1c9ef3b..4d405a1a03 100644 --- a/lib/Doctrine/ODM/MongoDB/Persisters/PersistenceBuilder.php +++ b/lib/Doctrine/ODM/MongoDB/Persisters/PersistenceBuilder.php @@ -68,14 +68,11 @@ public function prepareInsertData($document) foreach ($class->fieldMappings as $mapping) { $new = $changeset[$mapping['fieldName']][1] ?? null; - if ($new === null && $mapping['nullable']) { - $insertData[$mapping['name']] = null; - } - - /* Nothing more to do for null values, since we're either storing - * them (if nullable was true) or not. - */ if ($new === null) { + if ($mapping['nullable']) { + $insertData[$mapping['name']] = null; + } + continue; } @@ -143,34 +140,36 @@ public function prepareUpdateData($document) [$old, $new] = $change; + if ($new === null) { + if ($mapping['nullable'] === true) { + $updateData['$set'][$mapping['name']] = null; + } else { + $updateData['$unset'][$mapping['name']] = true; + } + + continue; + } + // Scalar fields if (! isset($mapping['association'])) { - if ($new === null && $mapping['nullable'] !== true) { - $updateData['$unset'][$mapping['name']] = true; + if (isset($mapping['strategy']) && $mapping['strategy'] === ClassMetadata::STORAGE_STRATEGY_INCREMENT) { + $operator = '$inc'; + $type = Type::getType($mapping['type']); + assert($type instanceof Incrementable); + $value = $type->convertToDatabaseValue($type->diff($old, $new)); } else { - if ($new !== null && isset($mapping['strategy']) && $mapping['strategy'] === ClassMetadata::STORAGE_STRATEGY_INCREMENT) { - $operator = '$inc'; - $type = Type::getType($mapping['type']); - assert($type instanceof Incrementable); - $value = $type->convertToDatabaseValue($type->diff($old, $new)); - } else { - $operator = '$set'; - $value = $new === null ? null : Type::getType($mapping['type'])->convertToDatabaseValue($new); - } - - $updateData[$operator][$mapping['name']] = $value; + $operator = '$set'; + $value = Type::getType($mapping['type'])->convertToDatabaseValue($new); } + $updateData[$operator][$mapping['name']] = $value; + // @EmbedOne } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::EMBED_ONE) { // If we have a new embedded document then lets set the whole thing - if ($new && $this->uow->isScheduledForInsert($new)) { + if ($this->uow->isScheduledForInsert($new)) { $updateData['$set'][$mapping['name']] = $this->prepareEmbeddedDocumentValue($mapping, $new); - // If we don't have a new value then lets unset the embedded document - } elseif (! $new) { - $updateData['$unset'][$mapping['name']] = true; - // Update existing embedded document } else { $update = $this->prepareUpdateData($new); @@ -182,7 +181,7 @@ public function prepareUpdateData($document) } // @ReferenceMany, @EmbedMany - } elseif (isset($mapping['association']) && $mapping['type'] === 'many' && $new) { + } elseif (isset($mapping['association']) && $mapping['type'] === 'many') { if (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForUpdate($new)) { $updateData['$set'][$mapping['name']] = $this->prepareAssociatedCollectionValue($new, true); } elseif (CollectionHelper::isAtomic($mapping['strategy']) && $this->uow->isCollectionScheduledForDeletion($new)) { @@ -208,11 +207,7 @@ public function prepareUpdateData($document) // @ReferenceOne } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) { - if (isset($new) || $mapping['nullable'] === true) { - $updateData['$set'][$mapping['name']] = $new === null ? null : $this->prepareReferencedDocumentValue($mapping, $new); - } else { - $updateData['$unset'][$mapping['name']] = true; - } + $updateData['$set'][$mapping['name']] = $this->prepareReferencedDocumentValue($mapping, $new); } } @@ -250,31 +245,36 @@ public function prepareUpsertData($document) [$old, $new] = $change; + // Fields with a null value should only be written for inserts + if ($new === null) { + if ($mapping['nullable'] === true) { + $updateData['$setOnInsert'][$mapping['name']] = null; + } + + continue; + } + // Scalar fields if (! isset($mapping['association'])) { - if ($new !== null) { - if (empty($mapping['id']) && isset($mapping['strategy']) && $mapping['strategy'] === ClassMetadata::STORAGE_STRATEGY_INCREMENT) { - $operator = '$inc'; - $type = Type::getType($mapping['type']); - assert($type instanceof Incrementable); - $value = $type->convertToDatabaseValue($type->diff($old, $new)); - } else { - $operator = '$set'; - $value = Type::getType($mapping['type'])->convertToDatabaseValue($new); - } - - $updateData[$operator][$mapping['name']] = $value; - } elseif ($mapping['nullable'] === true) { - $updateData['$setOnInsert'][$mapping['name']] = null; + if (empty($mapping['id']) && isset($mapping['strategy']) && $mapping['strategy'] === ClassMetadata::STORAGE_STRATEGY_INCREMENT) { + $operator = '$inc'; + $type = Type::getType($mapping['type']); + assert($type instanceof Incrementable); + $value = $type->convertToDatabaseValue($type->diff($old, $new)); + } else { + $operator = '$set'; + $value = Type::getType($mapping['type'])->convertToDatabaseValue($new); } + $updateData[$operator][$mapping['name']] = $value; + // @EmbedOne } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::EMBED_ONE) { // If we don't have a new value then do nothing on upsert // If we have a new embedded document then lets set the whole thing - if ($new && $this->uow->isScheduledForInsert($new)) { + if ($this->uow->isScheduledForInsert($new)) { $updateData['$set'][$mapping['name']] = $this->prepareEmbeddedDocumentValue($mapping, $new); - } elseif ($new) { + } else { // Update existing embedded document $update = $this->prepareUpsertData($new); foreach ($update as $cmd => $values) { @@ -286,9 +286,7 @@ public function prepareUpsertData($document) // @ReferenceOne } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) { - if (isset($new) || $mapping['nullable'] === true) { - $updateData['$set'][$mapping['name']] = $new === null ? null : $this->prepareReferencedDocumentValue($mapping, $new); - } + $updateData['$set'][$mapping['name']] = $this->prepareReferencedDocumentValue($mapping, $new); // @ReferenceMany, @EmbedMany } elseif ( diff --git a/lib/Doctrine/ODM/MongoDB/Query/Builder.php b/lib/Doctrine/ODM/MongoDB/Query/Builder.php index a800303a58..2c959178ce 100644 --- a/lib/Doctrine/ODM/MongoDB/Query/Builder.php +++ b/lib/Doctrine/ODM/MongoDB/Query/Builder.php @@ -1028,11 +1028,11 @@ public function nearSphere($x, $y = null): self * @see Expr::not() * @see https://docs.mongodb.com/manual/reference/operator/not/ * - * @param array|Expr $expression + * @param array|Expr|mixed $valueOrExpression */ - public function not($expression): self + public function not($valueOrExpression): self { - $this->expr->not($expression); + $this->expr->not($valueOrExpression); return $this; } diff --git a/lib/Doctrine/ODM/MongoDB/Query/Expr.php b/lib/Doctrine/ODM/MongoDB/Query/Expr.php index e924a66fc8..86e47f4dfd 100644 --- a/lib/Doctrine/ODM/MongoDB/Query/Expr.php +++ b/lib/Doctrine/ODM/MongoDB/Query/Expr.php @@ -861,11 +861,11 @@ public function nearSphere($x, $y = null): self * @see Builder::not() * @see https://docs.mongodb.com/manual/reference/operator/not/ * - * @param array|Expr $expression + * @param array|Expr|mixed $valueOrExpression */ - public function not($expression): self + public function not($valueOrExpression): self { - return $this->operator('$not', $expression); + return $this->operator('$not', $valueOrExpression); } /** diff --git a/lib/Doctrine/ODM/MongoDB/UnitOfWork.php b/lib/Doctrine/ODM/MongoDB/UnitOfWork.php index 7b539a3631..428ad52a0b 100644 --- a/lib/Doctrine/ODM/MongoDB/UnitOfWork.php +++ b/lib/Doctrine/ODM/MongoDB/UnitOfWork.php @@ -1459,6 +1459,10 @@ public function scheduleForDelete(object $document, bool $isView = false): void unset($this->documentUpdates[$oid]); } + if (isset($this->documentUpserts[$oid])) { + unset($this->documentUpserts[$oid]); + } + if (isset($this->documentDeletions[$oid])) { return; } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index c308c9d739..9a8ee72c32 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -9,9 +9,8 @@ parameters: # Making classes final as suggested would be a BC-break - message: "#^Unsafe usage of new static\\(\\)\\.$#" - paths: - - lib/Doctrine/ODM/MongoDB/Aggregation/Expr.php - - lib/Doctrine/ODM/MongoDB/DocumentManager.php + count: 1 + path: lib/Doctrine/ODM/MongoDB/Aggregation/Expr.php # This cannot be solved the way it is, see https://github.com/vimeo/psalm/issues/5788 - @@ -25,6 +24,24 @@ parameters: count: 1 path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/GeoNear.php + # Fixed in 2.3 + - + message: "#^Method Doctrine\\\\ODM\\\\MongoDB\\\\DocumentManager\\:\\:find\\(\\) should return T of object\\|null but returns object\\|null\\.$#" + count: 1 + path: lib/Doctrine/ODM/MongoDB/DocumentManager.php + + # Fixed in 2.3 + - + message: "#^Return type \\(Doctrine\\\\ODM\\\\MongoDB\\\\Mapping\\\\ClassMetadataFactory\\) of method Doctrine\\\\ODM\\\\MongoDB\\\\DocumentManager\\:\\:getMetadataFactory\\(\\) should be compatible with return type \\(Doctrine\\\\Persistence\\\\Mapping\\\\ClassMetadataFactory\\\\>\\) of method Doctrine\\\\Persistence\\\\ObjectManager\\:\\:getMetadataFactory\\(\\)$#" + count: 1 + path: lib/Doctrine/ODM/MongoDB/DocumentManager.php + + # Making classes final as suggested would be a BC-break + - + message: "#^Unsafe usage of new static\\(\\)\\.$#" + count: 1 + path: lib/Doctrine/ODM/MongoDB/DocumentManager.php + # Union types cannot be added yet - message: "#^Result of && is always false\\.$#" diff --git a/psalm-baseline.xml b/psalm-baseline.xml new file mode 100644 index 0000000000..8e22801a5e --- /dev/null +++ b/psalm-baseline.xml @@ -0,0 +1,31 @@ + + + + + $document + $document + $document + $document + $document + $document + $documentName + $documentName + + + + + $fieldName + $fieldName + + + + + $value + + + + + $compositeExpr + + + diff --git a/psalm.xml b/psalm.xml index 0fdfeb3390..bbb07a611e 100644 --- a/psalm.xml +++ b/psalm.xml @@ -6,6 +6,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="https://getpsalm.org/schema/config" xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" + errorBaseline="psalm-baseline.xml" > diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/EmbeddedTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/EmbeddedTest.php index 8e1b9b54b8..d9e82a5e5e 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/EmbeddedTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/EmbeddedTest.php @@ -431,6 +431,7 @@ public function testRemoveEmbeddedDocument() $check = $this->dm->getDocumentCollection(User::class)->findOne(); $this->assertEmpty($check['phonenumbers']); + $this->assertNull($check['addressNullable']); $this->assertArrayNotHasKey('address', $check); } diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/QueryTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/QueryTest.php index cc7532a389..d9af374376 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/QueryTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/QueryTest.php @@ -15,6 +15,7 @@ use InvalidArgumentException; use IteratorAggregate; use MongoDB\BSON\ObjectId; +use MongoDB\BSON\Regex; use MongoDB\BSON\UTCDateTime; use function array_values; @@ -100,6 +101,27 @@ public function testAddNot() $this->assertNotNull($user); } + public function testNotAllowsRegex() + { + $user = new User(); + $user->setUsername('boo'); + + $this->dm->persist($user); + $this->dm->flush(); + + $qb = $this->dm->createQueryBuilder(User::class); + $qb->field('username')->not(new Regex('Boo', 'i')); + $query = $qb->getQuery(); + $user = $query->getSingleResult(); + $this->assertNull($user); + + $qb = $this->dm->createQueryBuilder(User::class); + $qb->field('username')->not(new Regex('Boo')); + $query = $qb->getQuery(); + $user = $query->getSingleResult(); + $this->assertNotNull($user); + } + public function testDistinct() { $user = new User(); diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH2310Test.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH2310Test.php new file mode 100644 index 0000000000..e00301c8a3 --- /dev/null +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH2310Test.php @@ -0,0 +1,124 @@ +dm->persist($document); + $this->dm->flush(); + $this->dm->clear(); + + $repository = $this->dm->getRepository(GH2310Container::class); + $result = $repository->find($document->id); + + self::assertInstanceOf(GH2310Container::class, $result); + self::assertSame($document->id, $result->id); + self::assertNull($result->embedded); + } + + public function testAggregatorBuilderWithNullableEmbeddedAfterUpsert(): void + { + $document = new GH2310Container((string) new ObjectId(), null); + $this->dm->persist($document); + $this->dm->flush(); + $this->dm->clear(); + + $aggBuilder = $this->dm->createAggregationBuilder(GH2310Container::class); + $aggBuilder->match()->field('id')->equals($document->id); + $result = $aggBuilder->hydrate(GH2310Container::class)->getAggregation()->getIterator()->current(); + + self::assertInstanceOf(GH2310Container::class, $result); + self::assertSame($document->id, $result->id); + self::assertNull($result->embedded); + } + + public function testFindWithNullableEmbeddedAfterInsert(): void + { + $document = new GH2310Container(null, null); + $this->dm->persist($document); + $this->dm->flush(); + + $repository = $this->dm->getRepository(GH2310Container::class); + $result = $repository->find($document->id); + + self::assertInstanceOf(GH2310Container::class, $result); + self::assertSame($document->id, $result->id); + self::assertNull($result->embedded); + } + + public function testAggregatorBuilderWithNullableEmbeddedAfterInsert(): void + { + $document = new GH2310Container(null, null); + $this->dm->persist($document); + $this->dm->flush(); + + $aggBuilder = $this->dm->createAggregationBuilder(GH2310Container::class); + $aggBuilder->match()->field('id')->equals($document->id); + $result = $aggBuilder->hydrate(GH2310Container::class)->getAggregation()->getIterator()->current(); + + self::assertInstanceOf(GH2310Container::class, $result); + self::assertSame($document->id, $result->id); + self::assertNull($result->embedded); + } + + public function testFindWithNullableEmbeddedAfterUpdate(): void + { + $document = new GH2310Container(null, new GH2310Embedded()); + $this->dm->persist($document); + $this->dm->flush(); + + // Update embedded document to trigger nullable behaviour + $document->embedded = null; + $this->dm->flush(); + $this->dm->clear(); + + $repository = $this->dm->getRepository(GH2310Container::class); + $result = $repository->find($document->id); + + self::assertInstanceOf(GH2310Container::class, $result); + self::assertSame($document->id, $result->id); + self::assertNull($result->embedded); + } + + public function testAggregatorBuilderWithNullableEmbeddedAfterUpdate(): void + { + $document = new GH2310Container(null, new GH2310Embedded()); + $this->dm->persist($document); + $this->dm->flush(); + + // Update embedded document to trigger nullable behaviour + $document->embedded = null; + $this->dm->flush(); + $this->dm->clear(); + + $aggBuilder = $this->dm->createAggregationBuilder(GH2310Container::class); + $aggBuilder->match()->field('id')->equals($document->id); + $result = $aggBuilder->hydrate(GH2310Container::class)->getAggregation()->getIterator()->current(); + + self::assertInstanceOf(GH2310Container::class, $result); + self::assertSame($document->id, $result->id); + self::assertNull($result->embedded); + } +} diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/UpsertTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/UpsertTest.php index 3449292ff7..dbbc148450 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/UpsertTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/UpsertTest.php @@ -8,6 +8,8 @@ use Doctrine\ODM\MongoDB\Tests\BaseTest; use MongoDB\BSON\ObjectId; +use function assert; + class UpsertTest extends BaseTest { /** @@ -17,11 +19,9 @@ class UpsertTest extends BaseTest */ public function testUpsertEmbedManyDoesNotCreateObject() { - $test = new UpsertTestUser(); - $test->id = (string) new ObjectId(); + $test = new UpsertTestUser(); $embedded = new UpsertTestUserEmbedded(); - $embedded->id = (string) new ObjectId(); $embedded->test = 'test'; $test->embedMany[] = $embedded; @@ -36,6 +36,54 @@ public function testUpsertEmbedManyDoesNotCreateObject() $this->dm->flush(); } + + public function testUpsertDoesNotOverwriteNullableFieldsOnNull() + { + $test = new UpsertTestUser(); + + $test->nullableField = 'value'; + $test->nullableReferenceOne = new UpsertTestUser(); + $test->nullableEmbedOne = new UpsertTestUserEmbedded(); + + $this->dm->persist($test); + $this->dm->flush(); + $this->dm->clear(); + + $upsert = new UpsertTestUser(); + + // Re-use old ID but don't set any other values + $upsert->id = $test->id; + + $this->dm->persist($upsert); + $this->dm->flush(); + $this->dm->clear(); + + $upsertResult = $this->dm->find(UpsertTestUser::class, $test->id); + assert($upsertResult instanceof $upsertResult); + self::assertNotNull($upsertResult->nullableField); + self::assertNotNull($upsertResult->nullableReferenceOne); + self::assertNotNull($upsertResult->nullableEmbedOne); + } + + public function testUpsertsWritesNullableFieldsOnInsert() + { + $test = new UpsertTestUser(); + $this->dm->persist($test); + $this->dm->flush(); + + $collection = $this->dm->getDocumentCollection(UpsertTestUser::class); + $result = $collection->findOne(['_id' => new ObjectId($test->id)]); + + self::assertEquals( + [ + '_id' => new ObjectId($test->id), + 'nullableField' => null, + 'nullableReferenceOne' => null, + 'nullableEmbedOne' => null, + ], + $result + ); + } } /** @ODM\Document */ @@ -44,16 +92,27 @@ class UpsertTestUser /** @ODM\Id */ public $id; + /** @ODM\Field(nullable=true) */ + public $nullableField; + + /** @ODM\EmbedOne(targetDocument=UpsertTestUserEmbedded::class, nullable=true) */ + public $nullableEmbedOne; + + /** @ODM\ReferenceOne(targetDocument=UpsertTestUser::class, cascade="persist", nullable=true) */ + public $nullableReferenceOne; + /** @ODM\EmbedMany(targetDocument=UpsertTestUserEmbedded::class) */ public $embedMany; + + public function __construct() + { + $this->id = (string) new ObjectId(); + } } /** @ODM\EmbeddedDocument */ class UpsertTestUserEmbedded { - /** @ODM\Id */ - public $id; - /** @ODM\Field(type="string") */ public $test; } diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Query/BuilderTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Query/BuilderTest.php index 9a644634ef..25420c34be 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Query/BuilderTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Query/BuilderTest.php @@ -19,6 +19,7 @@ use InvalidArgumentException; use IteratorAggregate; use MongoDB\BSON\ObjectId; +use MongoDB\BSON\Regex; use MongoDB\Driver\ReadPreference; use ReflectionProperty; @@ -299,6 +300,19 @@ public function testAddNot() $this->assertEquals($expected, $qb->getQueryArray()); } + public function testNotAllowsRegex() + { + $qb = $this->getTestQueryBuilder(); + $qb->field('username')->not(new Regex('Boo', 'i')); + + $expected = [ + 'username' => [ + '$not' => new Regex('Boo', 'i'), + ], + ]; + $this->assertEquals($expected, $qb->getQueryArray()); + } + public function testFindQuery() { $qb = $this->getTestQueryBuilder() diff --git a/tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkTest.php b/tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkTest.php index 5e5f3bd312..081058422f 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkTest.php @@ -116,6 +116,24 @@ public function testRegisterRemovedOnNewEntityIsIgnored() $this->assertFalse($this->uow->isScheduledForDelete($user)); } + public function testScheduleForDeleteShouldUnregisterScheduledUpserts() + { + $class = $this->dm->getClassMetadata(ForumUser::class); + $user = new ForumUser(); + $user->id = new ObjectId(); + $this->assertFalse($this->uow->isScheduledForInsert($user)); + $this->assertFalse($this->uow->isScheduledForUpsert($user)); + $this->assertFalse($this->uow->isScheduledForDelete($user)); + $this->uow->scheduleForUpsert($class, $user); + $this->assertFalse($this->uow->isScheduledForInsert($user)); + $this->assertTrue($this->uow->isScheduledForUpsert($user)); + $this->assertFalse($this->uow->isScheduledForDelete($user)); + $this->uow->scheduleForDelete($user); + $this->assertFalse($this->uow->isScheduledForInsert($user)); + $this->assertFalse($this->uow->isScheduledForUpsert($user)); + $this->assertTrue($this->uow->isScheduledForDelete($user)); + } + public function testThrowsOnPersistOfMappedSuperclass() { $this->expectException(MongoDBException::class); diff --git a/tests/Documents/User.php b/tests/Documents/User.php index a7de6df61c..688acfdc8f 100644 --- a/tests/Documents/User.php +++ b/tests/Documents/User.php @@ -28,9 +28,12 @@ class User extends BaseDocument /** @ODM\Field(type="date") */ protected $createdAt; - /** @ODM\EmbedOne(targetDocument=Address::class, nullable=true) */ + /** @ODM\EmbedOne(targetDocument=Address::class) */ protected $address; + /** @ODM\EmbedOne(targetDocument=Address::class, nullable=true) */ + protected $addressNullable; + /** @ODM\ReferenceOne(targetDocument=Profile::class, cascade={"all"}) */ protected $profile; @@ -181,12 +184,14 @@ public function getAddress() public function setAddress(?Address $address = null) { - $this->address = $address; + $this->address = $address; + $this->addressNullable = $address ? clone $address : $address; } public function removeAddress() { - $this->address = null; + $this->address = null; + $this->addressNullable = null; } public function setProfile(Profile $profile) diff --git a/tests/Documents74/GH2310Container.php b/tests/Documents74/GH2310Container.php new file mode 100644 index 0000000000..bfa5c7fa23 --- /dev/null +++ b/tests/Documents74/GH2310Container.php @@ -0,0 +1,34 @@ +id = $id; + $this->embedded = $embedded; + } +} + +/** + * @ODM\EmbeddedDocument + */ +class GH2310Embedded +{ + /** @ODM\Field(type="integer") */ + public int $value; +}