Hey all,
I poked around a bit in the Enum RFCs and I could find (maybe I missed) what's up with this:
enum Foo:string implements \Stringable
{
case FOO = 'bar';
public function __toString() {
return $this->value;
}
}
PHP Fatal error: Enum Foo cannot include magic method __toString
It seems to me that when the enum is a string it is entirely reasonable to expect __toString() to work here and support the \Stringable interface.
Is there a reason I'm missing as to why this is problematic in the engine that's been documented somewhere, or is this an oversight we can look at correcting?
John
Hey all,
I poked around a bit in the Enum RFCs and I could find (maybe I
missed) what's up with this:
Didja really?
https://wiki.php.net/rfc/auto-implement_stringable_for_string_backed_enums
Didja really?
https://wiki.php.net/rfc/auto-implement_stringable_for_string_backed_enums
Why has it been withdrawn?
Didja really?
https://wiki.php.net/rfc/auto-implement_stringable_for_string_backed_enums
I swear I did. <shame>
That said, looking at that RFC it's a slightly different take than what I am suggesting. This RFC suggests that string enums automatically implement Stringable . I am pointing out that it's a little silly IMO that we don't allow __toString() at all and let the developer choose if they want to implement Stringable . Looking through the PR comments it's pretty clear there was a decent amount of support for this alternative idea. It also looks like the PR for this died on the vine and I'm curious is there is any interest in reviving the discussion?
I'm not seeing an obvious upside to forbidding straight out __toString() , especially (as is pointed out in the PR comments) that you can use other native interfaces like JsonSerializable without issue.
I'm not seeing an obvious upside to forbidding straight out __toString()
I tend to agree, but Crell will drive by in a minute to drop some
philosophical nonsense about why we're all wrong :^)
Cheers,
Bilge
I'm not seeing an obvious upside to forbidding straight out __toString()
I tend to agree, but Crell will drive by in a minute to drop some
philosophical nonsense about why we're all wrong :^)Cheers,
Bilge
Made me chuckle 🤭
I'm not seeing an obvious upside to forbidding straight out __toString()
I tend to agree, but Crell will drive by in a minute to drop some philosophical nonsense about why we're all wrong :^)
If you could avoid disrespecting a contributor to the project, this would be appreciated.
Because I also agree with Larry on this topic that an enumeration is its own type and should not be coercible to another type.
The reason that you can use a backing value for enumeration is for serialization purposes.
There have been numerous discussion on this list about this topic, with plenty of people stating their opinions.
Best regards,
Gina P. Banyard
I'm not seeing an obvious upside to forbidding straight out |__toString()|
I tend to agree, but Crell will drive by in a minute to drop some
philosophical nonsense about why we're all wrong :^)If you could avoid disrespecting a contributor to the project, this
would be appreciated.
To be clear, I have nothing but love and respect for Larry.
Cheers,
Bilge
I'm not seeing an obvious upside to forbidding straight out
__toString()
I tend to agree, but Crell will drive by in a minute to drop some philosophical nonsense about why we're all wrong :^)
If you could avoid disrespecting a contributor to the project, this would be appreciated.
To be clear, I have nothing but love and respect for Larry.Cheers,
Bilge
I appreciate that. :-)
To the original question, there's two reasons that __toString() was deliberately omitted from enums:
-
To discourage their use as "fancy strings". Enums are their own type, independent of any other. Making them usable as 95% strings partially defeats their purpose.
-
We still intend to add associated values to enums at some point (aka ADTs, aka tagged unions), and... we're not really sure yet if that will impact __toString() at all. It may, it may not, we don't know. So for now we're keeping our options open by disallowing __toString(), in case it ends up being needed for some ADT-related behavior in the future.
Point 2 will, hopefully, resolve itself in time once we can get ADT support in. Point 1 will remain, however.
--Larry Garfield
To the original question, there's two reasons that __toString() was deliberately omitted from enums:
- To discourage their use as "fancy strings". Enums are their own type, independent of any other. Making them usable as 95% strings partially defeats their purpose.
I disagree with the idea that we need to take this extreme of a stance when it comes to the notion of protecting the developer from themselves. There are a lot of really value use cases where it makes sense to have a string-backed enum . Not allowing developers to cast it to a string isn't doing anything but making people type $enum->value instead of (string)$enum -- so if someone wants to defeat their purpose this isn't stopping them. It is, however, making the language inconsistent because I literally have a enumeration defined by strings and there is literally no way to make PHP treat it like a string. This makes APIs sloppy imo because consider something like this:
function foo(string|Stringable $bar) {}
Because of these limitations this blows up into a much uglier thing in order accomplish what IMO should be a straightforward task:
function fooThatWantsString(string|Stringable|UnitEnum $bar)
{
if(($bar instanceof UnitEnum) && !is_string($bar->value)) {
throw InvalidArgumentException('This is exactly the sort of thing we are trying to get rid of with type hinting');
} else {
$bar = $bar->value;
}
$bar = (string)$bar;
}
Of course, you could also pass $bar->value higher up the call stack but it's much cleaner IMO to let the function call be smart enough to typecast as necessary. What's the point of a Stringable interface if we can't actually implement __toString() for it? Note again we can implement JsonSerializable and nothing complains when enum serializes to a simple string.
- We still intend to add associated values to enums at some point (aka ADTs, aka tagged unions), and... we're not really sure yet if that will impact __toString() at all. It may, it may not, we don't know. So for now we're keeping our options open by disallowing __toString(), in case it ends up being needed for some ADT-related behavior in the future.
__toString() should return a string, if an enum literally represents a string constant it makes zero sense for it to ever return anything but that string constant. I don't think this is something ADTs or tagged unions or anything of the sort should play a role in deciding.
John
What's the point of a Stringable interface if we can't actually implement __toString() for it?
Just to show the range of viewpoints on this, I'd like to mention my opinion that Stringable is a horrible feature, with an implementation that's completely inconsistent with the rest of the language, and no clear semantic purpose. If your contract is "the input must be usable as a string", then the obvious type to require is "string".
I'm not saying my opinion is objectively right, or even held by a majority, I just wanted to provide a counterbalance of sorts.
function fooThatWantsString(string|Stringable|UnitEnum $bar)
{
if(($bar instanceof UnitEnum) && !is_string($bar->value)) {
throw InvalidArgumentException('This is exactly the sort of thing we are trying to get rid of with type hinting');
} else {
$bar = $bar->value;
}$bar = (string)$bar;
}
I would have thought the more common use case would be the opposite: you want the function to be limited to a particular enum, but allow strings for backward compatibility, and have this:
function fooThatTakesAnOption(FooOptionEnum|string $opt) {
if ( is_string($opt) ) {
$opt = FooOptionEnum::from($opt);
}
...
}
You might later want to use $opt->value, or $opt->getApiRepresentation(), or whatever else; but at that point you know you have an object of a particular type.
Regards,
Rowan Tommins
[IMSoP]
Just to show the range of viewpoints on this, I'd like to mention my opinion that Stringable is a horrible feature, with an implementation that's completely inconsistent with the rest of the language, and no clear semantic purpose. If your contract is "the input must be usable as a string", then the obvious type to require is "string".
I'm not saying my opinion is objectively right, or even held by a majority, I just wanted to provide a counterbalance of sorts.
I don't think it's without purpose... what it should be for is to allow developers to make flexible APIs where they can type-hint string|Stringable and the user of the API doesn't have to worry about it.
There are examples where complex data types (object, enums) can be intelligently and reasonably cast to a string for more than simply "output" purposes (e.g. print statements, log files and the like). Since we have __toString() it makes sense to me to have an interface I can type against to know that method is valid to call instead of hunting around for it with method_exists()
or other some such thing. See below for an example I pulled off the top of my head with an ORM.
function fooThatWantsString(string|Stringable|UnitEnum $bar)
I would have thought the more common use case would be the opposite: you want the function to be limited to a particular enum, but allow strings for backward compatibility, and have this:
function fooThatTakesAnOption(FooOptionEnum|string $opt) {
if ( is_string($opt) ) {
$opt = FooOptionEnum::from($opt);
}
...
}
I think there's pretty meaningful evidence based on the engagement in the PR (and the number of likes in that PR regarding specifically at least allowing __toString() to be implemented) that your version of the mock API above and my version don't have to be mutually exclusive things -- nor is one vs. the other better or worse from a generic perspective. A composer package you pull into your project won't know the Enum type you want to pass in, but it shouldn't be hard for you to pass in a string enum if the API requires a string. There are plenty of APIs out there in the wild right now buried in various composer packages that expect a string but a string-backed enum might make sense.... I haven't tested this directly but just as an offhand example consider something like Laravel's Eloquent ORM:
$model->query()->where('enumColumn', '=', MyEnum::MYVALUE)->get();
It's possible that Laravel has already been smart enough to add the necessary logic to look for UnitEnum here and resolve that to MyEnum::MYVALUE->value , but my argument is that it shouldn't have to try that hard. Nor should I have to write MyEnum::MYVALUE->value in my query -- nor should any developer ever have to think/worry about that for a string enum with a string parameter. As a library developer when I write a method to be used by others in an entirely unknown context, if my method takes a string input then I should be able to hint string|Stringable and know I've given users of my library maximum flexibility with basically no real effort on my part (other than a redundant $foo = (string)$foo line. The fact this works 99% of the time (except, strangely, string-backed enums) is the inconsistency I want to repair here.
As it seems to me there isn't a particularly strong argument for why we don't allow __toString() , would anyone have a strong objection to getting an RFC going to get this voted on? I didn't look closely at the original PR from the "auto implement __toString()" for string-backed enums, but I think this might literally be a one-liner just to enable enums to implement __toString() and the rest can be tossed.
John
I don't think it's without purpose... what it should be for is to allow developers to make flexible APIs where they can type-hint string|Stringable and the user of the API doesn't have to worry about it.
My view is that the user should have to think about it - if what the API needs is a string, tell me that, and I'll pick a method that returns a relevant string.
I also think it's weird that Stringable is a magic implicit interface. If it makes sense at all, surely it should be a pseudo-type like callable, and also accept int and float values, since they can be cast to string too.
But maybe I shouldn't have said anything, because I've had this conversation before and it generally just leads to agreeing to disagree. Plenty of people seem to think __toString and Stringable are a good idea, so I guess I'll just leave y'all to it.
Regards,
Rowan Tommins
[IMSoP]
As it seems to me there isn't a particularly strong argument for why we don't allow __toString() , would anyone have a strong objection to getting an RFC going to get this voted on? I didn't look closely at the original PR from the "auto implement __toString()" for string-backed enums, but I think this might literally be a one-liner just to enable enums to implement __toString() and the rest can be tossed.
I would vote against such a proposal because I fundamentally disagree from a language design principle.
I do tend to agree with Rowan that Stringable and the __toString() magic method overall is.... less than ideal.
Best regards,
Gina P. Banyard
would anyone have a strong objection to getting an RFC going to get
this voted on?
I don't think this is the right question to ask. Just write the RFC if
you want to. If anyone objects, they will do so on the ML and at vote
time, but it would be wrong for anyone to object to writing an RFC, I think.
Cheers,
Bilge
Didja really?
https://wiki.php.net/rfc/auto-implement_stringable_for_string_backed_enums
I swear I did. <shame>That said, looking at that RFC it's a slightly different take than what I am suggesting. This RFC suggests that
string
enums automatically implementStringable
. I am pointing out that it's a little silly IMO that we don't allow__toString()
at all and let the developer choose if they want to implementStringable
. Looking through the PR comments it's pretty clear there was a decent amount of support for this alternative idea. It also looks like the PR for this died on the vine and I'm curious is there is any interest in reviving the discussion?I'm not seeing an obvious upside to forbidding straight out
__toString()
, especially (as is pointed out in the PR comments) that you can use other native interfaces likeJsonSerializable
without issue.
Hello,
I personally find it an interesting design choice. I agree with it, and disagree with it at the same time. Like, anytime I find myself wanting to reach for enums as strings, I realize I want a constant instead. But then someone sees a string type and manually types it instead of using the constant (heh, I’ve been guilty of that myself a few times).
That being said, I would like to be able to use | and & on integer enums more than I would strings as stringables. Something like “flags” mode in C#. Maybe even make “flag” a backing type of enums. It would make a ton of json flags much simpler to reason about (and filters/sanitizers).
— Rob
That being said, I would like to be able to use | and & on integer enums more than I would strings as stringables. Something like “flags” mode in C#. Maybe even make “flag” a backing type of enums. It would make a ton of json flags much simpler to reason about (and filters/sanitizers).
It'd be nice but I'm not gonna argue about the integer flavors of the enums right now :)
Hey all,
I poked around a bit in the Enum RFCs and I could find (maybe I missed) what's up with this:
enum Foo:string implements \Stringable
{
case FOO = 'bar';public function __toString() { return $this->value; }
}
PHP Fatal error: Enum Foo cannot include magic method __toString
It seems to me that when the enum is a string it is entirely reasonable to expect __toString() to work here and support the \Stringable interface.
Is there a reason I'm missing as to why this is problematic in the engine that's been documented somewhere, or is this an oversight we can look at correcting?
John
As others have said, there are definite objections to
auto-implementing string conversions. I think that opt-in string
conversions are sufficiently different that you would have a shot at
an RFC passing. No guarantees about that, of course!