diff --git a/readme.md b/readme.md index 33ba4332..288ce745 100644 --- a/readme.md +++ b/readme.md @@ -538,7 +538,9 @@ Each type or union/intersection type can be passed as a string, you can also use use Nette\PhpGenerator\Type; $member->setType('array'); // or Type::Array; -$member->setType('array|string'); // or Type::union('array', 'string') +$member->setType('?array'); // or Type::nullable(Type::Array); +$member->setType('array|string'); // or Type::union(Type::Array, Type::String) +$member->setType('array|string|null'); // or Type::nullable(Type::union(Type::Array, Type::String)) $member->setType('Foo&Bar'); // or Type::intersection(Foo::class, Bar::class) $member->setType(null); // removes type ``` diff --git a/src/PhpGenerator/Type.php b/src/PhpGenerator/Type.php index d122ab71..3bc96428 100644 --- a/src/PhpGenerator/Type.php +++ b/src/PhpGenerator/Type.php @@ -9,6 +9,8 @@ namespace Nette\PhpGenerator; +use Nette; + /** * PHP return, property and parameter types. @@ -85,7 +87,17 @@ class Type public static function nullable(string $type, bool $nullable = true): string { - return ($nullable ? '?' : '') . ltrim($type, '?'); + if (str_contains($type, '&')) { + return $nullable + ? throw new Nette\InvalidArgumentException('Intersection types cannot be nullable.') + : $type; + } + + $nnType = preg_replace('#^\?|^null\||\|null(?=\||$)#i', '', $type); + if ($nullable && $type === $nnType) { + $type = str_contains($type, '|') ? $type . '|null' : '?' . $type; + } + return $nullable ? $type : $nnType; } diff --git a/tests/PhpGenerator/Type.phpt b/tests/PhpGenerator/Type.phpt index aa737db6..9dd76e34 100644 --- a/tests/PhpGenerator/Type.phpt +++ b/tests/PhpGenerator/Type.phpt @@ -6,12 +6,42 @@ use Nette\PhpGenerator\Type; use Tester\Assert; require __DIR__ . '/../bootstrap.php'; +// Nullable +Assert::same('?int', Type::nullable(Type::Int)); +Assert::same('int', Type::nullable(Type::Int, nullable: false)); -Assert::same('A|string', Type::union(A::class, Type::String)); +Assert::same('?int', Type::nullable('?int')); +Assert::same('int', Type::nullable('?int', nullable: false)); + +// TODO: +Assert::same('null', Type::nullable('null')); +Assert::same('...', Type::nullable('null', nullable: false)); +Assert::same('mixed', Type::nullable('mixed')); +Assert::same('...', Type::nullable('mixed', nullable: false)); + + +Assert::same('int|float|string|null', Type::nullable('int|float|string')); +Assert::same('int|float|string', Type::nullable('int|float|string', nullable: false)); + +Assert::same('NULL|int|float|string', Type::nullable('NULL|int|float|string')); +Assert::same('int|float|string', Type::nullable('NULL|int|float|string', nullable: false)); -Assert::same('?A', Type::nullable(A::class)); -Assert::same('?A', Type::nullable(A::class)); -Assert::same('A', Type::nullable(A::class, nullable: false)); +Assert::same('int|float|string|null', Type::nullable('int|float|string|null')); +Assert::same('int|float|string', Type::nullable('int|float|string|null', nullable: false)); + +Assert::same('int|float|null|string', Type::nullable('int|float|null|string')); +Assert::same('int|float|string', Type::nullable('int|float|null|string', nullable: false)); + +Assert::exception( + fn() => Type::nullable('Foo&Bar'), + Nette\InvalidArgumentException::class, + 'Intersection types cannot be nullable.', +); +Assert::same('Foo&Bar', Type::nullable('Foo&Bar', nullable: false)); + + +// Union +Assert::same('A|string', Type::union(A::class, Type::String)); -Assert::same('?A', Type::nullable('?A')); -Assert::same('A', Type::nullable('?A', nullable: false)); +// Intersection +Assert::same('A&string', Type::intersection(A::class, Type::String));