← All results

@template T of array — a bound that is not a class

Group: PHPDoc generics · Category: phpdoc · File: generics_template_bound_array.php

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

Analyzer results

AnalyzerVersionResultDiagnostics
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-rules
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 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 diff
Line 57: Expected 1 error(s)
intelephense 1.18.5intelephense 1.18.5 Fail

No diagnostics reported.

Expectation diff
Line 57: Expected 1 error(s)
phpy 0.2.0phpy 0.2.0 Fail

No diagnostics reported.

Expectation diff
Line 57: Expected 1 error(s)
qodana 262.8665.325Qodana 262.8665.325 Fail

No diagnostics reported.

Expectation diff
Line 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 diff
Line 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]

Source

<?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