Requests for comment/Typesafe enums

From mediawiki.org
Request for comment (RFC)
Typesafe enums
Component General
Creation date
Author(s) Andrew Green
Document status in discussion

Implemented and merged on a WIP branch of the Campaigns extension for the Editor campaigns project; now benchmarking and adding requested features

This RFC proposes a mechanism for creating typesafe enums. An implementation with tests and examples is provided.

Problem statement[edit]

By using enums, you can improve code in several ways. Many languages have native enum constructs, but PHP doesn't. In MediaWiki, class constants are usually used instead. However, PHP developers often roll enum implementations that are as good or nearly as good as native constructs in other languages.

Adding enum facilities to core would provide MediaWiki code the benefits of fully featured enums, including type safety, information hiding, meaningful conversion to strings, helper functions on sets of enum values, and extensibility.

Options[edit]

Options include:

  • Do nothing. There's nothing inherently wrong with class constants.
  • Use PHP's experimental SplEnum class. Disadvantages: this class is unintuitive to use, since enum consumers must instantiate enum values. Also, it's needlessly crufty—enum definitions still assign integers to each enum!
  • Use an existing implementation. This is a possibility, though there doesn't seem to be any agreed-upon standard.
  • Build something better ourselves. Hopefully the proposed implementation, described below, fits the bill.

Implementation[edit]

Current functionality[edit]

With the proposed implementation, you can define some enums like this:

class DayOfTheWeek extends TypesafeEnum {

    static $MONDAY;
    static $TUESDAY;
    static $WEDNESDAY;
    static $THURSDAY;
    static $FRIDAY;
    static $SATURDAY;
    static $SUNDAY;
}

DayOfTheWeek::setUp();


The call to setUp() can go in the same .php file as the enum definition. It assigns unique values to the static properties and sets up an index for helper functions. It must be performed before the enum is used.

Once you've defined your enums and called setUp(), here's what you can do:

// Type hint!
function todayIs( DayOfTheWeek $day ) {

    // Default __toString() just outputs the property name.
    return 'Today is ' . $day . '.';
}

// Prints 'Today is TUESDAY.'
print( todayIs( DayOfTheWeek::$TUESDAY ) );

function printSomething( DayOfTheWeek $day ) {

    // Switch works as expected
    switch ( $day ) {
        case DayOfTheWeek::$MONDAY:
            print 'It\'s a beautiful day in the neighborhood!';
            break;
 
        case DayOfTheWeek::$TUESDAY:
            print 'Ruby...';
            break;

        default:
            print 'Ho hum.';
    } 
}

// Prints 'MONDAY'
print( DayOfTheWeek::$MONDAY->getName() );

// Prints 'DayOfTheWeek::$SATURDAY'
// If the enum is defined with a namespace, that will be prepended
print( DayOfTheWeek::$SATURDAY->getFullyQualifiedName() );

// Get an array with all the values of this enum type
$vals = DayOfTheWeek::getValues();

// $val will be DayOfTheWeek::$SATURDAY
$val = DayOfTheWeek::getValueByName( 'SATURDAY' );

// $val will be null
$val = DayOfTheWeek::getValueByName( 'THIS_IS_NOT_A_DAY' );


There's also an interface, ITypesafeEnum, which you can extend. For example:

interface IMoon extends ITypesafeEnum { }

class MoonOfMars extends TypesafeEnum implements IMoon {

    static $PHOBOS;
    static $DEIMOS;
}

MoonOfMars::setUp();

class MoonOfPluto extends TypesafeEnum implements IMoon {

    static $CHARON;
    static $STYX;
    static $NIX;
    static $KERBEROS;
    static $HYDRA;
}

MoonOfPluto::setUp();

function doSomethingMoony( IMoon $moon ) {
    ...
}

Extensibility[edit]

In the current implementation, it's already possible to add custom methods and properties to enum classes. If use cases arose, it would be easy to add custom constructors and constructor calls. (Just like in Java! See, for example, this tutorial.)

Tests[edit]

The implementation includes unit tests.

Advantages[edit]

  • Compact
  • Intuitive
  • Typesafe
  • No need to declare or see meaningless integer values
  • Meaningful conversion to strings
  • Helper functions on sets of enum values
  • Extensible
  • IDE autocompletion works

Disadvantages[edit]

  • You have to add a $ before the enum names.
  • The enums look like constants, but they aren't really constants. Though it's unlikely, the values could be changed by accident.
  • setUp() must be called on each enum class before it can be used. The above suggestion that it be called in the same .php file as the enum definition appears to violate Section 2.3 of PSR-1. (However, it may not violate the spirit of that standard—see the talk page for further discussion.)

Alternate approach[edit]

Another common approach to enums emulation in PHP is to use methods. Here is how enums might be defined and used following that approach:

class DayOfTheWeek extends MethodBasedTypesafeEnum {

    static $MONDAY;
    static $TUESDAY;
    static $WEDNESDAY;
    static $THURSDAY;
    static $FRIDAY;
    static $SATURDAY;
    static $SUNDAY;
}

print( todayIs( DayOfTheWeek::MONDAY() ) );

Advantages[edit]

  • Compact
  • Typesafe
  • No need to declare or see meaningless integer values
  • Meaningful conversion to strings
  • Helper functions on sets of enum values
  • Extensible
  • No need to call setUp() or transgress PSR-1
  • Values can't be changed by accident

Disadvantages[edit]

  • Less intuitive: you have to think of EnumClass::THIS() as a constant rather than a function
  • No IDE autocompletion

Use cases[edit]

Current work in progress on Editor campaigns uses the proposed implementation quite a bit.

For more possible use cases, just go to the core or mediawiki-extensions directories say grep -r "const [A-Z_]* = [[:digit:]]*".