From 185b4f20b929691cb8c1e94fb18ccdbc1895fb62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mor=C3=A1vek?= Date: Sat, 7 Nov 2026 16:45:12 +0100 Subject: [PATCH 1/4] Container: getValues('array') returns array The 'array' branch is checked first in the conditional return type, so PHPStan no longer reports array|array and verifies the return statement of getValues(), which makes its ignoreErrors entry obsolete. Co-Authored-By: Claude Code --- phpstan.neon | 5 ----- src/Forms/Container.php | 4 ++-- tests/types/forms-types.php | 7 +++++-- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 8ee0af89..d841dda9 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -113,11 +113,6 @@ parameters: message: '#^Parameter \#1 \$returnType of method Nette\\Forms\\Container::(getValues|getUntrustedValues)\(\) expects#' path: src/Forms/Container.php - - # Complex template type T cannot be fully tracked through getValues delegation - message: '#^Method Nette\\Forms\\Container::getValues\(\) should return#' - path: src/Forms/Container.php - count: 1 - - message: '#^Call to function is_string\(\) with string will always evaluate to true\.$#' identifier: function.alreadyNarrowedType diff --git a/src/Forms/Container.php b/src/Forms/Container.php index 590efa9f..cf94bf65 100644 --- a/src/Forms/Container.php +++ b/src/Forms/Container.php @@ -118,7 +118,7 @@ public function setValues(array|object $values, bool $erase = false, bool $onlyD * @template T of object * @param class-string|T|'array'|null $returnType * @param ?list $controls - * @return ($returnType is class-string|T ? T : ($returnType is 'array' ? mixed[] : ArrayHash)) + * @return ($returnType is 'array' ? array : ($returnType is class-string|T ? T : ArrayHash)) */ public function getValues(string|object|null $returnType = null, ?array $controls = null): object|array { @@ -154,7 +154,7 @@ public function getValues(string|object|null $returnType = null, ?array $control * @template T of object * @param class-string|T|'array'|null $returnType * @param ?list $controls - * @return ($returnType is class-string|T ? T : ($returnType is 'array' ? mixed[] : ArrayHash)) + * @return ($returnType is 'array' ? array : ($returnType is class-string|T ? T : ArrayHash)) */ public function getUntrustedValues(string|object|null $returnType = null, ?array $controls = null): object|array { diff --git a/tests/types/forms-types.php b/tests/types/forms-types.php index 396147fa..4e9e56d6 100644 --- a/tests/types/forms-types.php +++ b/tests/types/forms-types.php @@ -19,15 +19,18 @@ class FormDto function testFormContainerGetValues(Container $container): void { assertType('Nette\Utils\ArrayHash', $container->getValues()); - assertType('array|array', $container->getValues('array')); + assertType('array', $container->getValues('array')); + assertType('array', $container->getValues(Container::Array)); assertType(FormDto::class, $container->getValues(FormDto::class)); + assertType('array|Nette\Utils\ArrayHash', $container->getValues(rand() ? 'array' : null)); + assertType('array|FormDto', $container->getValues(rand() ? 'array' : FormDto::class)); } function testFormContainerGetUntrustedValues(Container $container): void { assertType('Nette\Utils\ArrayHash', $container->getUntrustedValues()); - assertType('array|array', $container->getUntrustedValues('array')); + assertType('array', $container->getUntrustedValues('array')); assertType(FormDto::class, $container->getUntrustedValues(FormDto::class)); } From e20454115f082264403d1aa7c47257dad3948384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mor=C3=A1vek?= Date: Sat, 7 Nov 2026 16:45:12 +0100 Subject: [PATCH 2/4] Container: controls are typed with string keys getControls() yields control names, which are always strings; a generator keeps them as is, so numeric names are not turned into integers. Co-Authored-By: Claude Code --- src/Forms/Container.php | 8 +++++--- tests/types/forms-types.php | 7 +++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Forms/Container.php b/src/Forms/Container.php index cf94bf65..1ea6e2e9 100644 --- a/src/Forms/Container.php +++ b/src/Forms/Container.php @@ -18,7 +18,7 @@ * * @property-read string $name * @property ArrayHash $values - * @property-read \Iterator $controls + * @property-read \Iterator $controls * @property-read ?Form $form * @implements \ArrayAccess */ @@ -356,13 +356,15 @@ public function addComponent( /** * Iterates over all form controls including nested. Keys are control names and are not unique across containers. - * @return iterable + * @return iterable */ public function getControls(): iterable { foreach ($this->getComponentTree() as $component) { if ($component instanceof Control) { - yield $component->getName() => $component; + $name = $component->getName(); + assert($name !== null); + yield $name => $component; } } } diff --git a/tests/types/forms-types.php b/tests/types/forms-types.php index 4e9e56d6..60ffd708 100644 --- a/tests/types/forms-types.php +++ b/tests/types/forms-types.php @@ -67,3 +67,10 @@ function testFormGetSubmittedData(Form $form, Nette\Forms\Container $container): assertType('array', $form->getSubmittedData()); assertType('array', $container->getSubmittedData()); } + + +function testFormContainerControls(Container $container): void +{ + assertType('Iterator', $container->controls); + assertType('iterable', $container->getControls()); +} From 85390cb5397158e389b146d79121f11f01e6ced6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mor=C3=A1vek?= Date: Sat, 7 Nov 2026 16:45:12 +0100 Subject: [PATCH 3/4] errors and multiple uploads are typed as lists The $errors and $ownErrors properties match getErrors() and getOwnErrors(), including Stringable messages. A multiple UploadControl returns a list, as its value is read with Form::DataList. Co-Authored-By: Claude Code --- src/Forms/Controls/BaseControl.php | 2 +- src/Forms/Controls/UploadControl.php | 2 +- src/Forms/Form.php | 4 ++-- tests/types/forms-types.php | 14 ++++++++++++++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/Forms/Controls/BaseControl.php b/src/Forms/Controls/BaseControl.php index b35f8777..be2dab0f 100644 --- a/src/Forms/Controls/BaseControl.php +++ b/src/Forms/Controls/BaseControl.php @@ -31,7 +31,7 @@ * @property-read Html $labelPrototype * @property bool $required * @property-deprecated bool $filled - * @property-read string[] $errors + * @property-read list $errors * @property-deprecated array $options * @property-read string $error */ diff --git a/src/Forms/Controls/UploadControl.php b/src/Forms/Controls/UploadControl.php index 286cb0da..f27f6c54 100644 --- a/src/Forms/Controls/UploadControl.php +++ b/src/Forms/Controls/UploadControl.php @@ -79,7 +79,7 @@ public function setValue(mixed $value): static /** * Returns the uploaded file(s), a dummy FileUpload(null) when nothing was uploaded, or null when nullable is set. - * @return FileUpload|FileUpload[]|null + * @return FileUpload|list|null */ public function getValue(): FileUpload|array|null { diff --git a/src/Forms/Form.php b/src/Forms/Form.php index b9de3b9c..88c13398 100644 --- a/src/Forms/Form.php +++ b/src/Forms/Form.php @@ -18,8 +18,8 @@ * by default the form reads its own HTTP request (standalone usage), inside Nette Application * use Nette\Application\UI\Form, other sources are set via setSubmissionSource(). * - * @property-read string[] $errors - * @property-read array $ownErrors + * @property-read list $errors + * @property-read list $ownErrors * @property-read Html $elementPrototype * @property-deprecated FormRenderer $renderer * @property-deprecated string $action diff --git a/tests/types/forms-types.php b/tests/types/forms-types.php index 60ffd708..4f087fdf 100644 --- a/tests/types/forms-types.php +++ b/tests/types/forms-types.php @@ -74,3 +74,17 @@ function testFormContainerControls(Container $container): void assertType('Iterator', $container->controls); assertType('iterable', $container->getControls()); } + + +function testFormErrors(Form $form, Nette\Forms\Controls\TextInput $input): void +{ + assertType('list', $form->errors); + assertType('list', $form->ownErrors); + assertType('list', $input->errors); +} + + +function testUploadControlGetValue(Nette\Forms\Controls\UploadControl $upload): void +{ + assertType('list|Nette\Http\FileUpload|null', $upload->getValue()); +} From 8cbf2187e8afd1f2cbab76c6e0ae45f9d32a6ddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mor=C3=A1vek?= Date: Sat, 7 Nov 2026 16:45:12 +0100 Subject: [PATCH 4/4] Latte, DI: more precise types of internal arrays Co-Authored-By: Claude Code --- src/Bridges/FormsDI/FormsExtension.php | 2 +- src/Bridges/FormsLatte/Runtime.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Bridges/FormsDI/FormsExtension.php b/src/Bridges/FormsDI/FormsExtension.php index cff15e24..8591ee9d 100644 --- a/src/Bridges/FormsDI/FormsExtension.php +++ b/src/Bridges/FormsDI/FormsExtension.php @@ -20,7 +20,7 @@ class FormsExtension extends Nette\DI\CompilerExtension public function __construct() { $this->config = new class { - /** @var string[] */ + /** @var array */ public array $messages = []; }; } diff --git a/src/Bridges/FormsLatte/Runtime.php b/src/Bridges/FormsLatte/Runtime.php index 670589da..8fb0206b 100644 --- a/src/Bridges/FormsLatte/Runtime.php +++ b/src/Bridges/FormsLatte/Runtime.php @@ -19,7 +19,7 @@ */ class Runtime { - /** @var Container[] */ + /** @var list */ private array $stack = []; /** @var list parallel to $stack; non-null element = id of a detached form */ @@ -28,7 +28,7 @@ class Runtime /** @var list parallel to $stack; true = scope the template's $form variable was bound to */ private array $formBindings = []; - /** @var Nette\Forms\Repeater[] */ + /** @var list */ private array $repeaters = [];