From a4d238f1192efc2e934ac08910d52538e8f2c45f Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Mon, 13 Jul 2026 02:44:02 +0200 Subject: [PATCH] Allow passing methods with arguments to `extensionMethod` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since https://github.com/nette/forms/commit/01863f2c219235beacfdbf99234ff71e8b3cab6b introduced `callable(static): mixed` type hint for the `extensionMethod` parameter, it is no longer possible to pass functions with more than one argument to it. This is because argument of type `callable(static): mixed` only has a limited number of subtypes: functions that can be called with the `static` argument, i.e. those functions expecting any superclass of `static`. The functions can also narrow down as they want since the return type since `mixed` is supertype of every type. The functions cannot have any extra arguments since the container would not know what to pass to them. But having fewer arguments is fine since PHP will ignore any extra arguments passed to a function. Formally, the subtyping relation of callables must respect the variance rules: https://www.php.net/manual/en/language.oop5.variance.php If we want to allow wider range of subtypes, we must choose a suitably wide supertype in the type lattice. One option would be just using `mixed` or `callable` but those do not really guide user very well. We want to include at least the `static` argument since that is always passed by `__call`. One option would be using a _bottom_ type (`never`) as the parameter type since it is a subtype of any type – the function with `never` argument can never be called so substituting it with a function that accepts a supertype cannot break any callers, satisfying the contravariance rule. For the return type, we can use a _top_ type (`mixed`) since callers expecting mixed must be able to deal with narrower types so functions returning a subtype cannot break anything either, thus covariance of return type. The main limitation is that we can only have as many arguments as we specify in the type signature as mentioned above. I chose 9 extra arguments since that should be enough for most uses. https://phpstan.org/writing-php-code/phpdoc-types#bottom-type Though, since the function with `never` argument cannot be called because there are no values that can inhabit `never` type, and thus there is nothing to be passed to arguments, we need to cast down the arguments in `__call` to `never` types. This is a valid operation since `never` is a subtype of any type but it is a bit weird. https://phpstan.org/writing-php-code/phpdocs-basics#inline-%40var --- src/Forms/Container.php | 5 +++-- src/Forms/Controls/BaseControl.php | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Forms/Container.php b/src/Forms/Container.php index e7c3a9417..7fa47ab7c 100644 --- a/src/Forms/Container.php +++ b/src/Forms/Container.php @@ -36,7 +36,7 @@ class Container extends Nette\ComponentModel\Container implements \ArrayAccess public array $onValidate = []; protected ?ControlGroup $currentGroup = null; - /** @var array */ + /** @var array */ private static array $extMethods = []; private ?bool $validated = false; private ?string $mappedType = null; @@ -626,6 +626,7 @@ public function addContainer(string|int $name): self public function __call(string $name, array $args) { if (isset(self::$extMethods[$name])) { + /** @var never[] $args */ return (self::$extMethods[$name])($this, ...$args); } @@ -633,7 +634,7 @@ public function __call(string $name, array $args) } - /** @param callable(static): mixed $callback */ + /** @param callable(static, never, never, never, never, never, never, never, never, never): mixed $callback */ public static function extensionMethod(string $name, callable $callback): void { if (str_contains($name, '::')) { // back compatibility diff --git a/src/Forms/Controls/BaseControl.php b/src/Forms/Controls/BaseControl.php index fa616e509..61608f55a 100644 --- a/src/Forms/Controls/BaseControl.php +++ b/src/Forms/Controls/BaseControl.php @@ -50,7 +50,7 @@ abstract class BaseControl extends Nette\ComponentModel\Component implements Con /** @var bool|bool[] */ protected bool|array $disabled = false; - /** @var array> */ + /** @var array> */ private static array $extMethods = []; private string|Stringable|null $caption; @@ -566,6 +566,7 @@ public function __call(string $name, array $args) $class = static::class; do { if (isset(self::$extMethods[$name][$class])) { + /** @var never[] $args */ return (self::$extMethods[$name][$class])($this, ...$args); } @@ -576,7 +577,7 @@ public function __call(string $name, array $args) } - /** @param callable(static): mixed $callback */ + /** @param callable(static, never, never, never, never, never, never, never, never, never): mixed $callback */ public static function extensionMethod(string $name, callable $callback): void { if (str_contains($name, '::')) { // back compatibility