From a2e403cea19ba44591e7097699ff392e4ac22969 Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Mon, 13 Jul 2026 02:10:16 +0200 Subject: [PATCH 1/7] composer: Fix `ci` script For some reason, it was introduced escaped in 23a0df98bf80251cc5129948bc4c4fcc1d6ad817, which is not supported and would fail with: sh: line 1: [@lint,: command not found The proper format is using JSON arrays: https://getcomposer.org/doc/articles/scripts.md#referencing-scripts --- composer.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 16addb8..b9529f2 100644 --- a/composer.json +++ b/composer.json @@ -67,6 +67,12 @@ "ecs": "php ./vendor/bin/ecs", "rector": "php ./vendor/bin/rector process --dry-run --ansi", "test": "php ./vendor/bin/tester -s -p php --colors 1 -C ./tests/Replicator", - "ci": "[\"@lint\", \"@phpstan\", \"@ecs\", \"@rector\", \"@test\"]" + "ci": [ + "@lint", + "@phpstan", + "@ecs", + "@rector", + "@test" + ] } } From dd0f067b900c90676ae3b86288200fe549528607 Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Mon, 13 Jul 2026 01:16:07 +0200 Subject: [PATCH 2/7] Fix deprecated `Component::monitor()` usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Calling monitor with implicit handlers has been deprecated since component-model 3.0: https://github.com/nette/component-model/commit/857a493aa995f4b71ebb48df5c13e0d5f04e6e82 And has been removed in 4.0: https://github.com/nette/component-model/commit/71321c9d8f3af9d69a7bd223b51a8ea0baf66a57 Let’s switch to explicit handler. Using different name to avoid deprecation warnings with nette/component-model 3.2.0. --- src/Replicator/Container.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Replicator/Container.php b/src/Replicator/Container.php index 1755dbb..5d38c66 100644 --- a/src/Replicator/Container.php +++ b/src/Replicator/Container.php @@ -66,8 +66,8 @@ class Container extends Nette\Forms\Container */ public function __construct(callable $factory, int $createDefault = 0, bool $forceDefault = FALSE) { - $this->monitor(Nette\Application\UI\Presenter::class); - $this->monitor(Nette\Forms\Form::class); + $this->monitor(Nette\Application\UI\Presenter::class, $this->componentAttached(...)); + $this->monitor(Nette\Forms\Form::class, $this->componentAttached(...)); try { $this->factoryCallback = Closure::fromCallable($factory); @@ -92,10 +92,8 @@ public function setFactory(callable $factory): void /** * Magical component factory */ - protected function attached(Nette\ComponentModel\IComponent $obj): void + private function componentAttached(Nette\ComponentModel\IComponent $obj): void { - parent::attached($obj); - if ( !$obj instanceof Nette\Application\UI\Presenter && From f97b315ef63608b02acfd1c3f17bab7a9df0ff86 Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Mon, 13 Jul 2026 01:20:42 +0200 Subject: [PATCH 3/7] Use proper getters for components component-model 4.0 removes the use of `Nette\SmartObject` so we can no longer access the private properties: https://github.com/nette/component-model/commit/c07bbeebfd49149ead63aef7e7750813bade343c --- src/Replicator/Container.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Replicator/Container.php b/src/Replicator/Container.php index 5d38c66..4dc694e 100644 --- a/src/Replicator/Container.php +++ b/src/Replicator/Container.php @@ -141,7 +141,7 @@ protected function createComponent(string $name): ?Nette\ComponentModel\ICompone ($this->factoryCallback)($container); - return $this->created[$container->name] = $container; + return $this->created[$container->getName()] = $container; } private function getFirstControlName(): ?string @@ -272,8 +272,8 @@ private function getHttpData(): ?array */ public function remove(Nette\ComponentModel\Container $container, bool $cleanUpGroups = FALSE): void { - if ($container->parent !== $this) { - throw new Nette\InvalidArgumentException('Given component ' . $container->name . ' is not children of ' . $this->name . '.'); + if ($container->getParent() !== $this) { + throw new Nette\InvalidArgumentException('Given component ' . $container->getName() . ' is not children of ' . $this->getName() . '.'); } // to check if form was submitted by this one @@ -450,7 +450,7 @@ function (Nette\Forms\Controls\SubmitButton $_this, ?callable $callback = NULL) $_this->onClick[] = function (Nette\Forms\Controls\SubmitButton $button) use ($callback) { /** @var self $replicator */ $replicator = $button->lookup(static::class); - $container = $button->parent; + $container = $button->getParent(); \assert($container instanceof Nette\ComponentModel\Container); if (is_callable($callback)) { $callback($replicator, $container); From 47a7d85c8f343f99f0fe86c37ad075c699385703 Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Mon, 13 Jul 2026 02:05:28 +0200 Subject: [PATCH 4/7] Work around PHPStan `Container::getComponent()` under-inference PHPStan is confused by stubs, and infers that `Container::getComponents()` returns an untyped `array`. Perhaps because the stubs claim to return `Iterator` while upstream returns an `IComponent[]`. https://github.com/phpstan/phpstan-nette/issues/141 --- src/Replicator/Container.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Replicator/Container.php b/src/Replicator/Container.php index 4dc694e..13ab1e2 100644 --- a/src/Replicator/Container.php +++ b/src/Replicator/Container.php @@ -146,9 +146,10 @@ protected function createComponent(string $name): ?Nette\ComponentModel\ICompone private function getFirstControlName(): ?string { + // TODO: The first instanceof check should be redundant but PHPStan infers getComponent() returns untyped array. $controls = array_filter( $this->getComponents(), - fn ($component): bool => $component instanceof Nette\Forms\Control, + fn ($component): bool => $component instanceof Nette\ComponentModel\IComponent && $component instanceof Nette\Forms\Control, ); $firstControl = reset($controls); @@ -377,9 +378,11 @@ public function countFilledWithout(array $components = [], array $subComponents public function isAllFilled(array $exceptChildren = []): bool { $components = []; + + // TODO: The first instanceof check should be redundant but PHPStan infers getComponent() returns untyped array. $controls = array_filter( $this->getComponents(), - fn ($component): bool => $component instanceof Nette\Forms\Control, + fn ($component): bool => $component instanceof Nette\ComponentModel\IComponent && $component instanceof Nette\Forms\Control, ); foreach ($controls as $control) { if (($name = $control->getName()) !== null) { From cd47834d1c06db1c2de49312a42606c6dce71c8d Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Mon, 13 Jul 2026 01:11:00 +0200 Subject: [PATCH 5/7] phpstan: Disable reporting stale suppressions component-model 4.0 seems to have fixed the issue by changing the return type hint to `array`: https://github.com/nette/component-model/commit/c40aa8ed45324438100a4d931ed1fb8dc16b4f34 --- phpstan-baseline.neon | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 13cd09b..fa84832 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,14 +1,26 @@ parameters: ignoreErrors: # https://github.com/phpstan/phpstan-nette/issues/141 + - + message: '#^Instanceof between Nette\\ComponentModel\\IComponent and Nette\\ComponentModel\\IComponent will always evaluate to true\.$#' + identifier: instanceof.alwaysTrue + # Only occurs with forms < 3.3.0 + reportUnmatched: false + count: 2 + path: src/Replicator/Container.php + - message: '#^Parameter \#1 \$array of function array_filter expects array, Iterator\ given\.$#' identifier: argument.type + # Only occurs with forms < 3.3.0 + reportUnmatched: false count: 3 path: src/Replicator/Container.php - message: '#^Parameter \#1 \$array of function array_filter expects array, Iterator\\|list\ given\.$#' identifier: argument.type + # Only occurs with forms < 3.3.0 + reportUnmatched: false count: 2 path: src/Replicator/Container.php From 5f4fd4a83e5775e4f0d50f9d2258cc9ac6425648 Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Mon, 13 Jul 2026 02:19:55 +0200 Subject: [PATCH 6/7] phpstan: Suppress `extensionMethod` parameter type mismatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We are passing a function that has extra arguments compared to the `callable` in `extensionMethod` parameter type. Since a function with extra arguments is a supertype of a function without them, not a subtype, it will be rejected as per contravariance of arguments rule. See https://github.com/nette/forms/pull/354 Let’s ignore it for now. --- phpstan-baseline.neon | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index fa84832..39d1707 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -24,3 +24,12 @@ parameters: reportUnmatched: false count: 2 path: src/Replicator/Container.php + + - + # https://github.com/nette/forms/pull/354 + message: '#^Parameter \#2 \$callback of static method Nette\\Forms\\Container\:\:extensionMethod\(\) expects callable\(static\)\: mixed, Closure\(Nette\\Forms\\Container, string, callable, int\=, bool\=\)\: static given\.$#' + identifier: argument.type + # Only occurs with forms ≥ 3.3.0 + reportUnmatched: false + count: 1 + path: src/Replicator/Container.php From 7c5a3dfa5ad80e89581447cda7c1db10cd5229ed Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Wed, 15 Jul 2026 00:39:19 +0200 Subject: [PATCH 7/7] Fix compatibility with component-model 4.0.0 After the dependency bump, the tests started to fail: Nette\InvalidStateException: Component 'users' is not attached to 'Nette\Forms\Form'. 1. tests/Replicator/ContainerTest.php:376 Tester\TestCase->run() 2. vendor/nette/tester/src/Framework/TestCase.php:53 Tester\TestCase->runTest() 3. vendor/nette/tester/src/Framework/TestCase.php:132 KdybyTests\Replicator\ContainerTest->testAddContainer() 4. tests/Replicator/ContainerTest.php:257 KdybyTests\Replicator\BaseForm->addDynamic() 5. tests/Replicator/ContainerTest.php:350 Nette\Forms\Container->offsetSet() 6. vendor/nette/component-model/src/ComponentModel/ArrayAccess.php:27 Nette\Forms\Container->addComponent() 7. vendor/nette/forms/src/Forms/Container.php:299 Nette\ComponentModel\Container->addComponent() 8. vendor/nette/component-model/src/ComponentModel/Container.php:76 Nette\ComponentModel\Component->setParent() 9. vendor/nette/component-model/src/ComponentModel/Component.php:181 Nette\ComponentModel\Component->refreshMonitors() 10. vendor/nette/component-model/src/ComponentModel/Component.php:230 Kdyby\Replicator\Container->attached() 11. src/Replicator/Container.php:105 Kdyby\Replicator\Container->loadHttpData() 12. src/Replicator/Container.php:221 Nette\Forms\Container->getForm() 13. vendor/nette/forms/src/Forms/Container.php:330 Nette\ComponentModel\Component->lookup() 14. vendor/nette/component-model/src/ComponentModel/Component.php:67 Bisected that to https://github.com/nette/component-model/commit/b4e82e1a3a7ca6f66ce766704aa2e148de277276 --- src/Replicator/Container.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Replicator/Container.php b/src/Replicator/Container.php index 13ab1e2..0b57bbf 100644 --- a/src/Replicator/Container.php +++ b/src/Replicator/Container.php @@ -66,8 +66,8 @@ class Container extends Nette\Forms\Container */ public function __construct(callable $factory, int $createDefault = 0, bool $forceDefault = FALSE) { - $this->monitor(Nette\Application\UI\Presenter::class, $this->componentAttached(...)); $this->monitor(Nette\Forms\Form::class, $this->componentAttached(...)); + $this->monitor(Nette\Application\UI\Presenter::class, $this->componentAttached(...)); try { $this->factoryCallback = Closure::fromCallable($factory);