diff --git a/src/Util/ClassSourceManipulator.php b/src/Util/ClassSourceManipulator.php index 7d48ee63e..0c9687891 100644 --- a/src/Util/ClassSourceManipulator.php +++ b/src/Util/ClassSourceManipulator.php @@ -531,12 +531,14 @@ private function addSingularRelation(BaseRelation $relation): void $setterNodeBuilder = $this->createSetterNodeBuilder( $relation->getPropertyName(), $typeHint, - // make the type-hint nullable always for ManyToOne to allow the owning - // side to be set to null, which is needed for orphanRemoval - // (specifically: when you set the inverse side, the generated - // code will *also* set the owning side to null - so it needs to be allowed) - // e.g. $userAvatarPhoto->setUser(null); - $relation instanceof RelationOneToOne ? $relation->isNullable() : true + // A ManyToOne setter must stay nullable as soon as the relation maps an + // inverse side: the collection remove*() generated on that side sets the + // owning side back to null (orphanRemoval), e.g. $userAvatarPhoto->setUser(null); + // so null must be allowed there. A non-nullable, *unidirectional* ManyToOne + // has no such code path, so its setter can use a strict, non-nullable type-hint. + $relation instanceof RelationOneToOne + ? $relation->isNullable() + : ($relation->isNullable() || $relation->getMapInverseRelation()) ); // set the *owning* side of the relation diff --git a/tests/Util/ClassSourceManipulatorTest.php b/tests/Util/ClassSourceManipulatorTest.php index be9a3d5d0..700f4180e 100644 --- a/tests/Util/ClassSourceManipulatorTest.php +++ b/tests/Util/ClassSourceManipulatorTest.php @@ -411,6 +411,20 @@ public static function getAddManyToOneRelationTests(): \Generator isNullable: true, ), ]; + + // A non-nullable, unidirectional ManyToOne has no inverse remove*() + // setting the owning side back to null, so its setter can be strict. + yield 'many_to_one_not_nullable_no_inverse' => [ + 'User_simple.php', + 'User_simple_not_nullable_no_inverse.php', + new RelationManyToOne( + propertyName: 'category', + targetClassName: \App\Entity\Category::class, + targetPropertyName: 'foods', + mapInverseRelation: false, + isOwning: true, + ), + ]; } /** diff --git a/tests/Util/fixtures/add_many_to_one_relation/User_simple_not_nullable_no_inverse.php b/tests/Util/fixtures/add_many_to_one_relation/User_simple_not_nullable_no_inverse.php new file mode 100644 index 000000000..8bc2d455c --- /dev/null +++ b/tests/Util/fixtures/add_many_to_one_relation/User_simple_not_nullable_no_inverse.php @@ -0,0 +1,35 @@ +id; + } + + public function getCategory(): ?Category + { + return $this->category; + } + + public function setCategory(Category $category): static + { + $this->category = $category; + + return $this; + } +}