[PHP-DEV] [RFC] [Discussion] Add values() Method to BackedEnum

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 ==

  1. Is the conditional approach technically sound?
  2. Is the API consistency trade-off acceptable given the zero BC breaks?
  3. Any concerns with the implementation approach?
  4. 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

пн, 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 ==

  1. Is the conditional approach technically sound?
  2. Is the API consistency trade-off acceptable given the zero BC breaks?
  3. Any concerns with the implementation approach?
  4. 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.

···

Best regards, Valentin

пн, 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 ==

  1. Is the conditional approach technically sound?
  2. Is the API consistency trade-off acceptable given the zero BC breaks?
  3. Any concerns with the implementation approach?
  4. 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

On Tue, Nov 11, 2025 at 10:29 AM Mikhail Savin <mikhail.d.savin@gmail.com> wrote:

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

Do you think you can quantify this BC break: how many implementations are having a different signature? And update the RFC “Impact on Ecosystem” section?

Small note:
implementations usually use array_column(self::cases(), 'value') instead of array_map(static fn(self $c) => $c->value, self::cases()).
and that’s just normal, as it is 2x faster: https://3v4l.org/Q5AYg#v8.4.14
maybe mention it as well, or instead of the one you had it the examples, as this is usually how it can be found in libraries.


Alex

On Tue, 11 Nov 2025, Mikhail Savin wrote:

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

Right now, enums can have a "values()" function that can return anything
I desire: an array of enum cases, or objects created from these, or
strings, or M_PI.

If it has to be compatible with

  `BackedEnum::values(): array(int|string)`

(which is what your contract demands, even though it can't express the
int|string part)

Then that is a BC break again.

cheers,
Derick

вт, 11 нояб. 2025 г. в 14:50, Derick Rethans <derick@php.net>:

On Tue, 11 Nov 2025, Mikhail Savin wrote:

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

Right now, enums can have a “values()” function that can return anything
I desire: an array of enum cases, or objects created from these, or
strings, or M_PI.

If it has to be compatible with

BackedEnum::values(): array(int|string)

(which is what your contract demands, even though it can’t express the
int|string part)

Then that is a BC break again.

cheers,
Derick

Hi, guys, thanks for feedback

Following the discussion with Alex and Derick, I’ve completed a comprehensive BC
analysis. I need community input on a key decision: whether to include a return
type in the interface declaration.

The Two Options

Option A: WITH return type (current PR)

interface BackedEnum {
public static function values(): array;
}
  • BC Breaks: 71-600 implementations (1.0-8.8%)
  • Better type safety and IDE support
  • Consistent with cases(): array

Option B: WITHOUT return type (alternative)

interface BackedEnum {
public static function values();
}
  • BC Breaks: 0 (0%)
  • All existing implementations compatible
  • Can add : array in PHP 9.0

BC Analysis Data

Total enums with values(): 6,800

  • Compatible (: array): 6,200 (91.2%)
  • Missing return type: 64 (0.9%)
  • Wrong return types: 7 (0.1%)
  • Unaccounted: ~529 (7.8%)

All GitHub search links: https://github.com/php/php-src/pull/20398

Question for Community

Which approach should we take for PHP 8.6?

Option A: Accept 1-9% BC break for full type safety
Option B: Zero BC breaks, add typing in PHP 9.0

I’m inclined toward Option B (zero breaks for 8.6), but want to hear community
preference before changing the implementation.

Thoughts?


Additional context:

  • Implementation change is trivial (one line)

  • Native implementation returns array regardless of interface

  • Alex’s array_column suggestion incorporated (+3.6k usages)

  • All data verifiable via GitHub searches in PR

Best regards, Mikhail

On Wed, 12 Nov 2025, 19:30 Mikhail Savin, <mikhail.d.savin@gmail.com> wrote:

Hi, guys, thanks for feedback

Following the discussion with Alex and Derick, I’ve completed a comprehensive BC
analysis. I need community input on a key decision: whether to include a return
type in the interface declaration.

The Two Options

Option A: WITH return type (current PR)

interface BackedEnum {
public static function values(): array;
}
  • BC Breaks: 71-600 implementations (1.0-8.8%)
  • Better type safety and IDE support
  • Consistent with cases(): array

Option B: WITHOUT return type (alternative)

interface BackedEnum {
public static function values();
}
  • BC Breaks: 0 (0%)
  • All existing implementations compatible
  • Can add : array in PHP 9.0

BC Analysis Data

Total enums with values(): 6,800

  • Compatible (: array): 6,200 (91.2%)
  • Missing return type: 64 (0.9%)
  • Wrong return types: 7 (0.1%)
  • Unaccounted: ~529 (7.8%)

All GitHub search links: https://github.com/php/php-src/pull/20398

Question for Community

Which approach should we take for PHP 8.6?

Option A: Accept 1-9% BC break for full type safety
Option B: Zero BC breaks, add typing in PHP 9.0

I’m inclined toward Option B (zero breaks for 8.6), but want to hear community
preference before changing the implementation.

Thoughts?


Additional context:

  • Implementation change is trivial (one line)

  • Native implementation returns array regardless of interface

  • Alex’s array_column suggestion incorporated (+3.6k usages)

  • All data verifiable via GitHub searches in PR

Best regards, Mikhail

Hi Mikhail,

I personally don’t see any added value by adding such a method to enums at this point when getting the equivalent results is already possible by a one liner.

array_column(EnumFqcn::cases(), 'value');

Kind regards,
Faizan

On Wed, Nov 12, 2025, at 5:35 AM, Mikhail Savin wrote:

Hi, guys, thanks for feedback

Following the discussion with Alex and Derick, I've completed a
comprehensive BC
analysis. I need community input on a key decision: whether to include
a return
type in the interface declaration.

## The Two Options

**Option A: WITH return type (current PR)**

interface BackedEnum {
    public static function values(): array;
}

- BC Breaks: 71-600 implementations (1.0-8.8%)
- Better type safety and IDE support
- Consistent with cases(): array

**Option B: WITHOUT return type (alternative)**

interface BackedEnum {
    public static function values();
}

- BC Breaks: 0 (0%)
- All existing implementations compatible
- Can add `: array` in PHP 9.0

## BC Analysis Data

Total enums with values(): 6,800
- Compatible (: array): 6,200 (91.2%)
- Missing return type: 64 (0.9%)
- Wrong return types: 7 (0.1%)
- Unaccounted: ~529 (7.8%)

All GitHub search links: Add values() Method to BackedEnum by savinmikhail · Pull Request #20398 · php/php-src · GitHub

## Question for Community

Which approach should we take for PHP 8.6?

**Option A:** Accept 1-9% BC break for full type safety
**Option B:** Zero BC breaks, add typing in PHP 9.0

I'm inclined toward Option B (zero breaks for 8.6), but want to hear community
preference before changing the implementation.

Thoughts?

---
Additional context:
- Implementation change is trivial (one line)
- Native implementation returns array regardless of interface
- Alex's array_column suggestion incorporated (+3.6k usages)
- All data verifiable via GitHub searches in PR

Isn't this what the #[ReturnTypeWillChange] attribute was intended for?

cf: PHP: rfc:internal_method_return_types

That seems like an option C? It's technically a BC break, but people can just drop an attribute on it to disable the return type check until 9.0. We did the same for the various internal methods, as noted in that RFC.

--Larry Garfield

On 12/11/2025 20:01, Larry Garfield wrote:

Isn't this what the #[ReturnTypeWillChange] attribute was intended for?

cf: PHP: rfc:internal_method_return_types

That seems like an option C? It's technically a BC break, but people can just drop an attribute on it to disable the return type check until 9.0. We did the same for the various internal methods, as noted in that RFC.

Parameter checks are not disabled by that attribute, so the BC problem remains.

ср, 12 нояб. 2025 г. в 22:26, Niels Dossche <dossche.niels@gmail.com>:

On 12/11/2025 20:01, Larry Garfield wrote:

Isn’t this what the #[ReturnTypeWillChange] attribute was intended for?

cf: https://wiki.php.net/rfc/internal_method_return_types

That seems like an option C? It’s technically a BC break, but people can just drop an attribute on it to disable the return type check until 9.0. We did the same for the various internal methods, as noted in that RFC.

Parameter checks are not disabled by that attribute, so the BC problem remains.

Hi everyone,

Based on your feedback, I’ve updated both the PR and the RFC by removing the return type declaration from the interface. Thanks for the input.

Hi

On 11/12/25 19:42, Faizan Akram Dar wrote:

I personally don't see any added value by adding such a method to enums at
this point when getting the equivalent results is already possible by a one
liner.

`array_column(EnumFqcn::cases(), 'value');`

I agree with that. I feel that BackedEnums are already over-used to the point where they just act as “fancy strings” instead of a proper data type on their own and I don't think we should further encourage treating enums as fancy strings.

In addition to that, a custom implementation of the method is trivially done as shown by Faizan and doesn't even need to be made available by the author of the enum but can be written on demand when having all values (without the corresponding names) is necessary.

And even when there is not a BC break right now, by not actually fixating the signature in the interface, it would be with PHP 9.0 and from then on it would (needlessly) block another method on enums that folks might otherwise be interested in using, since the name is very generic. Even when never adding it to the interface - static methods on interfaces are odd - it would break user expectations that some enum methods can be redefined and others can't.

Best regards
Tim Düsterhus