rfc:enum_allow_static_properties

This is an old revision of the document!


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 (use of disk, cpu-intensive, service/db calls
  2. Keeping track of which enum case reflects the current state of a state machine or system

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 shared state in code involving instances of those enums or static methods of those enums.

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.

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.

  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. Migrate 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.

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.

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.

Proposed Voting Choices

Yes/No, requiring a 2/3 majority

References

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

enums RFC

rfc/enum_allow_static_properties.1621296349.txt.gz · Last modified: 2021/05/18 00:05 by tandre