Hi people!
Today I tried to do ResourceType::from()
and was surprised that my IDE immediately screamed at me. After further investigation, I realized that Basic Enums (this is what the RFC called it [1]) does not have these commodities backed into them.
I did some digging and I found 2 main discussions: Stringable by default [2][3] and backed-in commodities [4]. I think Benjamin, Derick and Nicolas have argued why Stringable by default may not be desirable and I only mention this for completeness of the conversation. I don’t want to talk about that at all. Instead, I want to focus on the other side: construct from string by default (from/tryFrom()).
I also read Larry’s post that has been shared throughout these discussions [5] and it seems fair and sound to use guardrails to discourage unintended use. This gets into the territory of “let me do what I want, I know what I’m doing” vs “let me limit what you can do because I built it and I know exactly what it was intended for”, which I also want to try and steer away from debating, each camp has its merits.
Bilge said:
My question, then, is why can’t basic enumerations have these semantics by default? Or, to state it more concretely, what would be the downside to having all “basic” enumerations actually being “backed” enumerations whose values implicitly mirror their names for the purposes of converting to/from strings? Would this not make basic enumeration more useful without any particular downsides?
While I’m searching for similar clarification, I want to pose the question differently. I feel like the answer to his question is in Larry’s article about discouraging fancy strings. My question boils down purely to: can Basic Enums implement ::from() / tryFrom() methods?
Larry said:
[…] In your case, you want to “upcast” a string to an enum. That means you’re doing some sort of deserialization, presumably. In that case, a backed enum is what you want. A unit enum isn’t serializable, by design.
Although this starts to thread into very pedantic territory, I think, in fact, a unit enum (assuming it means Basic enum) is in fact always serializable to a string: $enum->value
. By design, the value is a string and it cannot have duplicate values in a single enum. It means it’s extremely easy to define an Enum in PHP and at some point store it in a storage engine / database in the form of $enum->value
. Now if I want to get back my Enum at a later stage I need to implement exactly the same code that already exists in the PHP engine behind Enum::from()
. Maybe it’s not serializable in the sense that it doesn’t implement any true serialization mechanism, which I’m guessing a backed-enum probably does, but I’m trying to come from the very practical application of creating a Basic Enum at an HTTP context (which is the bread and butter of PHP) and then recovering said Enum in a background worker context without having to use PHP serialize()
function and store PHP-specific dialect in a database that is used by multiple teams and programming languages.
I also take the point that it is easy to argue against all this: just put : string
on your Enum and duplicate the names with values. Still, this doesn’t address the “surprise effect” of “why this Enum doesn’t have ::from() in it?”. There doesn’t exist any other value (string or otherwise) that could be used in ::from() or ::tryFrom() in a Basic Enum, which could make it less contentious. Also, in the spirit of NOT making Enums “Fancy strings”, I’m looking for ways to reconstruct my Enum and all the behaviors available inside of it without even having to associate or think about a string. The only reason a string comes into the discussion is because $enum->value is one and is stored. I also checked and:
enum Foo {
case 1;
case 2;
}
is a parse error. [6].
Larry has also suggested that instead of making Basic Enum implement ::from()
and ::tryFrom()
we could instead offer auto-populated String-Backed Enum values. That means transforming this:
enum Foo: string {
case Bar;
case Baz;
}
(which is a Fatal Error today) into this:
enum Foo: string {
case Bar = ‘Bar’;
case Baz = ‘Baz’;
}
I also like this proposal. Although semantically, I think it would be better / have been better to have Basic Enum implementing the ::from() methods, one could argue that adding it now could be a breaking change since people could have Basic Enum already implementing their own custom ::from() method.
In conclusion, the “complex” (as opposed to primitive) object Enum is not a Fancy String and where I’m coming from I believe to be in sync with that mindset. However, PHP is highly used in Web context where we may need to use asynchronous processes to make API calls fast and schedule executions at a different context which often involves serialization. As such, being able to reconstruct a Basic Enum seems a rather fundamental need that we can still make it viable by making a small : string
change to the Enum and opting-in into the from / tryFrom utilities. This should not affect Int-Backed Enums at all.
Where casing is concerned (camelCase, PascalCase, snake-case, etc) [7], one can argue that if you want to have full control over casing, you should definitely take control over the values of your Enum. The beauty (in my mind) about making it default to the enum name is that it doesn’t matter if I follow PER-CS rules or not, the truth is I don’t need to think about strings at all because my Enum is not a Fancy string.
I didn’t intend to write such a long email, but I’m really keen on hearing arguments against everything I shared to see if there are any flaws in my thought process.
[1] https://wiki.php.net/rfc/enumerations#basic_enumerations
[2] https://externals.io/message/118040
[3] https://externals.io/message/124991
[4] https://externals.io/message/123388
[5] https://peakd.com/hive-168588/@crell/on-the-use-of-enums
[6] https://3v4l.org/cDISV#v8.4.10
[7] https://externals.io/message/123388#123394
···
Marco Deleu