Hello internals,
I would like to propose a new type of backed enum: flag. This is an integer-backed enum that requires power-of-two values that can be combined with the bitwise operators to be used as flags.
In some core functions, it might look like this:
enum JSON_FLAG: flag {
case None = 0;
case ThrowOnError = 0x1 << 1;
// etc.
case PrettyPrint = 0x01 << 4;
// etc.
}
function json_decode(string $data, bool $assoc = false, int $depth = 512, JSON_FLAG|int $options = JSON_FLAG::None) {}
json_decode($data, options: JSON_FLAG::ThrowOnError | JSON_FLAG::PrettyPrint);
Note that the goal here isn’t so much to save typing space, as it is to provide easier discoverability through IDEs, better type safety, and better ergonomics in the language overall.
Attempting to use a backed value that is not a power-of-two would result in a compilation error. Bitwise operations are performed as though they’re performed on a regular integer.
What do you think?
I couldn’t find any similar proposals (listed on the RFC page) before proposing this on the list, so if you’re working on this, please speak up; I’d like to join forces.
— Rob
Hi Rob,
Am 3. September 2025 22:44:58 MESZ schrieb Rob Landers rob@bottled.codes:
Hello internals,
I would like to propose a new type of backed enum: flag. This is an integer-backed enum that requires power-of-two values that can be combined with the bitwise operators to be used as flags.
In some core functions, it might look like this:
enum JSON_FLAG: flag {
case None = 0;
case ThrowOnError = 0x1 << 1;
// etc.
case PrettyPrint = 0x01 << 4;
// etc.
}function json_decode(string $data, bool $assoc = false, int $depth = 512, JSON_FLAG|int $options = JSON_FLAG::None) {}
json_decode($data, options: JSON_FLAG::ThrowOnError | JSON_FLAG::PrettyPrint);
Note that the goal here isn’t so much to save typing space, as it is to provide easier discoverability through IDEs, better type safety, and better ergonomics in the language overall.
Attempting to use a backed value that is not a power-of-two would result in a compilation error. Bitwise operations are performed as though they’re performed on a regular integer.
What do you think?
I think what we actually would need is an EnumSet which can be used with any enum but (based on its ordinal) but this would play nicely together with generics new EnumSet<JsonOption>
.
I couldn’t find any similar proposals (listed on the RFC page) before proposing this on the list, so if you’re working on this, please speak up; I’d like to join forces.
— Rob
Marc
Attempting to use a backed value that is not a power-of-two would result in a compilation error. Bitwise operations are performed as though they’re performed on a regular integer.
What do you think?
I think what we actually would need is an EnumSet which can be used with any enum but (based on its ordinal) but this would play nicely together with generics
new EnumSet<JsonOption>
.
Agreed; using enums-backed-by-powers-of-two is just a way to encode sets
of values drawn from an enum (it's a way to enumerate the possible
subsets of a set). It should be possible to have a set of Enum values
for any Enum, rather than break the Enum type concept by exposing
implementation details, and allow sets for some types but not others
depending on how they are serialised.
A class can be built in userspace as a "SetOfEnum"; or such a class
could be added to the standard library; or even have, for each Enum
declared, an EnumSet (final) class is also implicitly declared.
Attempting to use a backed value that is not a power-of-two would result in a compilation error. Bitwise operations are performed as though they’re performed on a regular integer.
What do you think?
I think what we actually would need is an EnumSet which can be used with any enum but (based on its ordinal) but this would play nicely together with generics
new EnumSet<JsonOption>
.Agreed; using enums-backed-by-powers-of-two is just a way to encode sets
of values drawn from an enum (it's a way to enumerate the possible
subsets of a set). It should be possible to have a set of Enum values
for any Enum, rather than break the Enum type concept by exposing
implementation details, and allow sets for some types but not others
depending on how they are serialised.A class can be built in userspace as a "SetOfEnum"; or such a class
could be added to the standard library; or even have, for each Enum
declared, an EnumSet (final) class is also implicitly declared.
It feels like there's two different related topics here.
One is using enums as a way to define a set of bit flags rather than constants. I'm not sure if this is necessary, but if so, I'd expect a bit more automation than the initial proposal here. The statement that bitwise operations would fall back to integer ops makes me think it falls into the "fancy constants" category we have tried to avoid.
The other is enum sets, which I am very much in favor of but designing those in a clean way is non-trivial. My collections research with Derick a while back included a Set
type, which also supported operator overrides for the |, <, <=, -, and other operators, so it did effectively give a very nice enum set syntax. Whether that is the optimal approach or not is unclear. But an Enum Set feature should work with any enum. (And of course runs into the eternal generics question.)
So, Rob, which one are you really talking about?
--Larry Garfield
Attempting to use a backed value that is not a power-of-two would result in a compilation error. Bitwise operations are performed as though they’re performed on a regular integer.
What do you think?
I think what we actually would need is an EnumSet which can be used with any enum but (based on its ordinal) but this would play nicely together with generics
new EnumSet<JsonOption>
.Agreed; using enums-backed-by-powers-of-two is just a way to encode sets
of values drawn from an enum (it's a way to enumerate the possible
subsets of a set). It should be possible to have a set of Enum values
for any Enum, rather than break the Enum type concept by exposing
implementation details, and allow sets for some types but not others
depending on how they are serialised.A class can be built in userspace as a "SetOfEnum"; or such a class
could be added to the standard library; or even have, for each Enum
declared, an EnumSet (final) class is also implicitly declared.It feels like there's two different related topics here.
One is using enums as a way to define a set of bit flags rather than constants. I'm not sure if this is necessary, but if so, I'd expect a bit more automation than the initial proposal here. The statement that bitwise operations would fall back to integer ops makes me think it falls into the "fancy constants" category we have tried to avoid.
The other is enum sets, which I am very much in favor of but designing those in a clean way is non-trivial. My collections research with Derick a while back included a
Set
type, which also supported operator overrides for the |, <, <=, -, and other operators, so it did effectively give a very nice enum set syntax. Whether that is the optimal approach or not is unclear. But an Enum Set feature should work with any enum. (And of course runs into the eternal generics question.)So, Rob, which one are you really talking about?
--Larry Garfield
Hey Larry, Marc, Morgan,
You are all asking basically the same question; I think: "This looks like a leaky abstraction and could possibly be solved more elegantly?"
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. I'm not opposed to it, however, especially when end-to-end values are in PHP itself. But, when you need to communicate with external systems, being able to have that control is very important.
After doing a bit more digging, it appears this can be handled entirely as an extension, so I think I'll just go that route for now.
— Rob
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]
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.
Interesting... but we don’t really have a way to say that a parameter or return value must satisfy any constraints except that it be of a certain type. Hmmm, I guess we have "true" and "false" as values that can be returned/accepted by functions — but I think they’re the only ones.
A form of generics could solve some of this... an enum is a set, so what we really want to say is: "map this set of PHP identifiers to this other set of values". Which we already have via backed enums. What we lack is some set of operators to allow us to select subsets of the set and represent them. I was thinking of bitwise operators, because it seems the most natural. In fact, my extension approach was to just inject some operators on the class entry and call it a day.
But with your response and my implementation that needed to deal with string-backed enums... The bitwise operators with enums would be a bit weird. Because | is a type union and saying Foo::PrettyPrint | Foo::ThrowOnError also kind of a union, but really, we’re constructing a new subset. So, maybe | isn’t the best operator. Then, when you would check, you use & (Foo::PrettyPrint & $val === 0)? It is not that elegant looking... idiomatic in languages like C, sure, but PHP ... not so much.
If I were to step back, I’d probably end up with something like this:
// use array-ish syntax to say that it is a subset of Foo
$foo = Foo::[PrettyPrint, ThrowOnError];
// have a function accept any Foo but returns only specific subsets of Foo.
function doFoo(Foo $anyFoo): Foo::[PrettyPrint, ThrowOnError]|Foo::[PrettyPrint]|null
// check if a value of Foo intersects with another set of Foo
return enum_intersects($anyFoo, Foo::[PrettyPrint]);
In this case, backing values don’t matter at all. Serialization would still be an issue, but then again, it is already.
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]
— Rob
Interesting... but we don’t really have a way to say that a parameter or return value must satisfy any constraints except that it be of a certain type. Hmmm, I guess we have "true" and "false" as values that can be returned/accepted by functions — but I think they’re the only ones.
Declaring an enum already declares a type. I wasn't thinking about constraining on lists of values, just saying that something should be a "set of T" where T is the enum type.
Relying on integer ops, the result ends up as an int:
enum FooOption: flag { ... }
class Foo {
...
function setOption(FooOption $opt): void { ... }
function getCurrentOptions(): int { ... }
}
With general-purpose generics, you can make that strongly typed:
enum FooOption { ... }
class Foo {
...
function setOption(FooOption $opt): void { ... }
function getCurrentOptions(): Set<FooOption> { ... }
}
A specific EnumBitSet<T> could implement the integer serialisation:
class EnumBitSet<T> extends Set<T> {
function asBitMask(): int { ... }
}
enum FooOption: int { ... }
class Foo {
...
function setOption(FooOption $opt): void { ... }
function getCurrentOptions(): EnumBitSet<FooOption> { ... }
}
$foo = new Foo;
...
$foo->getCurrentOptions()->asBitMask();
With the recently proposed declare-time generics, you'd just need an extra declaration alongside your enum:
class EnumBitSet<T> extends Set<T> {
function asBitMask(): int { ... }
}
enum FooOption: int { ... }
class FooOptionSet extends EnumSet<FooOption> {
// Can add additional methods here, but don't need to
}
class Foo {
...
function setOption(FooOption $opt): void { ... }
function getCurrentOptions(): FooOptionSet { ... }
}
$foo = new Foo;
...
$foo->getCurrentOptions()->asBitMask();
All the operator overloads or methods for things like intersect, union, contains, etc could be on the top-level Set<T>, since they don't intrinsically have anything to do with enums. That's the joy of our enum implementation being object-based: any algorithm that works on object identity automatically works on enum cases.
Rowan Tommins
[IMSoP]