====== PHP RFC: Add get_declared_enums() function ====== * Version: 0.1 * Date: 2024-08-19 * Author: Juliette Reinders Folmer (), Ayesh Karunaratne () * Status: Under Discussion * Implementation: https://github.com/php/php-src/pull/15443 * First Published at: http://wiki.php.net/rfc/get_declared_enums ===== 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: * Deprecating support for enums in the class_exists() function in the next PHP 8.x and removing support for enum from class_exists() in PHP 9.0. * Soft deprecating retrieving enums via the get_declared_classes() function and to remove enums from the return value of get_declared_classes() in PHP 9.0. ===== Proposal ===== There are four different OO structures in PHP: * Classes * Interfaces * Traits * Enums 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 ? ==== [[https://externals.io/message/124937#124941|According to Larry Garfield]] (one of the authors of the [[enumerations|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 ? ==== [[https://externals.io/message/124937#125028|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 [[https://github.com/search?q=get_declared_enums+language%3APHP+&type=code|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 [[https://externals.io/message/124937#124973|Christoph M. Becker]] and [[https://externals.io/message/124937#124980|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 [[https://github.com/PHPCompatibility/PHPCompatibility|PHPCompatibility]], [[http://phpstan.org/|PHPStan]] and [[http://exakat.io/|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) ===== * Next PHP 8.x for introducing the ''get_declared_enums()'' function. * Next PHP 8.x for deprecating the use of class_exists() on enum symbols. * PHP 9.0 for the change to the return value of get_declared_classes() (to not include enums). * PHP 9.0 for the change to the return value of class_exists() (to return ''false'' for enums instead of ''true''). ===== 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 ===== * The pre-RFC discussion yielded some interesting ideas regarding potentially new functionality to retrieve the symbols loaded by a specific file ''include''/''require''. These are not addressed in this RFC and are left as a potential future language enhancement. * The pre-RFC discussion also highlighted that the ''get_declared_*()'' functions are not terribly memory efficient. Cutting down on memory fragmentation/garbage collector churn caused by these functions is outside the scope of the current proposal. ===== 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. * Yes * No \\ * Yes * No \\ * Yes * No * The third vote is conditional on the first vote passing. ===== Patches and Tests ===== * Patch introducing ''get_declared_enums()'' for 8.x: https://github.com/php/php-src/pull/15443 * [WIP] Patch to deprecate using class_exists() with enums: https://github.com/Ayesh/php-src/pull/14 * No patch for the changes proposed for PHP 9.0 is available at this time. ===== Implementation ===== After the project is implemented, this section should contain - the version(s) it was merged into - a link to the git commit(s) - a link to the PHP manual entry for the feature - a link to the language specification section (if any) ===== References ===== * [[enumerations|RFC which introduced enumurations]] * [[https://www.php.net/manual/en/ref.classobj.php|Class/Object functions in the manual]] * [[https://externals.io/message/124937|Initial pre-RFC discussion]] ===== Rejected Features ===== None at this time.