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 dfe9b317da..37d1f49186 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'] === ClassMetadata::MANY && $new) { + } elseif (isset($mapping['association']) && $mapping['type'] === ClassMetadata::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 aa94b86514..17272863db 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..afb54dcb07 100644 --- a/lib/Doctrine/ODM/MongoDB/Query/Expr.php +++ b/lib/Doctrine/ODM/MongoDB/Query/Expr.php @@ -861,7 +861,7 @@ 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 $expression */ public function not($expression): self { diff --git a/lib/Doctrine/ODM/MongoDB/UnitOfWork.php b/lib/Doctrine/ODM/MongoDB/UnitOfWork.php index a9cdaabbf2..5cef8cf6ba 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/tests/Doctrine/ODM/MongoDB/Tests/Functional/EmbeddedTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/EmbeddedTest.php index e64d68e896..0135b84829 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(): void $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 f396968c33..59d1183c50 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/QueryTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/QueryTest.php @@ -16,6 +16,7 @@ use InvalidArgumentException; use IteratorAggregate; use MongoDB\BSON\ObjectId; +use MongoDB\BSON\Regex; use MongoDB\BSON\UTCDateTime; use function array_values; @@ -102,6 +103,27 @@ public function testAddNot(): void $this->assertNotNull($user); } + public function testNotAllowsRegex(): void + { + $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(): void { $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 620ac7112b..c76b1f9aac 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(): void { - $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(): void $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 UpsertTestUser); + 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 cc216afd98..b13cea35a0 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Query/BuilderTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Query/BuilderTest.php @@ -20,6 +20,7 @@ use InvalidArgumentException; use IteratorAggregate; use MongoDB\BSON\ObjectId; +use MongoDB\BSON\Regex; use MongoDB\Driver\ReadPreference; use PHPUnit\Framework\MockObject\MockObject; use ReflectionProperty; @@ -301,6 +302,19 @@ public function testAddNot(): void $this->assertEquals($expected, $qb->getQueryArray()); } + public function testNotAllowsRegex(): void + { + $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(): void { $qb = $this->getTestQueryBuilder() diff --git a/tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkTest.php b/tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkTest.php index 7b7802f74a..17626c86a3 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/UnitOfWorkTest.php @@ -117,6 +117,24 @@ public function testRegisterRemovedOnNewEntityIsIgnored(): void $this->assertFalse($this->uow->isScheduledForDelete($user)); } + public function testScheduleForDeleteShouldUnregisterScheduledUpserts(): void + { + $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(): void { $this->expectException(MongoDBException::class); diff --git a/tests/Documents/User.php b/tests/Documents/User.php index 73edb56fc1..603ae7f50f 100644 --- a/tests/Documents/User.php +++ b/tests/Documents/User.php @@ -29,9 +29,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; @@ -182,12 +185,14 @@ public function getAddress() public function setAddress(?Address $address = null): void { - $this->address = $address; + $this->address = $address; + $this->addressNullable = $address ? clone $address : $address; } public function removeAddress(): void { - $this->address = null; + $this->address = null; + $this->addressNullable = null; } public function setProfile(Profile $profile): void 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; +}