rfc:enum_allow_static_properties

PHP RFC: Allow static properties in enums

Introduction

Although enums are immutable objects, it is often useful to have functions or methods that operate on enum instances. In many cases, it would make sense to declare that functionality as static methods on the enum itself. In cases where static methods require shared state, it would be useful to allow storing those shared state in static properties. To ensure immutability of enum instances, it's only necessary to forbid instance properties, but all properties were forbidden in the initial functionality included with the enums RFC.

Proposal

Allow static properties to be declared on enums or to be inherited by using traits. Continue to forbid instance properties. Additionally, update error messages to mention that only instance properties are forbidden on enums.

Arguments for allowing static properties

There is no technical reason to forbid static properties

Instances of an enum will be immutable even if static properties are allowed - instance properties are what must be forbidden to ensure immutability.

This is useful in cases where shared state involving immutable instances is used, e.g.

  1. Memoization of expensive operations with no side effects (reading and parsing a large file from disk, cpu-intensive operations, service/db calls)
  2. Keeping track of which enum case reflects the current state of a state machine or system

E.g. for Memoization, it may be useful to keep the result of expensive operations with no side effects in a static properties in the same module. Typically, php static properties are used where many other languages may have module functions and state in a file separate from a class definition (e.g. due to autoloading and coding standards requiring one class per file).

// https://en.wikipedia.org/wiki/Sprite_(computer_graphics)
enum Sprite {
    case FROG;
    case LOG;
    case GRASS;
    case WATER;
    // etc.
 
    /** @var array<string, MyModule\ImageData> */
    private static array $cache = [];
 
    public static function getImageData(SpriteArt $value): MyModule\ImageData {
        $key = $value->name;
        if (!isset(self::$cache[$key])) {
            self::$cache[$key] = self::loadImageData($value);
        }
        return self::$cache[$key];
    }
 
    // Called when is no longer used, the color scheme changed, etc.
    public static function clearImageData(SpriteArt $value): void {
        self::$cache = [];
    }
 
    // Slow operation: Read image data from disk and decode image data.
    public static function loadImageData(): void {
        // ...
    }
}

For example, one way to represent fetching the current environment (of a known enumeration of environments) would be a static method on the environment enum itself. The environment instance is immutable, but the environment being loaded depends on a file.

enum Environment {
    case DEV;
    case STAGE;
    case PROD;
 
    private static Environment $currentEnvironment;
 
    /**
     * Read the current environment from a file on disk, once.
     * This will affect various parts of the application.
     */
    public static function current(): Environment {
        if (!isset(self::$currentEnvironment)) {
            $info = json_decode(file_get_contents(__DIR__ . '/../../config.json'), true);
            self::$currentEnvironment = match($info['env']) {
                'dev' => self::DEV,
                'stage' => self::STAGE,
                'prod' => self::PROD,
            };
        }
        return self::$currentEnvironment;
    }
    // Other methods can also access self::$currentEnvironment
}
printf("Current environment is %s\n", Environment::current()->name);

This is better than alternative ways that can be used to store shared state

While I expect that a majority of enum declarations won't need shared state at all, some will benefit from (or require) shared state in code involving instances of those enums or static methods of those enums, e.g. due to optimizations or unexpected business logic complications.

Compared to alternatives such as global variables and local static variables or static properties of placeholder classes, this is a useful option to have for the following reasons:

  1. Property types can be used and enforced at runtime (and checked by type checkers).
  2. Visibility is easier to enforce and read (compared to global variables or static properties declared in other classes).
  3. It is easier to reset static properties in unit tests (compared to static variables).
  4. This can result in more concise and easier to understand code.

This is also useful because it allows enums to use traits that contain static properties, which was previously a fatal error.

Projects may wish to enforce their own coding standards on how to appropriately use static properties in enums - e.g. I can imagine different projects may have different opinions, but having the functionality available to make use of would help their maintainers

  • One project may forbid publicly visible static properties.
  • Another project may allow uses of static properties, but only if the enum's methods would be idempotent or appear to the callers to be free of side effects. (e.g. to permit memoization)
  • Another project may forbid static properties except when inherited from a trait that has other non-enum use cases.

Quoting Rasmus:

PHP is and should remain:
1) a pragmatic web-focused language
2) a loosely typed language
3) a language which caters to the skill-levels and platforms of a wide range of users

This minimizes the backward compatibility impact of adding static properties to traits

From the perspective of an end user of a library, adding a property to a trait (that previously had no instance properties) would be a new type of backwards compatibility break because if an enum were to use that trait, it would become an unavoidable backwards compatibility break because using that trait would become a fatal error at compile time.

For example, consider this trait

// StdoutLogger 1.0
trait StdoutLogger {
    private static function log(string $message) {
        printf("%s: %s\n", date(DATE_RFC2822), $message);
    }
}

If a subsequent release of the library providing the trait were to add a static property, then enums using that trait would have an unavoidable fatal error at compile time.

// StdoutLogger 1.1
trait StdoutLogger {
    // each class directly using this trait has different storage for property values
    private static bool $loggingEnabled = true;
 
    // self within a trait refers to the class that directly uses a trait for methods inherited by a class
    private static function log(string $message): void {
        if (self::$loggingEnabled) { printf("%s: %s\n", date(DATE_RFC2822), $message); }
    }
 
    public static function setLoggingEnabled(bool $enabled): void {
        self::$loggingEnabled = $enabled;
    }
}

The backwards compatibility break of adding static properties to traits would be minimized by allowing enums to contain static properties.

(Instance properties in traits would continue to be an issue if used by enums)

This may help in moving existing code to PHP enums

In some cases, enums may be associated with shared functionality that uses shared state, and static properties may be the most practical way for a developer/team to migrate it.

The following are potential use cases:

  1. Migrating an application or library from other programming languages to a similar API in php, where files can have variables local to a module or if static variables are allowed. (For example, Java also allows static properties on enums whether or not they are final https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html)
  2. Migrating code using PHP classes to PHP enums (if those classes already depended on static properties in a way where refactoring was impractical).

Backward Incompatible Changes

None

Proposed PHP Version(s)

8.1

RFC Impact

To SAPIs

None

To Opcache

None

Unaffected PHP Functionality

Instance properties continue to be forbidden on enums.

PHP enums were already able to have instance and static methods.

Discussion

Rare use case but no technical reason to forbid static properties

From Ilija Tovilo, co-author of the enums RFC

https://github.com/php/php-src/pull/6997#issuecomment-842356465

I'm not super convinced by the use case (of enum Environment). Using static variables in enums would certainly not be great. Methods will behave differently in different environments even if the case is the same. This is certainly less than optimal.

On the other hand, I do think that needlessly restricting the language to babysit developers is not good unless there's a technical reason to do so, which doesn't seem to be the case here.

Strongly prefer other ways to manage shared state/globals

From Larry Garfield, co-author of the enums RFC

https://externals.io/message/114494#114497

Would you be able to provide more real life example? The example in RFC could easily encapsulate current Environment reading in for eg. EnvironmentConfiguration class with static property and method and TBH possibly that would be my preference to solve this.

Cheers, Michał Marcin Brzuchalski

I would agree. Static properties are ugly to begin with. They're globals with extra syntax. I have no desire to see them on enums.

Also a clarification, since it wasn't entirely clear in Tyson's original email: Static methods on Enums are already supported. They were included in the original Enum RFC. The change proposed here is just about static properties.

Counterarguments include:

  1. If both Environment and EnvironmentConfig were small and tightly coupled, a library/application author may not wish to be forced to create separate files.
  2. An application may start off by only using pure static methods to an enum. But it may later unexpectedly need to add shared state to new or existing methods after that design was already finalized, and continuing to implement the functionality in static methods on the enum instance would be the most consistent with previous design decisions.

Would prefer that enums are collections of pure(side effect free) methods

https://externals.io/message/114494#114506

Personally, I'd prefer to see enums as value objects only, adding static properties allow to implementation of statically conditional behaviour. IMO enums should consist only of pure functions. This is why I'd vote NO on this proposal.

Cheers, Michał Marcin Brzuchalski

  1. Some operations that have no side effects and satisfying different definitions of pure (reading and parsing a large unchanging file from disk, cpu or memory-intensive operations, read-only service/db calls) may benefit from Memoization, which would rely on being able to save or read shared state
  2. Some end users may disagree with this philosophy, while others may be reluctantly forced into maintaining shared state due to unexpected feature requests or business logic reasons that weren't expected in the initial design. If functionality related to enums did end up using shared state, then this is better than alternative ways that can be used to store shared state.

Vote

This is a Yes/No vote, requiring a 2/3 majority.

Voting started on June 1, 2021 and ends on June 15, 2021

Allow static properties in enums
Real name Yes No
alcaeus (alcaeus)  
alec (alec)  
ashnazg (ashnazg)  
bmajdak (bmajdak)  
brzuchal (brzuchal)  
bwoebi (bwoebi)  
crell (crell)  
cschneid (cschneid)  
derick (derick)  
didou (didou)  
galvao (galvao)  
jasny (jasny)  
jhdxr (jhdxr)  
kalle (kalle)  
kguest (kguest)  
lcobucci (lcobucci)  
mcmic (mcmic)  
nicolasgrekas (nicolasgrekas)  
ocramius (ocramius)  
pollita (pollita)  
stas (stas)  
svpernova09 (svpernova09)  
tandre (tandre)  
thekid (thekid)  
theodorejb (theodorejb)  
Final result: 8 17
This poll has been closed.

References

https://externals.io/message/112626#113037 brought up the same suggestion.

enums RFC

rfc/enum_allow_static_properties.txt · Last modified: 2021/06/16 03:29 by tandre