From 59931b0d7b2ebc56ad4df99e46cf39510c68da95 Mon Sep 17 00:00:00 2001 From: Alex Martin Date: Tue, 17 Mar 2026 16:55:37 +0100 Subject: [PATCH] fix(fixture-factory): avoid inversedBy on inverse-side mappings Doctrine ORM 3 returns mapping objects for inverse one-to-one associations that do not expose an inversedBy property. Accessing it triggers a runtime error. Guard the property access and return null when inversedBy is unavailable so association updates are skipped safely. Add a regression test with a single-valued inverse-side one-to-one association to prove fixture creation no longer fails. --- example/src/Entity/Device.php | 68 ++++++++++++++++++++++++++++++ example/src/Entity/Fingerprint.php | 63 +++++++++++++++++++++++++++ src/FixtureFactory.php | 6 ++- test/Unit/FixtureFactoryTest.php | 24 +++++++++++ 4 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 example/src/Entity/Device.php create mode 100644 example/src/Entity/Fingerprint.php diff --git a/example/src/Entity/Device.php b/example/src/Entity/Device.php new file mode 100644 index 00000000..930e0ee5 --- /dev/null +++ b/example/src/Entity/Device.php @@ -0,0 +1,68 @@ +id = Uuid\Uuid::uuid4()->toString(); + $this->name = $name; + $this->fingerprint = $fingerprint; + } + + public function id(): string + { + return $this->id; + } + + public function name(): string + { + return $this->name; + } + + public function fingerprint(): ?Fingerprint + { + return $this->fingerprint; + } +} diff --git a/example/src/Entity/Fingerprint.php b/example/src/Entity/Fingerprint.php new file mode 100644 index 00000000..896866a0 --- /dev/null +++ b/example/src/Entity/Fingerprint.php @@ -0,0 +1,63 @@ +id = Uuid\Uuid::uuid4()->toString(); + $this->token = $token; + } + + public function id(): string + { + return $this->id; + } + + public function token(): string + { + return $this->token; + } + + public function device(): ?Device + { + return $this->device; + } +} diff --git a/src/FixtureFactory.php b/src/FixtureFactory.php index eca71e8a..424413b0 100644 --- a/src/FixtureFactory.php +++ b/src/FixtureFactory.php @@ -445,10 +445,14 @@ private static function resolveInversedBy( $association = $classMetadata->getAssociationMapping($fieldName); if (\is_array($association)) { - return $association['inversedBy']; + return $association['inversedBy'] ?? null; } if ($association instanceof ORM\Mapping\AssociationMapping) { + if (!\property_exists($association, 'inversedBy')) { + return null; + } + return $association->inversedBy; } diff --git a/test/Unit/FixtureFactoryTest.php b/test/Unit/FixtureFactoryTest.php index b93c164b..dc1761e6 100644 --- a/test/Unit/FixtureFactoryTest.php +++ b/test/Unit/FixtureFactoryTest.php @@ -516,6 +516,30 @@ public function testCreateOneEstablishesBidirectionalOneToManyAssociations(): vo self::assertContains($repository, $organization->repositories()); } + public function testCreateOneDoesNotFailForSingleValuedInverseSideAssociationsWithoutInversedBy(): void + { + $faker = self::faker(); + + $fixtureFactory = new FixtureFactory( + self::entityManager(), + $faker, + ); + + $fixtureFactory->define(Entity\Device::class, [ + 'name' => $faker->word(), + ]); + + $fixtureFactory->define(Entity\Fingerprint::class, [ + 'token' => $faker->sha1(), + 'device' => FieldDefinition::reference(Entity\Device::class), + ]); + + /** @var Entity\Fingerprint $fingerprint */ + $fingerprint = $fixtureFactory->createOne(Entity\Fingerprint::class); + + self::assertInstanceOf(Entity\Device::class, $fingerprint->device()); + } + public function testOptionalFieldValuesAreSetToNullWhenFakerReturnsFalse(): void { $fixtureFactory = new FixtureFactory(