Finally, PHP got proper native enums. We can back them with strings or ints:
enum MyStringEnum: string
{
case MyCase = 'mycase';
}
enum MyIntEnum: int
{
case MyCase = 100;
}
Internally, any enum with values implements BackedEnum
interface. But there is no interface to distinguish string ones from integer ones. PHPStorm is using IntBackedEnum
and StringBackedEnum
internally, but that is not usable in your codebase. This becomes problematic in functions similar to this one:
/**
* @param BackedEnum[] $enums
* @return int[]|string[]
*/
public static function enumsToValues(array $enums): array
{
return array_map(fn(BackedEnum $enum) => $enum->value, $enums);
}
The return value int[]|string[]
is not precise. We know it depends on the input. How to solve that? In PHPStan, we could write custom DynamicMethodReturnTypeExtension
for this method, use ClassReflection::getBackedEnumType
and teach PHPStan when it returns int[]
and when string[]
. But there is a simpler way. Let’s add generics to BackedEnum at least for PHPStan! We can define BackedEnum.php.stub
exactly as it is in PHP, but add the generic template:
/** @template T of int|string */
interface BackedEnum
{
/** @var T */
public $value;
}
Register it in phpstan.neon.dist
:
stubFiles:
- ./stubs/BackedEnum.php.stub
And use it:
/**
* @implements BackedEnum<string>
*/
enum MyStringEnum: string
{
case MyCase = 'mycase';
}
Now PHPStan starts complaining about using @implements
without any interface used, let’s ignore that in phpstan.neon.dist
:
ignoreErrors:
- '#^Enum .*? has @implements tag, but does not implement any interface.$#'
Great, now we can improve our function to use generics and return proper value:
/**
* @param BackedEnum<T>[] $enums
* @return list<T>
*
* @template T of string|int
*/
public static function enumsToValues(array $enums): array
{
return array_map(fn(BackedEnum $enum) => $enum->value, $enums);
}
Ok, but this way, PHPStan is not checking that every child of generic BackedEnum needs to define the type of generic value. Meaning I can still define enum without defining its BackedEnum
generic type like this:
enum MyStringEnum: string
{
case MyCase = 'mycase';
}
So we need to ensure that nobody forgets about the @implements
tag. For that purpose, there is a ShipMonk’s custom PHPStan rule.