Skip to content

Conversation

@savinmikhail
Copy link

@savinmikhail savinmikhail commented Nov 5, 2025

Summary

Introduce a native BackedEnum::values() static method that returns the list of all backing values (int|string) of a backed enum’s cases in declaration order. This eliminates boilerplate commonly implemented across projects and aligns with the existing cases() API.

Target: master (PHP 8.6)

Motivation

The ecosystem repeatedly implements the same pattern to produce an array of enum values:

enum Status: string {
    case Draft = 'draft';
    case Published = 'published';

    // Boilerplate implemented in countless codebases today
    public static function values(): array {
        return array_map(fn($case) => $case->value, self::cases());
    }
}

This pattern appears widely across GitHub and frameworks, often implemented directly or via traits (which hides usage from code search). A conservative summary of real-world evidence:

Quantitative (code search + trait multiplier):

Pattern GitHub search Results
array_map(fn($case) => $case->value, self::cases()) click ~330
return array_map + self::cases() + ->value click ~1,900
return array_map + fn($case) => $case->value click ~324
function values() + return array_map + self::cases() click ~784
function toArray() + array_map + self::cases() + ->value click ~236
function getValues() + array_map + self::cases() click ~196
function values() + foreach + self::cases() + [] = $ + ->value click ~90
Total ~3,860

Trait pattern multiplier:

  • Many projects factor this into a trait (e.g., EnumValuesTrait) and then use it in dozens of enums, so direct search counts significantly understate usage.
  • PHP manual example shows trait approach

Qualitative (frameworks & libraries):

Providing a native values() method:

  • Removes boilerplate and fragmentation (different traits/implementations).
  • Improves discoverability and consistency (parallels cases() nicely).
  • Simplifies migrations from legacy enum libraries and existing project traits.

Proposal

Add the following method to the BackedEnum interface:

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

Semantics:

  • Available only for backed enums (int|string). Not available on unit enums.
  • Returns a 0-based, indexed array of the backing values in declaration order.
  • For int-backed enums, returns array<int>; for string-backed, array<string>.

Examples:

enum Priority: int { case Low = 1; case High = 10; }
Priority::values(); // [1, 10]

enum Color: string { case Red = 'red'; case Blue = 'blue'; }
Color::values(); // ['red', 'blue']

Implementation Details

Engine changes (Zend):

  • Add stub and arginfo
    • Zend/zend_enum.stub.php: add BackedEnum::values() signature.
    • Zend/zend_enum_arginfo.h: regenerated (committed) to include arginfo_class_BackedEnum_values.
  • Add interned string identifier
    • Zend/zend_string.h: add ZEND_STR_VALUES ("values").
  • Implement and register the method
    • Zend/zend_enum.c:
      • Implement zend_enum_values_func(), mirroring cases() but extracting the value property of each case object.
      • Register for backed enums in both the internal method table and zend_enum_register_funcs() (user classes).

Tests:

  • Reflection: ensure the method appears on backed enums
    • ext/reflection/tests/BackedEnum_values_reflection.phpt
    • Update toString expectations for backed enums:
      • ext/reflection/tests/ReflectionEnum_toString_backed_int.phpt
      • ext/reflection/tests/ReflectionEnum_toString_backed_string.phpt
  • Enum behavior tests
    • Zend/tests/enum/backed-values-int.phpt
    • Zend/tests/enum/backed-values-string.phpt
    • Zend/tests/enum/backed-values-empty.phpt
    • Zend/tests/enum/backed-values-order.phpt
    • Zend/tests/enum/backed-values-not-on-pure.phpt
    • Zend/tests/enum/backed-values-ignore-regular-consts.phpt

Documentation in-tree:

  • NEWS: announce the feature under Core.
  • UPGRADING: list under “New Features”.

Manual (php/doc-en) to be added in a separate PR:

  • BackedEnum interface page: values() signature, description, examples.
  • Enumerations guide: mention values() alongside cases()/from()/tryFrom().
  • Migration guide for 8.6: add an entry for BackedEnum::values().

Backward Compatibility

No BC break.

  • The engine registers the native BackedEnum::values() only if the enum does not already declare a userland static values() method.
  • If an enum (or a trait it uses) defines values(), that method is preserved and continues to work unchanged.
  • Reflection continues to show a values() method; either the native one or the user-defined one.
  • Unit enums remain unaffected; they do not expose values().

Compatibility notes:

  • Projects may optionally remove duplicate userland implementations and rely on the native method, but no migration is required.
  • Behavior of existing custom values() implementations is preserved; the engine does not override or redeclare them.

Thanks, @vudaltsov, for highlighting the risks—resolved by conditional registration.

Optional Cleanup

Projects that wish to standardize on the native API can remove their custom values() implementations and rely on BackedEnum::values(); behavior will remain the same.

Mitigations Considered

  • Conditional registration (chosen): skip registering the native method if userland already defines values(); preserves BC while providing a standard API where missing.
  • Different method name (e.g., getValues(), valueList()): avoids collision but diverges from established conventions and symmetry with cases().
  • Phased rollout (deprecation first): unnecessary given the conditional approach above.

Performance

  • O(n) over the number of cases, similar to:
array_map(fn($case) => $case->value, self::cases())
  • Native implementation avoids userland call overhead and enables future internal optimizations if needed.

Alternatives Considered

  • Keep status quo: leaves significant, repeated boilerplate across the ecosystem.
  • Traits / helpers in userland: remains fragmented and non-discoverable.
  • Different naming (toArray(), getValues()): ambiguous or verbose; values() best matches existing community conventions and parallels cases().

Prior Art

No previous RFCs proposing a BackedEnum::values() method were found.
A review of the PHP RFC index shows several enum-related proposals:

However, none of them address adding a convenience method returning the list of backing values.
This proposal introduces it for the first time.

Checklist

  • Engine implementation
  • Arginfo generated and committed
  • Tests added (enum + reflection)
  • NEWS and UPGRADING updated
  • Manual docs (php/doc-en) PR to be submitted after review

Thank you for reviewing!

@noelmaHelloCSE
Copy link

noelmaHelloCSE commented Nov 6, 2025

Great and thorough study — the addition of this method seems highly anticipated, and I believe it's underrated because one of the most common approaches uses array_column :)

@savinmikhail
Copy link
Author

I emailed the internals, now I'm waiting for the feedback

Zend/zend_enum.c Outdated

static ZEND_NAMED_FUNCTION(zend_enum_values_func)
{
zend_class_entry *ce = execute_data->func->common.scope;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
zend_class_entry *ce = execute_data->func->common.scope;
zend_class_entry *ce = EX(func)->common.scope;

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thx, fixed it

Zend/zend_enum.c Outdated
}
zval *prop = zend_enum_fetch_case_value(Z_OBJ_P(zv));
zval tmp;
ZVAL_COPY(&tmp, prop);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would likely be better to use Z_ADDREF_P(prop); instead of ZVAL_COPY. Then use prop instead of &tmp below.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thx, fixed it

@savinmikhail
Copy link
Author

Based on the feedback from the internals so far: consensus seems to be “the feature is useful, but the BC break is too large.” To address that, I’ve adjusted the proposal so that user code is allowed to redeclare values() on backed enums. This keeps existing projects working unchanged.

I recognize this makes values() the only intrinsic enum method that can be overridden, unlike cases()/from()/tryFrom(). If we proceed with this transitional approach, I’ll document the inconsistency, add tests to lock down the behavior, and ensure Reflection output is predictable.

If, over time, the community prefers to consolidate on a single intrinsic (no redeclaration), we can introduce a deprecation phase in a later minor and make redeclaration an error in the next major. For now, the goal is to deliver the utility of values() without forcing immediate code changes.

@savinmikhail
Copy link
Author

savinmikhail commented Nov 7, 2025

@vudaltsov suggested to use virtual property $values for that, I am not sure would it be better than the current implementation (with redeclaring values() method). Any thoughts?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants