Observed under v0.10.3
/**
* @template T of PBase
* @template U of PBase
*/
interface Q10BoxInterface {}
/**
* @template TSelf of Q10eAbstract
* @template T of PBase
* @template U of PBase
* @implements Q10BoxInterface<T,U>
*/
abstract class Q10eAbstract implements Q10BoxInterface {}
/** @extends Q10eAbstract<Q10eConcrete,PA,PB> */
final class Q10eConcrete extends Q10eAbstract {}
final class Q10dGenericCreateShape
{
/**
* @template T of PBase
* @template U of PBase
* @param Q10BoxInterface<T,U> $dto
*/
public function __construct(protected readonly Q10BoxInterface $dto) {}
}
new Q10dGenericCreateShape(new Q10eConcrete());
Actual:
Q10dGenericCreateShape::__construct(): Argument $dto expects Q10BoxInterface<invariant PBase>, but Q10BoxInterface<PA> was given
The given type is resolved with a single type argument (<PA>, U dropped) and the method template falls back to its bound, checked invariantly. The trigger is the extra leading TSelf template on the abstract class: the same chain without it (variant f, @extends Q10fAbstract<PA,PB>) passes, a direct @implements Q10BoxInterface<PA,PB> on the argument's class (variant d) passes, and the chained argument against a non-templated @param Q10BoxInterface<PA,PB> (variant g) passes.
Expected: T = PA, U = PB inferred from the argument, as PHPStan resolves the @extends → @implements chain; the call is accepted.
Observed under v0.10.3
Actual:
The given type is resolved with a single type argument (
<PA>,Udropped) and the method template falls back to its bound, checked invariantly. The trigger is the extra leadingTSelftemplate on the abstract class: the same chain without it (variant f,@extends Q10fAbstract<PA,PB>) passes, a direct@implements Q10BoxInterface<PA,PB>on the argument's class (variant d) passes, and the chained argument against a non-templated@param Q10BoxInterface<PA,PB>(variant g) passes.Expected:
T = PA,U = PBinferred from the argument, as PHPStan resolves the@extends→@implementschain; the call is accepted.