On 4 September 2025 15:50:08 BST, Rob Landers <rob@bottled.codes> wrote: >I think this is a fair observation and a fair question; but I think it is >important not to have "magic". The power-of-two rule is to make it possible to >work back how $enum->value === 15 (0x1111) even if you are completely new to >the language. If you just use some magical cardinal order, it is impossible to >reserve ranges, handle communications with external systems, etc.
A set does not need elements to have a defined order, only a defined identity; that is available on any enum, even one with no backing at all. That pure set could be serialised in various ways, based on the available serialisations of its elements (comma-separated list, integer bitmask, binary string bitmask, etc). That's the strongly typed model. The weakly typed model is to keep the values permanently in their serialised form, and manipulate that directly. That is, you have a set of integers for the flags, and construct a new integer for the set of flags. That has the advantage of being simple and efficient, at the cost of safety and easy tool affordance. What you're suggesting sounds like somewhere between the two: the individual flags are of a specific type, rather than raw integers, but the set itself is just an integer composed of their "backing values". The big downside I see is that you can't natively label a parameter or return value as being a "set of flags from this enum". You could probably make a docblock type annotation work with an external static analyser, but in that case, you might as well use that tool to enforce the powers of 2 on your enum or Plain Old Constants. Indeed, enforcing "this integer must be a power of 2" is not really anything to do with enums, it would be useful on *any* type declaration. Personally, I find the concept of enum cases having a single backing value unnecessarily limiting, and would much prefer Java-style case properties. enum FooFlag: object { case None(0, ''); case ThrowOnError(0b1, 'T'); // etc. case PrettyPrint(0b1000, 'P'); // etc. public function __construct(public int $flagForBinaryApi, public string $flagForTextApi){} } FooFlag::ThrowOnError->flagForBinaryApi; // 0b1 FooFlag:: PrettyPrint->flagForTextApi; // 'P' Ideally we would declare that $flagForBinaryApi must be a power of 2, and that $flagForTextApi must be a single character, using some general-purpose feature of the type system. The only thing that might be enum-specific is a way to say whether the values of a property must be unique (something we don't currently have with single-value backed enums). Then the ideal would be an EnumSet customised to work with any properties or methods it wanted: $foo = FooFlag::ThrowOnError | FooFlag::PrettyPrint; $foo->serialiseForBinaryApi(); // 0b1001 $foo->serialiseForTextApi(); // 'TP' Rowan Tommins [IMSoP]