Table of Contents

PHP RFC: Add values() Method to BackedEnum

Introduction

This RFC proposes adding a native values() method to the BackedEnum interface that returns an indexed array of all backing values. The method is declared in the interface without a return type and the native implementation is added conditionally - only when the enum doesn't already define its own values() method, ensuring zero backward compatibility breaks.

<?php
 
enum Status: string {
    case Active = 'active';
    case Inactive = 'inactive';
    case Archived = 'archived';
}
 
// Automatically available - no manual implementation needed:
var_dump(Status::values());
// array(3) { [0]=> string(6) "active" [1]=> string(8) "inactive" [2]=> string(8) "archived" }
 
?>

Common use cases:

Proposal

Add a native values() static method to the BackedEnum interface that is conditionally registered based on whether the user has already defined it. The interface declares the method without a return type to maintain maximum backward compatibility:

<?php
 
interface BackedEnum extends UnitEnum
{
    /**
     * Returns an indexed array of all backing values for the enum cases.
     *
     * This method is automatically available unless the enum defines its own
     * values() method, in which case the user-defined implementation is used.
     *
     * @return int[]|string[]
     */
    public static function values();
}
 
?>

Key decisions:

Conditional Registration

The native implementation is only registered when the enum does not already define a values() method:

<?php
 
// Case 1: No user-defined values() - native implementation added automatically
enum Status: string {
    case Active = 'active';
    case Inactive = 'inactive';
}
 
Status::values(); // Native implementation: ['active', 'inactive']
 
// Case 2: User-defined values() - native implementation NOT added, user's version is used
enum Priority: int {
    case Low = 1;
    case High = 10;
 
    // Can use any return type since interface has no return type
    public static function values(): array {
        // Custom implementation - maybe sorted
        $values = array_map(fn($c) => $c->value, self::cases());
        sort($values);
        return $values;
    }
}
 
Priority::values(); // User's implementation: [1, 10] (sorted)
 
// Case 3: Different return type - also allowed (though semantically unusual)
enum Status: string {
    case Active = 'active';
 
    // No error - interface allows any return type
    public static function values(): string {
        return implode(',', array_map(fn($c) => $c->value, self::cases()));
    }
}
 
Status::values(); // "active,inactive,..."
 
?>

This approach ensures:

Behavior

When the native implementation is used:

Examples

Basic usage (native implementation):

<?php
 
enum Priority: int {
    case Low = 1;
    case Medium = 5;
    case High = 10;
}
 
var_dump(Priority::values());
// array(3) { [0]=> int(1) [1]=> int(5) [2]=> int(10) }
 
?>

Database migrations:

<?php
 
enum OrderStatus: string {
    case Pending = 'pending';
    case Processing = 'processing';
    case Completed = 'completed';
    case Cancelled = 'cancelled';
}
 
// Laravel migration
Schema::create('orders', function (Blueprint $table) {
    $table->enum('status', OrderStatus::values());
    // ['pending', 'processing', 'completed', 'cancelled']
});
 
?>

Form validation:

<?php
 
enum Country: string {
    case USA = 'us';
    case Canada = 'ca';
    case Mexico = 'mx';
}
 
// Symfony validation
use Symfony\Component\Validator\Constraints as Assert;
 
class Address {
    #[Assert\Choice(callback: [Country::class, 'values'])]
    public string $countryCode;
}
 
// Laravel validation
$validator = Validator::make($data, [
    'country' => ['required', 'in:' . implode(',', Country::values())]
]);
 
?>

API responses:

<?php
 
enum Feature: string {
    case BasicPlan = 'basic';
    case ProPlan = 'pro';
    case EnterprisePlan = 'enterprise';
}
 
// OpenAPI / JSON Schema
return response()->json([
    'available_plans' => Feature::values(),
    // ['basic', 'pro', 'enterprise']
]);
 
?>

User-defined implementation (respects custom behavior):

<?php
 
enum Color: string {
    case Red = 'red';
    case Green = 'green';
    case Blue = 'blue';
 
    // Custom implementation - returns uppercase values
    // Can use : array or any other return type (interface has no type requirement)
    public static function values(): array {
        return array_map(
            fn($c) => strtoupper($c->value),
            self::cases()
        );
    }
}
 
var_dump(Color::values());
// array(3) { [0]=> string(3) "RED" [1]=> string(5) "GREEN" [2]=> string(4) "BLUE" }
 
?>

Library compatibility example:

<?php
 
// Library code that needs to support PHP 8.4+
enum LibraryEnum: string {
    case Option1 = 'opt1';
    case Option2 = 'opt2';
 
    // Defined for PHP 8.4/8.5 compatibility
    // In PHP 8.6+, this is used instead of native (no conflict)
    public static function values(): array {
        return array_map(fn($c) => $c->value, self::cases());
    }
}
 
// Works in both PHP 8.4 and PHP 8.6+
var_dump(LibraryEnum::values());
// array(2) { [0]=> string(4) "opt1" [1]=> string(4) "opt2" }
 
?>

Trait-based implementations work unchanged:

<?php
 
trait EnumValues {
    public static function values(): array {
        return array_map(fn($c) => $c->value, self::cases());
    }
}
 
enum Status: string {
    use EnumValues; // User's trait takes precedence
 
    case Draft = 'draft';
    case Published = 'published';
}
 
// Uses trait implementation, native is not added
var_dump(Status::values());
 
?>

Empty enum edge case:

<?php
 
enum EmptyEnum: string {}
 
var_dump(EmptyEnum::values());
// array(0) { }
 
?>

Backward Incompatible Changes

This RFC introduces ZERO backward compatibility breaks.

Two complementary design decisions ensure complete backward compatibility:

1. No return type in interface:

2. Conditional registration:

Result: No migration required for any existing code.

How It Works

During enum registration, the engine checks if a values() method already exists:

// Simplified concept (actual implementation in zend_enum.c)
if (user_defined_values_exists(enum_class)) {
    // User has values() - respect their implementation
    return;
}
 
// No user-defined values() - register native implementation
register_native_values(enum_class);

This means:

Impact on Ecosystem

Positive impacts:

Optional cleanup opportunity: Over time, projects can optionally remove redundant values() implementations when they drop support for PHP < 8.6:

<?php
 
// PHP 8.4/8.5: Need custom implementation
enum Status: string {
    case Active = 'active';
 
    public static function values(): array {
        return array_map(fn($c) => $c->value, self::cases());
    }
}
 
// PHP 8.6+: Can remove (but not required)
enum Status: string {
    case Active = 'active';
    // Native values() available automatically
}
 
?>

Trade-off: API Consistency

This approach makes values() the only enum method that can be user-defined:

Method User-definable?
------------------------
cases() ❌ No (always native)
from() ❌ No (always native)
tryFrom() ❌ No (always native)
values() ✅ Yes (conditional)

Rationale for this trade-off:

Future path to full consistency: If desired, a future RFC could:

1. Emit ''E_DEPRECATED'' for user-defined ''values()'' in PHP 8.x
2. Make it non-overridable in PHP 9.0
3. Achieve full consistency with other enum methods

This RFC deliberately prioritizes pragmatic value delivery over perfect API consistency.

Proposed PHP Version(s)

Next PHP 8.x (PHP 8.6)

This is a feature addition with zero BC breaks, appropriate for a minor version.

RFC Impact

To the Ecosystem

Positive impacts:

No negative impacts:

To Existing Extensions

No impact to existing extensions. This is a core enum feature with no extension dependencies.

To SAPIs

No impact. This is a language-level feature that behaves identically across all SAPIs (CLI, FPM, embedded, etc.).

Open Issues

None. The conditional approach addresses the BC concerns raised during initial discussion.

Future Scope

Optional: Future Convergence on Mandatory values()

If the community later prefers full consistency (non-overridable values() like other enum methods):

Phase 1 (PHP 8.x): Emit E_DEPRECATED for user-defined values()

<?php
enum Status: string {
    case Active = 'active';
 
    public static function values(): array { ... }
    // Deprecated: Status::values() is provided natively and should not be redeclared
}
?>

Phase 2 (PHP 9.0): Make user-defined values() an error

This gives the ecosystem years to migrate while eventually achieving full API consistency.

This future scope is NOT part of the current RFC - just documenting the possibility.

Potential Enhancement: Array Keys

Future RFC could add optional parameter to control array keys:

<?php
// Hypothetical future enhancement (NOT part of this RFC)
Status::values(preserveKeys: true);  // ['Active' => 'active', 'Inactive' => 'inactive']
Status::values(preserveKeys: false); // ['active', 'inactive'] (default)
?>

This RFC deliberately keeps the API simple with indexed arrays matching cases() behavior.

Additional Helper Methods

Future RFCs could add related methods:

These are intentionally excluded from this RFC to keep scope focused.

Voting Choices

This is a simple yes/no vote requiring 2/3 majority as it's a language feature addition.

Vote will open 2 weeks after RFC announcement and remain open for 2 weeks.

Add BackedEnum::values() method with conditional registration as described in this RFC?
Real name Yes No
Final result: 0 0
This poll has been closed.

Patches and Tests

Pull Request: https://github.com/php/php-src/pull/20398

Implementation includes:

Key implementation detail:

The conditional check happens during enum registration:

// In zend_enum_register_funcs() - simplified
zend_function *existing = zend_hash_str_find_ptr(
    &ce->function_table, "values", sizeof("values")-1
);
 
if (existing && existing->common.scope == ce) {
    // User defined values() on this enum - respect it
    return;
}
 
// No user-defined values() - register native implementation
zend_internal_function *values_function = ...;
zend_enum_register_func(ce, ZEND_STR_VALUES, values_function);

All tests pass. Implementation is ready for merge pending RFC approval.

Implementation

After the RFC is approved, this section will contain:

References

Research and Evidence:

Search queries performed:

Prior Art:

Related RFCs:

Rejected Features

Interface WITH Return Type ('': array'')

An alternative implementation was considered where the interface would explicitly declare an : array return type:

<?php
 
interface BackedEnum extends UnitEnum
{
    public static function values(): array;  // ← WITH return type
}
 
?>

Advantages:

Why rejected:

Comprehensive GitHub search analysis found 6,800 existing values() implementations:

Category Count %
Compatible: : array return type 6,200 91.2%
Missing return type 64 0.9% ❌
Incompatible: other types (: string, : Iterator, etc.) 7 0.1% ❌
Unaccounted ~529 ~7.8% ❓
Total potential BC breaks 71-600 1.0-8.8%

Example breaking code:

<?php
 
enum Status: string {
    case Active = 'active';
 
    public static function values() {  // ❌ Missing : array
        return array_column(self::cases(), 'value');
    }
}
// Fatal error: Declaration of Status::values() must be compatible
// with BackedEnum::values(): array
 
?>

While 91.2% of implementations already have the correct signature, breaking even 1-9% of the ecosystem (71-600 codebases) was deemed unacceptable for a convenience feature.

Solution chosen: Omit return type from interface. This allows all existing implementations to remain compatible while the native implementation still returns a proper array.

Future consideration: Stricter typing with : array could be reconsidered for PHP 9.0 (major version where BC breaks are more acceptable), with a deprecation period in PHP 8.x.

Mandatory Native Implementation (Breaking Change)

Initially considered making values() always native and non-overridable (like cases()/from()/tryFrom()).

Rejected because:

Conditional approach chosen instead: Provides benefit without breaking changes.

Alternative Method Names

getValues(): More verbose, doesn't match cases() style toArray(): Ambiguous - case objects or values? Names or values? valueList(): Unnecessarily verbose extractValues(): Too long, unclear

Decision: values() best matches:

Virtual/Magic Properties

Suggestion to use Status::$values instead of Status::values().

Rejected because:

User-land Trait in Standard Library

Suggestion to provide standard library trait instead of native method.

Rejected because:

array_column() Alternative

Suggestion that array_column(Status::cases(), 'value') is sufficient.

Rejected because:

Different Signature

Considered allowing customization: values(sorted: true) or values(unique: true).

Rejected because:

<?php
// Users can customize as needed
$sorted = Status::values();
sort($sorted);
 
$unique = array_unique(Status::values());
?>

Deprecation Period for User-Defined values()

Considered emitting E_DEPRECATED for user-defined values() immediately.

Rejected because:

Changelog