Given the following example, shouldn't Psalm complain about an invalid argument BaseballPlayer?
abstract class Player
{
}
final class FootballPlayer extends Player
{
}
final class BaseballPlayer extends Player
{
}
/**
* @template T of Player
*/
final class Team
{
/**
* @var array<T>
*/
private $players = [];
/**
* @param array<T> $players
*/
public function __construct(array $players)
{
$this->players = $players;
}
}
/**
* @var Team<FootballPlayer>
*/
$team = new Team(
[
new FootballPlayer,
new BaseballPlayer,
]
);
Psalm uses @var as a type override for the assignment expression here. It doesn't check type compatibility for assignments at all:
/** @var int $i */
$i = 1;
$i = "a string"; // it's fine, $i has no fixed type
Basically, assignment makes $team var to assume Team<FootballPlayer|BaseballPlayer> type, and then you're overriding it to be Team<FootballPlayer> with @var.
Thank you for the quick reply!
Is there currently a way to achieve something comparable? Because it feels like there should but I'm just missing something :-D.
No easy way (that wouldn't require changes to the actual code) that I know of. Function return type is checked for compatibility with the type of return value, so theoretically you could wrap it into a closure:
$team =
/** @return Team<FootballPlayer> */
(function() {
return new Team(
[
new FootballPlayer,
new BaseballPlayer,
]
);
})();
This is too ugly (not to mention performance hit) though.
@muglug What do you think about introducing annotation that would ensure expression type?
@basbl is there a problem if you omit the @var? Psalm should infer it, and warn you when passing to functions that expect Team<FootballPlayer>
No problem when I omit the @var annotation and indeed when passed to a function that expects Team<FootballPlayer> Psalm will infer the type correctly and complain when given a mixed version.
But when a function expects the generic version Team<Player> it will also complain.
$team = new Team(
[
new FootballPlayer,
new FootballPlayer,
]
);
/**
* @param Team<Player> $team
*/
function allTeamsAllowed(Team $team): Team
{
return $team;
}
allTeamsAllowed($team);
Output:
➜ vendor/bin/psalm src/
Scanning files...
Analyzing files...
ERROR: InvalidArgument - src/test.php:54:17 - Argument 1 of allTeamsAllowed expects Team<Player>, Team<FootballPlayer> provided
allTeamsAllowed($team
@weirdan 's suggestion would be a massive help and tide us over until PHP gets actual support for Generics. And allow better reuse of code without any more specific versions.
But when a function expects the generic version
Team<Player>it will also complain.
That's due to template invariance - you can fix that by changing @template T of Player to @template-covariant T of Player
So like a @psalm-check annotation?
Thanks @muglug ! @template-covariant was exactly what I was looking for! But instead of @template-covariant T of Player it should be @template-covariant T extends Player.
Something along the way of @psalm-check would be awesome!
Most helpful comment
That's due to template invariance - you can fix that by changing
@template T of Playerto@template-covariant T of PlayerSo like a
@psalm-checkannotation?