Table of Contents

PHP RFC: Add get_declared_enums() function

Introduction

This RFC proposes to add a new get_declared_enums() function in the next PHP 8.x release and to make the behaviour of the related functions consistent by:

Proposal

There are four different OO structures in PHP:

For all four, a *_exists() function is available, i.e. class_exists(), interface_exists(), trait_exists() and enum_exists().

But only for three out of the four, a get_declared_*() function exists. There is get_declared_classes(), get_declared_interfaces(), get_declared_traits(), but no get_declared_enums().

This proposal intends to remedy this by adding a get_declared_enums() function in the next PHP 8.x release.

Furthermore, the behaviour of the get_declared_classes() and class_exists() functions is inconsistent, as get_declared_classes() currently also returns the names of enums and class_exists() returns true for both classes as well as enums.

To illustrate:

Function/Given Class Interface Trait Enum
class_exists() true/false false false true/false
interface_exists() false true/false false false
trait_exists() false false true/false false
enum_exists() false false false true/false

Demo: https://3v4l.org/1PWFo

Function/Returns Classes Interfaces Traits Enums
get_declared_classes() yes no no yes
get_declared_interfaces() no yes no no
get_declared_traits() no no yes no
get_declared_enums() (proposed) no no no yes

Demo: https://3v4l.org/0ub6I

This means that these functions do not act as the “silos” they appear to be, which can be a cause of bugs and other surprises.

To remedy this, a deprecation notice will be added when class_exists() is used with an enum name. As of PHP 9.0, class_exists() will no longer act on enum names and return false for those.

As for get_declared_classes(), the proposal is to soft deprecate the return value including enum names, and to change the return value to no longer include enum names as of PHP 9.0.

Discussion

Why was get_declared_enums() not added when enums were added in PHP 8.1 ?

According to Larry Garfield (one of the authors of the enumerations RFC):

I can't remember the last time I used get_declared_classes() [...], so when we were working on enums it never occurred to us to think about it. It wasn't a deliberate decision to omit, as far as I recall.

In other words: it appears to have been just an oversight. Not an intentional design choice.

Enums are a sub-set of classes, why should the behaviour of get_declared_classes() be changed ?

For the lack of a get_declared_enums() function, it is fortunate that get_declared_classes() includes enums in its return value, as otherwise there would be no way, at this time, to retrieve the list of enum symbols.

However, once the get_declared_enums() function would be introduced, this behaviour becomes inconsistent and surprising.

After all, internally, classes, interfaces, traits and enums are all considered classes, so why should get_declared_classes() include enums, but not interfaces and traits ?

Changing the return value of get_declared_classes(), however, constitutes a BC-break for the (limited) set of users consciously using get_declared_classes() to also retrieve the names of declared enum symbols. With that in mind, changing the return value of get_declared_classes() is proposed for PHP 9.0.

Enums are a sub-set of classes, why should the behaviour of class_exists() be changed ?

Along the same lines as the above: interface and traits are also classes internally, but calling class_exists() on an interface or trait name will return false, while calling it on an enum name will return true.

This is inconsistent and surprising behaviour which this RFC intends to fix.

Why only soft deprecate the change to the return value for get_declared_classes() ?

The function itself is not deprecated, but the return value will change to not include enums.

As, when the function is called, there is no way to know how the return value will be used, it seems inappropriate to throw a deprecation notice.

What does soft deprecate even mean in this context ?

A warning about the upcoming change in the return value of get_declared_classes() in both the UPGRADING document, the migration guide, and on the manual page for get_declared_classes().

Were any alternative approaches considered ?

Claude Pache brought up the following alternative approach on the mailing list:

const SIMPLE_CLASS = 1;
const ABSTRACT_CLASS = 2;
const ANONYMOUS_CLASS = 4;
const ENUM = 8;
 
function get_declared_classes(int $type = SIMPLE_CLASS | ABSTRACT_CLASS | ANONYMOUS_CLASS | ENUM): array { /* ... */ }

This approach could even be extrapolated to the following:

const SIMPLE_CLASSES = 1;
const ABSTRACT_CLASSES = 2;
const ANONYMOUS_CLASSES = 4;
const INTERFACES = 8;
const TRAITS = 16;
const ENUMS = 32;
 
function get_declared_symbols(?int $type = null, bool $categorize = false): array { /* ... */ }

This would then allow for deprecating - and eventually removing - the get_declared_classes(), get_declared_interfaces() and get_declared_traits() methods in favour of the unified get_declared_symbols() method.

Deprecating and removing the get_declared_classes(), get_declared_interfaces() and get_declared_traits() methods would, however, be more disruptive to userland than the current proposal, which is why the current proposal prevailed.

Backward Incompatible Changes

Introduction of the new function

The introduction of the new function means the name can no longer be used in userland code (without a function_exists() wrapper).

A GitHub code search yielded zero matches on public GitHub repositories that define a function with the same name.

If so desired, it is possible to polyfill the new get_declared_enums() function (props Christoph M. Becker and Nicolas Grekas):

function get_declared_enums(): array {
    $enums = [];
    $exts = get_loaded_extensions(false);
    foreach ($exts as $ext) {
        $re = new ReflectionExtension($ext);
        $classes = $re->getClasses();
        foreach ($classes as $class) {
            if ($class->isEnum()) {
                $enums[] = $class->name;
            }
        }
    }
    return $enums;
}
function get_declared_enums(): array {
     return array_filter(get_declared_classes(), 'enum_exists');
}

Deprecation of using class_exists() on enum names

The enum_exists() function has been included in PHP since PHP 8.1, when enums were introduced. Code specifically intended to check whether an enum exists, will (or should) be using the enum_exists() function already.

There may be some code, which intentionally uses class_exists() as a convenient catch-all to check for both classes as well as enums, such code could be adjusted per the below to be cross-version compatible (and avoid the deprecation notice):

-if (class_exists($name)) {}
+if (enum_exists($name) || class_exists($name)) {}

Change of the return value of get_declared_classes()

As of PHP 9.0, existing code which explicitly relies on get_declared_classes() to retrieve both class, as well as enum symbol names, will break.

If so desired, this can be worked around as follows in a cross-version compatible manner to maintain the old behaviour:

$classes = get_declared_classes();
if (function_exists('get_declared_enums')) {
    $enums   = get_declared_enums();
    $classes = array_values(array_unique(array_merge($classes, $enums)));
}

Alternatively, the new (PHP 9.0) behaviour could be emulated in a cross-version compatible manner, like so:

$classes = get_declared_classes();
$enums   = [];
if (function_exists('get_declared_enums')) {
    $enums = get_declared_enums();
}
 
if (PHP_VERSION_ID < 90000) {
    if (!empty($enums)) {
        // PHP 8.5 < 9.0: only remove enums from $classes.
        $classes = array_diff($classes, $enums);
    } elseif (function_exists('enum_exists')) {
        // PHP 8.1 - 8.4: remove enums from $classes and add to $enums.
        $classes = array_values(array_filter(
            $classes,
            function ($name) use (&$enums) {
                if (enum_exists($name, false)) {
                    $enums[] = $name;
                    return false;
                }
 
                return true;
            }
        ));
    }
}

Tools like PHPCompatibility, PHPStan and Exakat, could possibly/probably flag up code which would potentially be affected by the PHP 9.0 removal of enums from the return value of get_declared_classes().

Proposed PHP Version(s)

RFC Impact

To SAPIs

None

To Existing Extensions

None

To Opcache

None

New Constants

None

php.ini Defaults

None

Unaffected PHP Functionality

The behaviour of the get_declared_interfaces() and get_declared_traits() functions remains unchanged.
The behaviour of the interface_exists(), trait_exists() and enum_exists() functions remains unchanged.

The Reflection API for enums also remains unchanged.

Future Scope

Proposed Voting Choices

Each vote would need a 2/3 majority. Voting started on 2024-XX-XX YY:YY UTC and closed on 2024-XX-XX YY:YY UTC.

Add a new get_declared_enums() function in the next PHP 8.x
Real name Yes No
Final result: 0 0
This poll has been closed.


Deprecate using class_exists() for enums in PHP 8.x and change class_exists() to return false for enums in PHP 9.0
Real name Yes No
Final result: 0 0
This poll has been closed.


Remove enums from the return value of get_declared_classes() in PHP 9.0
Real name Yes No
Final result: 0 0
This poll has been closed.

* The third vote is conditional on the first vote passing.

Patches and Tests

Implementation

After the project is implemented, this section should contain

  1. the version(s) it was merged into
  2. a link to the git commit(s)
  3. a link to the PHP manual entry for the feature
  4. a link to the language specification section (if any)

References

Rejected Features

None at this time.