@template T of array — a bound that is not a classarray is a keyword, and a tool that treats everything
after of as a class name either invents a class called array or drops the
bound entirely.
References:
- PHPStan phpdocs-basics: @template bounds
- Psalm templated_annotations.md: template bounds| Analyzer | Version | Result | Diagnostics |
|---|---|---|---|
| phan | 6.0.7Phan 6.0.7 php-ast version 1.1.3 PHP version used to run Phan: 8.5.9 | Pass | generics_template_bound_array.php:57: Unused result of new object creation expression in new Collection(1) (this is likely free of side effects - there is no known non-empty constructor or destructor) [PhanNoopNewNoSideEffects] generics_template_bound_array.php:57: Template type T of \Conformance\Tests\GenericsTemplateBoundArray\Collection::__construct() must be compatible with array, but 1 was provided in call to \Conformance\Tests\GenericsTemplateBoundArray\Collection::__construct() [PhanTemplateTypeConstraintViolation] |
| phpstan | 2.2.8PHPStan - PHP Static Analysis Tool 2.2.8 | Pass reported Lv.4+ | generics_template_bound_array.php:57: Call to new Conformance\Tests\GenericsTemplateBoundArray\Collection() on a separate line has no effect. [identifier=new.resultUnused] [reported-from-level=4] generics_template_bound_array.php:57: Parameter #1 $items of class Conformance\Tests\GenericsTemplateBoundArray\Collection constructor expects T of array, int given. [identifier=argument.type] [reported-from-level=5] With strict-rulesgenerics_template_bound_array.php:57: Call to new Conformance\Tests\GenericsTemplateBoundArray\Collection() on a separate line has no effect. [identifier=new.resultUnused] generics_template_bound_array.php:57: Parameter #1 $items of class Conformance\Tests\GenericsTemplateBoundArray\Collection constructor expects T of array, int given. [identifier=argument.type] |
| psalm | 6.16.1Psalm 6.16.1@f1f5de594dc76faf8784e02d3dc4716c91c6f6ac next: 7.0.0-beta19 |
Pass | generics_template_bound_array.php:57: Argument 1 of Conformance\Tests\GenericsTemplateBoundArray\Collection::__construct expects array<array-key, mixed>, but 1 provided [InvalidArgument] |
| mago | 1.46.0mago 1.46.0 | Pass | generics_template_bound_array.php:57: Argument type mismatch for template `T`. [template-constraint-violation]
generics_template_bound_array.php:57: Invalid argument type for argument #1 of `Conformance\Tests\GenericsTemplateBoundArray\Collection::__construct`: expected `('T.conformance\tests\genericstemplateboundarray\collection extends array<array-key, mixed>)`, but found `int(1)`. [invalid-argument] |
| mir | 0.70.1mir 0.70.1 | Pass | generics_template_bound_array.php:57: InvalidTemplateParam: Template type 'T' inferred as '1' does not satisfy bound 'array' [MIR0900] |
| phpantom | 0.9.0phpantom_lsp 0.9.0 | Fail | No diagnostics reported. Expectation diffLine 57: Expected 1 error(s) |
| intelephense | 1.18.5intelephense 1.18.5 | Fail | No diagnostics reported. Expectation diffLine 57: Expected 1 error(s) |
| phpy | 0.2.0phpy 0.2.0 | Fail | No diagnostics reported. Expectation diffLine 57: Expected 1 error(s) |
| qodana | 262.8665.325Qodana 262.8665.325 | Fail | No diagnostics reported. Expectation diffLine 57: 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_template_bound_array.php:38: param $collection miss matched with phpdoc type <<Collection<array{id: int}>>> [funcParamTypeMissMatch]
generics_template_bound_array.php:49: param $row miss matched with phpdoc type <<array{id: int}>> [funcParamTypeMissMatch]
generics_template_bound_array.php:57: Expression evaluated but not used [discardExpr]Expectation diffLine 38: Unexpected errors ["param $collection miss matched with phpdoc type <<Collection<array{id: int}>>> [funcParamTypeMissMatch]"]
Line 49: Unexpected errors ["param $row miss matched with phpdoc type <<array{id: int}>> [funcParamTypeMissMatch]"] |
| steins | 0.1.5steins 0.1.5 (2026-08-12 revision 39b4cc1) | Pass | generics_template_bound_array.php:57: argument 1 to Collection::__construct() violates declared @param array $items — declared contract violation [phpdoc.param-mismatch] |
| phpstan-strict | 2.2.8PHPStan - PHP Static Analysis Tool 2.2.8 | Pass | generics_template_bound_array.php:57: Call to new Conformance\Tests\GenericsTemplateBoundArray\Collection() on a separate line has no effect. [identifier=new.resultUnused] generics_template_bound_array.php:57: Parameter #1 $items of class Conformance\Tests\GenericsTemplateBoundArray\Collection constructor expects T of array, int given. [identifier=argument.type] |
| psalm-next | 7.0.0-beta19Psalm 7.0.0-beta19@7e751c06a756fa64dc4c759c09fe4a173afcb433 | Pass | generics_template_bound_array.php:57: Argument 1 of Conformance\Tests\GenericsTemplateBoundArray\Collection::__construct expects array<array-key, mixed>, but 1 provided [InvalidArgument] |
<?php
declare(strict_types=1);
namespace Conformance\Tests\GenericsTemplateBoundArray;
/**
* `@template T of array` — a bound that is not a class
*
* The existing bound test uses an interface, which a tool can check by looking
* the name up in its class table. A native type in the same position cannot be
* resolved that way: `array` is a keyword, and a tool that treats everything
* after `of` as a class name either invents a class called `array` or drops the
* bound entirely.
*
* References:
* - PHPStan phpdocs-basics: @template bounds
* - Psalm templated_annotations.md: template bounds
*/
/**
* @template T of array
*/
final class Collection
{
/**
* @param T $items
*/
public function __construct(
public $items,
) {
}
}
/**
* @param Collection<array{id: int}> $collection
*/
function takesArrayCollection(Collection $collection): void
{
}
/**
* An array satisfies the bound. The row is passed in rather than written
* inline, so a tool that infers `array{id: 1}` from a literal does not reject
* the valid case for being too precise.
*
* @param array{id: int} $row
*/
function fillCollection(array $row): void
{
takesArrayCollection(new Collection($row));
}
// The constructor takes `T $items`, so with T bound to `array` the bound is
// what an argument has to satisfy. Blaming a call site keeps the expectation
// off the docblock/declaration pair the analyzers disagree about.
new Collection(1); // E: 1 is outside the `of array` bound on T