fix: wrap bare intersection types in parentheses when adding nullable (#1756)

When a MorphTo relation's docblock uses a bare intersection type in the
generic parameter (e.g. `MorphTo<BaseModel&CanBeAssigned, $this>`), the
generated property type incorrectly becomes `BaseModel&CanBeAssigned|null`
instead of the correct `(BaseModel&CanBeAssigned)|null`.

This adds `wrapIntersectionType()` which ensures bare intersection types
are wrapped in parentheses before `|null` is appended, producing valid
DNF type syntax. Applied in both `setProperty()` and `applyNullability()`.
This commit is contained in:
Pieter Willekens
2026-02-10 15:19:29 +01:00
committed by GitHub
parent 5aca186d92
commit 3bd70eafd0
6 changed files with 141 additions and 2 deletions
+18 -2
View File
@@ -534,7 +534,7 @@ class ModelsCommand extends Command
}
if ($isNullable) {
$type .= '|null';
$type = $this->wrapIntersectionType($type) . '|null';
} else {
$type = str_replace($nullString, '', $type);
}
@@ -542,6 +542,22 @@ class ModelsCommand extends Command
return $type;
}
/**
* Wraps a bare intersection type in parentheses for correct DNF syntax.
*
* For example, `A&B` becomes `(A&B)` so that adding `|null` produces
* `(A&B)|null` instead of the ambiguous `A&B|null`.
* Types that are already parenthesized or contain union types are returned as-is.
*/
protected function wrapIntersectionType(string $type): string
{
if (str_contains($type, '&') && !str_contains($type, '|') && $type[0] !== '(') {
return '(' . $type . ')';
}
return $type;
}
/**
* Returns the override type for the give type.
*
@@ -995,7 +1011,7 @@ class ModelsCommand extends Command
if ($type !== null) {
$newType = $this->getTypeOverride($type);
if ($nullable) {
$newType .= '|null';
$newType = $this->wrapIntersectionType($newType) . '|null';
}
$this->properties[$name]['type'] = $newType;
}