Collection<User>::map(fn ($u) => new Order()) is a
Collection<Order>.
References:
- PHPStan generics by example (callable template binding)
- Psalm templated callables| Analyzer | Version | Result | Diagnostics |
|---|---|---|---|
| phan | 6.0.10Phan 6.0.10 php-ast version 1.1.3 PHP version used to run Phan: 8.5.11 | Fail | generics_closure_param_from_method_template.php:90: Call to undeclared method \Conformance\Tests\GenericsClosureParamFromMethodTemplate\Order::name [PhanUndeclaredMethod] Expectation diffLine 80: Expected 1 error(s) |
| phpstan | 2.3.1PHPStan - PHP Static Analysis Tool 2.3.1 | Pass reported Lv.2+ | generics_closure_param_from_method_template.php:80: Call to an undefined method Conformance\Tests\GenericsClosureParamFromMethodTemplate\User::missing(). [reported-from-level=2] generics_closure_param_from_method_template.php:80: Parameter #1 (mixed) of echo cannot be converted to string. [reported-from-level=10] generics_closure_param_from_method_template.php:90: Call to an undefined method Conformance\Tests\GenericsClosureParamFromMethodTemplate\Order::name(). [reported-from-level=2] generics_closure_param_from_method_template.php:90: Parameter #1 (mixed) of echo cannot be converted to string. [reported-from-level=10] With strict-rulesgenerics_closure_param_from_method_template.php:80: Call to an undefined method Conformance\Tests\GenericsClosureParamFromMethodTemplate\User::missing(). generics_closure_param_from_method_template.php:80: Parameter #1 (mixed) of echo cannot be converted to string. generics_closure_param_from_method_template.php:90: Call to an undefined method Conformance\Tests\GenericsClosureParamFromMethodTemplate\Order::name(). generics_closure_param_from_method_template.php:90: Parameter #1 (mixed) of echo cannot be converted to string. |
| psalm | 6.20.0Psalm 6.20.0@804f03c0c183d149fcd4b8c7c0c9b3fbf836d34c next: 7.0.0-rc2 |
Pass | generics_closure_param_from_method_template.php:80: Argument 1 of echo cannot be mixed, expecting string [MixedArgument] generics_closure_param_from_method_template.php:80: Method Conformance\Tests\GenericsClosureParamFromMethodTemplate\User::missing does not exist [UndefinedMethod] generics_closure_param_from_method_template.php:90: Argument 1 of echo cannot be mixed, expecting string [MixedArgument] generics_closure_param_from_method_template.php:90: Method Conformance\Tests\GenericsClosureParamFromMethodTemplate\Order::name does not exist [UndefinedMethod] |
| mago | 1.57.0mago 1.57.0 | Pass | generics_closure_param_from_method_template.php:80: Method `missing` does not exist on type `Conformance\Tests\GenericsClosureParamFromMethodTemplate\User`. [non-existent-method] generics_closure_param_from_method_template.php:80: The first value for `echo` is too general. [mixed-argument] generics_closure_param_from_method_template.php:90: Method `name` does not exist on type `Conformance\Tests\GenericsClosureParamFromMethodTemplate\Order`. [non-existent-method] generics_closure_param_from_method_template.php:90: The first value for `echo` is too general. [mixed-argument] |
| mir | 0.87.0mir 0.87.0 | Pass | generics_closure_param_from_method_template.php:80: UndefinedMethod: Method Conformance\Tests\GenericsClosureParamFromMethodTemplate\User::missing() does not exist [MIR0004] generics_closure_param_from_method_template.php:90: UndefinedMethod: Method Conformance\Tests\GenericsClosureParamFromMethodTemplate\Order::name() does not exist [MIR0004] |
| phpantom | 0.11.1phpantom_lsp 0.11.1 | Pass | generics_closure_param_from_method_template.php:80: Method 'missing' not found on class 'Conformance\Tests\GenericsClosureParamFromMethodTemplate\User' [unknown_member] generics_closure_param_from_method_template.php:90: Method 'name' not found on class 'Conformance\Tests\GenericsClosureParamFromMethodTemplate\Order' [unknown_member] |
| intelephense | 1.18.5intelephense 1.18.5 | Pass | generics_closure_param_from_method_template.php:80: Expected type 'string|Stringable|null'. Found 'mixed'. [P1006] generics_closure_param_from_method_template.php:80: Undefined method 'missing'. [P1013] generics_closure_param_from_method_template.php:90: Expected type 'string|Stringable|null'. Found 'mixed'. [P1006] generics_closure_param_from_method_template.php:90: Undefined method 'name'. [P1013] |
| phpactor | 2026.10.07.0Phpactor 2026.10.07.0 | Fail | No diagnostics reported. Expectation diffLine 80: Expected 1 error(s) Line 90: Expected 1 error(s) |
| phpy | 1.0.19409phpy 1.0.19409 | Pass | generics_closure_param_from_method_template.php:80: Call to unknown method: Conformance\Tests\GenericsClosureParamFromMethodTemplate\User::missing() generics_closure_param_from_method_template.php:90: Call to unknown method: Conformance\Tests\GenericsClosureParamFromMethodTemplate\Order::name() |
| qodana | 262.10968.76PhpStorm 262.10968.76 | Fail | No diagnostics reported. Expectation diffLine 80: Expected 1 error(s) Line 90: Expected 1 error(s) |
| noverify | 0.5.5NoVerify, version 0.5.5: built on: 2025.04.22 14:36:46 OS: r-kuchinskas arm64 Commit: 4774b82f8adc53fbef38f95e97af4b953b4d1529 | Fail | generics_closure_param_from_method_template.php:44: param $items miss matched with phpdoc type <<list<T>>> [funcParamTypeMissMatch]
generics_closure_param_from_method_template.php:52: potentially not safe call in function Closure$(T):void signature of param closure param #0 [notSafeCall]
generics_closure_param_from_method_template.php:74: param $users miss matched with phpdoc type <<Collection<User>>> [funcParamTypeMissMatch]
generics_closure_param_from_method_template.php:85: param $users miss matched with phpdoc type <<Collection<User>>> [funcParamTypeMissMatch]
generics_closure_param_from_method_template.php:88: Call to undefined method {\Conformance\Tests\GenericsClosureParamFromMethodTemplate\T|mixed}->total() [undefinedMethod]
generics_closure_param_from_method_template.php:90: Call to undefined method {\Conformance\Tests\GenericsClosureParamFromMethodTemplate\T|mixed}->name() [undefinedMethod]Expectation diffLine 80: Expected 1 error(s)
Line 88: Expected valid value to be accepted, got ["Call to undefined method {\\Conformance\\Tests\\GenericsClosureParamFromMethodTemplate\\T|mixed}->total() [undefinedMethod]"]
Line 44: Unexpected errors ["param $items miss matched with phpdoc type <<list<T>>> [funcParamTypeMissMatch]"]
Line 52: Unexpected errors ["potentially not safe call in function Closure$(T):void signature of param closure param #0 [notSafeCall]"]
Line 74: Unexpected errors ["param $users miss matched with phpdoc type <<Collection<User>>> [funcParamTypeMissMatch]"]
Line 85: Unexpected errors ["param $users miss matched with phpdoc type <<Collection<User>>> [funcParamTypeMissMatch]"] |
| steins | 0.1.8steins 0.1.8 (2026-09-21 revision 76306a9) | Fail | No diagnostics reported. Expectation diffLine 80: Expected 1 error(s) Line 90: Expected 1 error(s) |
| phpstan-strict | 2.3.1PHPStan - PHP Static Analysis Tool 2.3.1 | Pass | generics_closure_param_from_method_template.php:80: Call to an undefined method Conformance\Tests\GenericsClosureParamFromMethodTemplate\User::missing(). generics_closure_param_from_method_template.php:80: Parameter #1 (mixed) of echo cannot be converted to string. generics_closure_param_from_method_template.php:90: Call to an undefined method Conformance\Tests\GenericsClosureParamFromMethodTemplate\Order::name(). generics_closure_param_from_method_template.php:90: Parameter #1 (mixed) of echo cannot be converted to string. |
| psalm-next | 7.0.0-rc2Psalm 7.0.0-rc2@20339e3fe5b8d03bcd8061030eb2a83ad50e5783 | Pass | generics_closure_param_from_method_template.php:80: Argument 1 of echo cannot be mixed, expecting string [MixedArgument] generics_closure_param_from_method_template.php:80: Method Conformance\Tests\GenericsClosureParamFromMethodTemplate\User::missing does not exist [UndefinedMethod] generics_closure_param_from_method_template.php:90: Argument 1 of echo cannot be mixed, expecting string [MixedArgument] generics_closure_param_from_method_template.php:90: Method Conformance\Tests\GenericsClosureParamFromMethodTemplate\Order::name does not exist [UndefinedMethod] |
<?php
declare(strict_types=1);
namespace Conformance\Tests\GenericsClosureParamFromMethodTemplate;
/**
* Closure parameter and return bound through method templates.
*
* A closure passed to a method of a generic class takes its parameter type
* from the class template, and a method-level template is bound from the
* closure's return: `Collection<User>::map(fn ($u) => new Order())` is a
* `Collection<Order>`.
*
* References:
* - PHPStan generics by example (callable template binding)
* - Psalm templated callables
*/
final class User
{
public function name(): string
{
return 'user';
}
}
final class Order
{
public function __construct(public string $customer)
{
}
public function total(): int
{
return 1;
}
}
/** @template T */
final class Collection
{
/** @param list<T> $items */
public function __construct(private array $items)
{
}
/** @param callable(T): void $callback */
public function each(callable $callback): void
{
foreach ($this->items as $item) {
$callback($item);
}
}
/**
* @template U
* @param callable(T): U $callback
* @return Collection<U>
*/
public function map(callable $callback): Collection
{
return new Collection(array_map($callback, $this->items));
}
/** @return T */
public function first(): mixed
{
return $this->items[0];
}
}
/** @param Collection<User> $users */
function closureParameter(Collection $users): void
{
$users->each(function ($user): void {
echo $user->name(); // V
});
$users->each(function ($user): void {
echo $user->missing(); // E: the closure parameter is a User
});
}
/** @param Collection<User> $users */
function closureReturn(Collection $users): void
{
$orders = $users->map(fn ($user) => new Order($user->name()));
echo $orders->first()->total(); // V
$mapped = $users->map(fn ($user) => new Order($user->name()));
echo $mapped->first()->name(); // E: U is bound to Order, not User
}