Bug report
A type from a new expression is a final-marked class reflection. That final-ness then leaks into any @template-covariant generic type that is inferred from it, and the resulting covariant subtype check fails against the same type, which is not final-marked.
Here an example:
<?php declare(strict_types = 1);
class Model {}
class ExtendedModel extends Model {}
/** @template-covariant T of Model */
class Component {}
/** @extends Component<ExtendedModel> */
class ExtendedComponent extends Component {}
/** @template M of Model */
class ComponentBuilder
{
/** @param M $model */
public function __construct(public readonly Model $model) {}
/** @param Component<M> $component */
public function accept(Component $component): void {}
}
function fromParameter(ExtendedModel $model): void
{
$builder = new ComponentBuilder($model);
$builder->accept(new ExtendedComponent()); // all good
}
function fromNew(): void
{
$model = new ExtendedModel();
$builder = new ComponentBuilder($model);
$builder->accept(new ExtendedComponent()); // Parameter #1 $component of method ComponentBuilder<ExtendedModel>::accept()
// expects Component<ExtendedModel>, ExtendedComponent given.
// 🪪 argument.type
}
The error message suggests, that the issue is the new ExtendedComponent() not beeing considered a Component<ExtendedModel> - however:
<?php
/** @var Component<ExtendedModel> $component */
$component = new ExtendedComponent();
$builder = new ComponentBuilder(new ExtendedModel());
$builder->accept($component); // Parameter #1 $component of method ComponentBuilder<ExtendedModel>::accept()
// expects Component<ExtendedModel>, Component<ExtendedModel> given.
// 🪪 argument.type
And this works:
<?php
/** @var ExtendedModel $model */
$model = new ExtendedModel();
$builder = new ComponentBuilder($model);
$builder->accept(new ExtendedComponent());
Other "fixes" include
final class ExtendedModel
M inferred from a natively typed parameter (like in fromParameter() above)
- explicit
/** @var ComponentBuilder<ExtendedModel> */
@template T of Model instead of @template-covariant
The new ExtendedModel() is typed via $classReflection->asFinal().
That is correct in itself - new X() is exactly X. But the final-marked ExtendedModel becomes the inferred value of M, so Component<M> resolves to Component<ExtendedModel-final>, while ExtendedComponent declares @extends Component<ExtendedModel> with a plain, non-final ExtendedModel. Under @template-covariant, PHPStan then asks whether ExtendedModel-final is a supertype of ExtendedModel, which it is not, because a non-final ExtendedModel value could be a subclass instance.
Code snippet that reproduces the problem
https://phpstan.org/r/839fffc8-aadb-4a78-ab15-e4e5e57fa374
Expected output
No errors. ExtendedComponent extends Component<ExtendedModel>, and $builder is ComponentBuilder<ExtendedModel> in both functions, so Component<M> is Component<ExtendedModel> in both cases.
Did PHPStan help you today? Did it make you happy in any way?
Absolutely! PHPStan is a wonderful piece of software - I can't imagine working with PHP without it.
I am very thankful for all the hard work you put into it
Bug report
A type from a
newexpression is a final-marked class reflection. That final-ness then leaks into any@template-covariantgeneric type that is inferred from it, and the resulting covariant subtype check fails against the same type, which is not final-marked.Here an example:
The error message suggests, that the issue is the
new ExtendedComponent()not beeing considered aComponent<ExtendedModel>- however:And this works:
Other "fixes" include
final class ExtendedModelMinferred from a natively typed parameter (like infromParameter()above)/** @var ComponentBuilder<ExtendedModel> */@template T of Modelinstead of@template-covariantThe
new ExtendedModel()is typed via$classReflection->asFinal().That is correct in itself -
new X()is exactlyX. But the final-markedExtendedModelbecomes the inferred value ofM, soComponent<M>resolves toComponent<ExtendedModel-final>, whileExtendedComponentdeclares@extends Component<ExtendedModel>with a plain, non-finalExtendedModel. Under@template-covariant, PHPStan then asks whetherExtendedModel-finalis a supertype ofExtendedModel, which it is not, because a non-finalExtendedModelvalue could be a subclass instance.Code snippet that reproduces the problem
https://phpstan.org/r/839fffc8-aadb-4a78-ab15-e4e5e57fa374
Expected output
No errors.
ExtendedComponent extends Component<ExtendedModel>, and$builderisComponentBuilder<ExtendedModel>in both functions, soComponent<M>isComponent<ExtendedModel>in both cases.Did PHPStan help you today? Did it make you happy in any way?
Absolutely! PHPStan is a wonderful piece of software - I can't imagine working with PHP without it.
I am very thankful for all the hard work you put into it