2222use ReflectionClass ;
2323use ReflectionNamedType ;
2424use ReflectionProperty ;
25+ use ReflectionType ;
2526use Stringable ;
2627
2728use function array_column ;
@@ -556,7 +557,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
556557 * @param string $name The name of the entity class the new instance is used for.
557558 * @phpstan-param class-string<T> $name
558559 */
559- public function __construct (public string $ name , NamingStrategy |null $ namingStrategy = null , TypedFieldMapper |null $ typedFieldMapper = null )
560+ public function __construct (public string $ name , NamingStrategy |null $ namingStrategy = null , TypedFieldMapper |null $ typedFieldMapper = null , public readonly bool $ inferPhpNullability = false )
560561 {
561562 $ this ->rootEntityName = $ name ;
562563 $ this ->namingStrategy = $ namingStrategy ?? new DefaultNamingStrategy ();
@@ -1124,14 +1125,12 @@ private function validateAndCompleteTypedFieldMapping(array $mapping): array
11241125 /**
11251126 * Validates & completes the basic mapping information based on typed property.
11261127 *
1127- * @param array{type: self::ONE_TO_ONE|self::MANY_TO_ONE|self::ONE_TO_MANY|self::MANY_TO_MANY, fieldName: string, targetEntity?: class-string} $mapping The mapping.
1128+ * @param array{type: self::ONE_TO_ONE|self::MANY_TO_ONE|self::ONE_TO_MANY|self::MANY_TO_MANY, fieldName: string, targetEntity?: class-string, joinColumns: array<int, array<string, mixed>>|null } $mapping The mapping.
11281129 *
11291130 * @return mixed[] The updated mapping.
11301131 */
1131- private function validateAndCompleteTypedAssociationMapping (array $ mapping ): array
1132+ private function validateAndCompleteTypedAssociationMapping (array $ mapping, ReflectionType | null $ type ): array
11321133 {
1133- $ type = $ this ->reflClass ->getProperty ($ mapping ['fieldName ' ])->getType ();
1134-
11351134 if ($ type === null || ($ mapping ['type ' ] & self ::TO_ONE ) === 0 ) {
11361135 return $ mapping ;
11371136 }
@@ -1152,6 +1151,7 @@ private function validateAndCompleteTypedAssociationMapping(array $mapping): arr
11521151 * id?: bool,
11531152 * generated?: self::GENERATED_*,
11541153 * enumType?: class-string,
1154+ * nullable?: bool|null,
11551155 * } $mapping The field mapping to validate & complete.
11561156 *
11571157 * @return FieldMapping The updated mapping.
@@ -1165,10 +1165,17 @@ protected function validateAndCompleteFieldMapping(array $mapping): FieldMapping
11651165 throw MappingException::missingFieldName ($ this ->name );
11661166 }
11671167
1168+ $ type = null ;
11681169 if ($ this ->isTypedProperty ($ mapping ['fieldName ' ])) {
1170+ $ type = $ this ->reflClass ->getProperty ($ mapping ['fieldName ' ])->getType ();
11691171 $ mapping = $ this ->validateAndCompleteTypedFieldMapping ($ mapping );
11701172 }
11711173
1174+ // Infer nullable from type or reset null back to true if type is missing
1175+ if ($ this ->inferPhpNullability && ! isset ($ mapping ['nullable ' ])) {
1176+ $ mapping ['nullable ' ] = $ type ?->allowsNull() ?? false ;
1177+ }
1178+
11721179 if (! isset ($ mapping ['type ' ])) {
11731180 // Default to string
11741181 $ mapping ['type ' ] = 'string ' ;
@@ -1276,8 +1283,29 @@ protected function _validateAndCompleteAssociationMapping(array $mapping): Assoc
12761283 // the sourceEntity.
12771284 $ mapping ['sourceEntity ' ] = $ this ->name ;
12781285
1286+ $ type = null ;
12791287 if ($ this ->isTypedProperty ($ mapping ['fieldName ' ])) {
1280- $ mapping = $ this ->validateAndCompleteTypedAssociationMapping ($ mapping );
1288+ $ type = $ this ->reflClass ->getProperty ($ mapping ['fieldName ' ])->getType ();
1289+ $ mapping = $ this ->validateAndCompleteTypedAssociationMapping ($ mapping , $ type );
1290+ }
1291+
1292+ // Infer nullable from type or reset null back to true if type is missing
1293+ if ($ this ->inferPhpNullability && $ mapping ['type ' ] & self ::TO_ONE ) {
1294+ if (! empty ($ mapping ['joinColumns ' ])) {
1295+ foreach ($ mapping ['joinColumns ' ] as $ key => $ data ) {
1296+ if (! isset ($ data ['nullable ' ])) {
1297+ $ mapping ['joinColumns ' ][$ key ]['nullable ' ] = $ type ?->allowsNull() ?? true ;
1298+ }
1299+ }
1300+ } elseif ($ type !== null ) {
1301+ $ mapping ['joinColumns ' ] = [
1302+ [
1303+ 'fieldName ' => $ mapping ['fieldName ' ],
1304+ 'nullable ' => $ type ->allowsNull (),
1305+ 'referencedColumnName ' => $ this ->namingStrategy ->referenceColumnName (),
1306+ ],
1307+ ];
1308+ }
12811309 }
12821310
12831311 if (isset ($ mapping ['targetEntity ' ])) {
0 commit comments