пн, 10 нояб. 2025 г. в 23:55, Valentin Udaltsov <udaltsov.valentin@gmail.com>:
пн, 10 нояб. 2025 г. в 08:44, Mikhail Savin <mikhail.d.savin@gmail.com>:
Hi internals,
I’ve created an RFC to add a native values() method to BackedEnum:
https://wiki.php.net/rfc/add_values_method_to_backed_enum
== Summary ==
The RFC proposes adding BackedEnum::values() that returns an indexed array
of all backing values. The implementation uses conditional registration -
the native method is only added when the enum doesn’t already define
values(), ensuring ZERO backward compatibility breaks.
Key points:
- Native values() added automatically to new enums
- Existing enums with custom values() continue working unchanged
- Trait-based implementations are respected
- Libraries can maintain their implementation for older PHP versions
- Solves boilerplate problem (3,860+ implementations, ~24k-44k real usage)
Common use cases:
- Database migrations: $table->enum(‘status’, Status::values())
- Form validation: in_array($input, Status::values())
- API responses: [‘allowed_values’ => Status::values()]
== Implementation ==
Working implementation with conditional registration:
https://github.com/php/php-src/pull/20398
The engine checks if values() exists before registering the native version:
- User-defined values() present? Use it.
- No user-defined values()? Add native implementation.
== No BC Breaks ==
This approach ensures:
- Existing code works unchanged (no migration needed)
- Immediate benefit for new code (automatic values())
- Gradual adoption possible (libraries can migrate at their pace)
Trade-off: Makes values() the only overridable enum method (unlike
cases/from/tryFrom). The RFC documents this as a pragmatic choice -
solving real problems without forced migrations.
== Questions ==
- Is the conditional approach technically sound?
- Is the API consistency trade-off acceptable given the zero BC breaks?
- Any concerns with the implementation approach?
- Should I implement some steps from “Future scope” of the rfc now?
Discussion period: 2 weeks minimum before voting.
Looking forward to your feedback!
Best regards,
Savin Mikhail
Hi, Mikhail!
Thank you for the RFC.
Consider this code if this RFC is accepted:
enum EnumWithUserDefinedValuesMethod: string
{
case X = 'x';
public static function values(): string
{
return 'values';
}
}
function getBackedEnumValues(BackedEnum $enum): array
{
return $enum::values();
}
getBackedEnumValues(EnumWithUserDefinedValuesMethod::X);
This code will suddenly break, because inside getBackedEnumValues I can safely assume that BackedEnum::values() returns an array, since it’s a part of the interface contract.
However, EnumWithUserDefinedValuesMethod breaks the Liskov Substitution Principle by defining a method with a non-compatible return type and gives a runtime error.
What you’ve basically suggested is to ignore the LSP. This is not a good idea.
–
Thanks for raising the LSP concern. You’re right that today a userland enum can define a values() with an arbitrary signature, and a helper like:
function getBackedEnumValues(BackedEnum $enum): array {
return $enum::values();
}
could blow up at runtime if that user method returns a non-array.
The RFC addresses this by adding “public static function values(): array” to the BackedEnum interface itself.
With that in place, an enum that defines an incompatible values() will fail at compile time with a normal method-signature incompatibility, exactly like it would for from()/tryFrom().
Here’s a .phpt demonstrating the intended behavior:
–TEST–
Backed enums: user-defined values() incompatible with interface signature
–FILE–
<?php
enum E: string {
case A = 'a';
// Intentional incompatibility: interface requires array return type
public static function values(): string {
return 'values';
}
}
?>
–EXPECTF–
Fatal error: Declaration of E::values(): string must be compatible with BackedEnum::values(): array in %s on line %d
Run with:
TEST_PHP_EXECUTABLE=sapi/cli/php sapi/cli/php -n run-tests.php -q Zend/tests/enum/backed-values-user-defined-incompatible.phpt
So LSP isn’t being ignored; it’s enforced by the interface method.
That said, adding values() to BackedEnum is a source-level BC break for codebases that already define conflicting values() on backed enums.
But, as we can see from GitHub searches that I posted in PR, most of the implementations is exactly the same, so BC break is very small
···
Best regards, Valentin