diff --git a/readme.md b/readme.md index 33ba4332..aa96e1d5 100644 --- a/readme.md +++ b/readme.md @@ -538,7 +538,8 @@ 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('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..0b519e57 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,25 @@ 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); + $always = (bool) preg_match('#^(null|mixed)$#i', $nnType); + if ($nullable) { + return match (true) { + $always, $type !== $nnType => $type, + str_contains($type, '|') => $type . '|null', + default => '?' . $type, + }; + } else { + return $always + ? throw new Nette\InvalidArgumentException("Type $type cannot be not nullable.") + : $nnType; + } } diff --git a/tests/PhpGenerator/Type.phpt b/tests/PhpGenerator/Type.phpt index aa737db6..a2f54cee 100644 --- a/tests/PhpGenerator/Type.phpt +++ b/tests/PhpGenerator/Type.phpt @@ -6,12 +6,50 @@ 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)); + +Assert::same('null', Type::nullable('null')); +Assert::same('NULL', Type::nullable('NULL')); +Assert::exception( + fn() => Type::nullable('null', nullable: false), + Nette\InvalidArgumentException::class, + 'Type null cannot be not nullable.', +); + +Assert::same('mixed', Type::nullable('mixed')); +Assert::exception( + fn() => Type::nullable('mixed', nullable: false), + Nette\InvalidArgumentException::class, + 'Type mixed cannot be not nullable.', +); + +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));