diff --git a/src/Console/ModelsCommand.php b/src/Console/ModelsCommand.php index be35b39..9303d80 100644 --- a/src/Console/ModelsCommand.php +++ b/src/Console/ModelsCommand.php @@ -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; } diff --git a/tests/Console/ModelsCommand/MorphToIntersection/Models/BaseModel.php b/tests/Console/ModelsCommand/MorphToIntersection/Models/BaseModel.php new file mode 100644 index 0000000..fc012e4 --- /dev/null +++ b/tests/Console/ModelsCommand/MorphToIntersection/Models/BaseModel.php @@ -0,0 +1,11 @@ + */ + public function assigneeWithParens(): MorphTo + { + return $this->morphTo(type: 'nullable_relation_morph_to_type', id: 'nullable_relation_morph_to_id'); + } + + /** @return MorphTo */ + public function assigneeWithoutParens(): MorphTo + { + return $this->morphTo(type: 'nullable_relation_morph_to_type', id: 'nullable_relation_morph_to_id'); + } + + /** @return MorphTo<(BaseModel&CanBeAssigned), $this> */ + public function nonNullableAssignee(): MorphTo + { + return $this->morphTo(type: 'relation_morph_to_type', id: 'relation_morph_to_id'); + } +} diff --git a/tests/Console/ModelsCommand/MorphToIntersection/Test.php b/tests/Console/ModelsCommand/MorphToIntersection/Test.php new file mode 100644 index 0000000..d99a7b1 --- /dev/null +++ b/tests/Console/ModelsCommand/MorphToIntersection/Test.php @@ -0,0 +1,24 @@ +app->make(ModelsCommand::class); + + $tester = $this->runCommand($command, [ + '--write' => true, + ]); + + $this->assertSame(0, $tester->getStatusCode()); + $this->assertStringContainsString('Written new phpDocBlock to', $tester->getDisplay()); + $this->assertMatchesMockedSnapshot(); + } +} diff --git a/tests/Console/ModelsCommand/MorphToIntersection/__snapshots__/Test__test__1.php b/tests/Console/ModelsCommand/MorphToIntersection/__snapshots__/Test__test__1.php new file mode 100644 index 0000000..0e5d6ba --- /dev/null +++ b/tests/Console/ModelsCommand/MorphToIntersection/__snapshots__/Test__test__1.php @@ -0,0 +1,48 @@ +|MorphToIntersection newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|MorphToIntersection newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|MorphToIntersection query() + * @method static \Illuminate\Database\Eloquent\Builder|MorphToIntersection whereNullableRelationMorphToId($value) + * @method static \Illuminate\Database\Eloquent\Builder|MorphToIntersection whereNullableRelationMorphToType($value) + * @method static \Illuminate\Database\Eloquent\Builder|MorphToIntersection whereRelationMorphToId($value) + * @method static \Illuminate\Database\Eloquent\Builder|MorphToIntersection whereRelationMorphToType($value) + * @mixin \Eloquent + */ +class MorphToIntersection extends Model +{ + protected $table = 'morphs'; + + /** @return MorphTo<(BaseModel&CanBeAssigned), $this> */ + public function assigneeWithParens(): MorphTo + { + return $this->morphTo(type: 'nullable_relation_morph_to_type', id: 'nullable_relation_morph_to_id'); + } + + /** @return MorphTo */ + public function assigneeWithoutParens(): MorphTo + { + return $this->morphTo(type: 'nullable_relation_morph_to_type', id: 'nullable_relation_morph_to_id'); + } + + /** @return MorphTo<(BaseModel&CanBeAssigned), $this> */ + public function nonNullableAssignee(): MorphTo + { + return $this->morphTo(type: 'relation_morph_to_type', id: 'relation_morph_to_id'); + } +}