Edit: Adjusted the example code of point 3 with elseif ($value instanceof BackedEnum)
Description
The PHP RFC: Enumerations states tryFrom() will return null if no corresponding Enum Case is found. This is incorrect in the current implementation.
- It throws on invalid types instead of returning
null. e.g BackedEnum::tryFrom(null) throws.
- And it throws on a totally valid
self value instead of returning the already correct enum value. e.g. BackedEnum::tryFrom(BackedEnum::someCase) throws.
tryFrom(int|string): ?self will take a scalar and return the corresponding Enum Case. If one is not found, it will return null. This is mainly useful in cases where the input scalar is untrusted and the caller wants to implement their own error handling or default-value logic.
The parameter for BackedEnum::tryFrom() is currently typed int|string. It should be typed mixed, because when not being in an all-variables-are-typed environment this could be anything. Which is acknowledged in the RFC text with
This is mainly useful in cases where the input scalar is untrusted.
The tryFrom() signature should change to:
BackedEnum::tryFrom(mixed)
The RFC states
and the caller wants to implement their own error handling or default-value logic.
Well if I have to accommodate for invalid types everywhere in my coding, how is tryFrom() than going to help me in the current int|string implementation?
Writing this everywhere as a wrapper around tryFrom() is not great developer experience.
$value = some-unknown-type;
if (is_string($value) || is_int($value)) {
$value = BackedEnum::tryFrom($value);
} elseif ($value instanceof BackedEnum) {
// do nothing, value is already of the expected type
} else {
$value = null;
}
The following code https://3v4l.org/DLGgU :
<?php
enum FormatOutput: string
{
case Text = '666777';
case Html = 'html';
}
const SOMECONST = 'Const hello';
function SomeFunction()
{
}
$SomeFunctionClosure = function() {
};
$SomeFunctionFirstClassCallable = SomeFunction(...);
$someFunctionClosureFromCallabe = Closure::fromCallable('SomeFunction');
$arrowFunction = fn($a) => $a;
$testcases = [
666777,
'666777',
FormatOutput::Text,
// For testing purposes: let's see what every type does...
SOMECONST,
'abc',
-6,
7,
-6.7,
null,
true,
false,
['a', 'b', 'c'],
[6 => 'six', 7 => 'seven', 67 => 'six seven'],
['a' => 'a', 'b' => 'b', 'c' => 'c', 0 => 'Zero'],
function() {},
$SomeFunctionClosure,
$SomeFunctionFirstClassCallable,
$someFunctionClosureFromCallabe,
$arrowFunction,
new DateTimeImmutable(),
fopen('php://memory', 'w+'),
];
foreach($testcases as $testcase) {
echo '--------------[ TESTCASE ]--------------'."\n";
var_dump($testcase);
echo "\n";
echo '--- tryFrom()'."\n";
try {
$value = FormatOutput::tryFrom($testcase);
if ($value !== null) {
echo 'Value is '.$value->name.' with value '.$value->value.'.'."\n";
} else {
echo 'Value is NULL.'."\n";
}
} catch (Throwable $exception) {
echo $exception::class, ': ', $exception->getMessage(), "\n";
}
echo '--- from()'."\n";
try {
$value = FormatOutput::from($testcase);
if ($value !== null) {
echo 'Value is '.$value->name.' with value '.$value->value.'.'."\n";
} else {
echo 'Value is NULL.'."\n";
}
} catch (Throwable $exception) {
echo $exception::class, ': ', $exception->getMessage(), "\n";
}
echo "\n";
echo "\n";
echo "\n";
}
Resulted in this output:
--------------[ TESTCASE ]--------------
int(666777)
--- tryFrom()
Value is Text with value 666777.
--- from()
Value is Text with value 666777.
--------------[ TESTCASE ]--------------
string(6) "666777"
--- tryFrom()
Value is Text with value 666777.
--- from()
Value is Text with value 666777.
--------------[ TESTCASE ]--------------
enum(FormatOutput::Text)
--- tryFrom()
TypeError: FormatOutput::tryFrom(): Argument #1 ($value) must be of type string|int, FormatOutput given
--- from()
TypeError: FormatOutput::from(): Argument #1 ($value) must be of type string|int, FormatOutput given
--------------[ TESTCASE ]--------------
string(11) "Const hello"
--- tryFrom()
Value is NULL.
--- from()
ValueError: "Const hello" is not a valid backing value for enum FormatOutput
--------------[ TESTCASE ]--------------
string(3) "abc"
--- tryFrom()
Value is NULL.
--- from()
ValueError: "abc" is not a valid backing value for enum FormatOutput
--------------[ TESTCASE ]--------------
int(-6)
--- tryFrom()
Value is NULL.
--- from()
ValueError: "-6" is not a valid backing value for enum FormatOutput
--------------[ TESTCASE ]--------------
int(7)
--- tryFrom()
Value is NULL.
--- from()
ValueError: "7" is not a valid backing value for enum FormatOutput
--------------[ TESTCASE ]--------------
float(-6.7)
--- tryFrom()
Deprecated: Implicit conversion from float -6.7 to int loses precision in /in/DLGgU on line 60
Value is NULL.
--- from()
Deprecated: Implicit conversion from float -6.7 to int loses precision in /in/DLGgU on line 72
ValueError: "-6" is not a valid backing value for enum FormatOutput
--------------[ TESTCASE ]--------------
NULL
--- tryFrom()
Deprecated: FormatOutput::tryFrom(): Passing null to parameter #1 ($value) of type string|int is deprecated in /in/DLGgU on line 60
Value is NULL.
--- from()
Deprecated: FormatOutput::from(): Passing null to parameter #1 ($value) of type string|int is deprecated in /in/DLGgU on line 72
ValueError: "0" is not a valid backing value for enum FormatOutput
--------------[ TESTCASE ]--------------
bool(true)
--- tryFrom()
Value is NULL.
--- from()
ValueError: "1" is not a valid backing value for enum FormatOutput
--------------[ TESTCASE ]--------------
bool(false)
--- tryFrom()
Value is NULL.
--- from()
ValueError: "0" is not a valid backing value for enum FormatOutput
--------------[ TESTCASE ]--------------
array(3) {
[0]=>
string(1) "a"
[1]=>
string(1) "b"
[2]=>
string(1) "c"
}
--- tryFrom()
TypeError: FormatOutput::tryFrom(): Argument #1 ($value) must be of type string|int, array given
--- from()
TypeError: FormatOutput::from(): Argument #1 ($value) must be of type string|int, array given
--------------[ TESTCASE ]--------------
array(3) {
[6]=>
string(3) "six"
[7]=>
string(5) "seven"
[67]=>
string(9) "six seven"
}
--- tryFrom()
TypeError: FormatOutput::tryFrom(): Argument #1 ($value) must be of type string|int, array given
--- from()
TypeError: FormatOutput::from(): Argument #1 ($value) must be of type string|int, array given
--------------[ TESTCASE ]--------------
array(4) {
["a"]=>
string(1) "a"
["b"]=>
string(1) "b"
["c"]=>
string(1) "c"
[0]=>
string(4) "Zero"
}
--- tryFrom()
TypeError: FormatOutput::tryFrom(): Argument #1 ($value) must be of type string|int, array given
--- from()
TypeError: FormatOutput::from(): Argument #1 ($value) must be of type string|int, array given
--------------[ TESTCASE ]--------------
object(Closure)#7 (3) {
["name"]=>
string(22) "{closure:/in/DLGgU:43}"
["file"]=>
string(9) "/in/DLGgU"
["line"]=>
int(43)
}
--- tryFrom()
TypeError: FormatOutput::tryFrom(): Argument #1 ($value) must be of type string|int, Closure given
--- from()
TypeError: FormatOutput::from(): Argument #1 ($value) must be of type string|int, Closure given
--------------[ TESTCASE ]--------------
object(Closure)#1 (3) {
["name"]=>
string(22) "{closure:/in/DLGgU:15}"
["file"]=>
string(9) "/in/DLGgU"
["line"]=>
int(15)
}
--- tryFrom()
TypeError: FormatOutput::tryFrom(): Argument #1 ($value) must be of type string|int, Closure given
--- from()
TypeError: FormatOutput::from(): Argument #1 ($value) must be of type string|int, Closure given
--------------[ TESTCASE ]--------------
object(Closure)#2 (1) {
["function"]=>
string(12) "SomeFunction"
}
--- tryFrom()
TypeError: FormatOutput::tryFrom(): Argument #1 ($value) must be of type string|int, Closure given
--- from()
TypeError: FormatOutput::from(): Argument #1 ($value) must be of type string|int, Closure given
--------------[ TESTCASE ]--------------
object(Closure)#3 (1) {
["function"]=>
string(12) "SomeFunction"
}
--- tryFrom()
TypeError: FormatOutput::tryFrom(): Argument #1 ($value) must be of type string|int, Closure given
--- from()
TypeError: FormatOutput::from(): Argument #1 ($value) must be of type string|int, Closure given
--------------[ TESTCASE ]--------------
object(Closure)#4 (4) {
["name"]=>
string(22) "{closure:/in/DLGgU:22}"
["file"]=>
string(9) "/in/DLGgU"
["line"]=>
int(22)
["parameter"]=>
array(1) {
["$a"]=>
string(10) "<required>"
}
}
--- tryFrom()
TypeError: FormatOutput::tryFrom(): Argument #1 ($value) must be of type string|int, Closure given
--- from()
TypeError: FormatOutput::from(): Argument #1 ($value) must be of type string|int, Closure given
--------------[ TESTCASE ]--------------
object(DateTimeImmutable)#8 (3) {
["date"]=>
string(26) "2026-08-20 13:05:20.023870"
["timezone_type"]=>
int(3)
["timezone"]=>
string(16) "Europe/Amsterdam"
}
--- tryFrom()
TypeError: FormatOutput::tryFrom(): Argument #1 ($value) must be of type string|int, DateTimeImmutable given
--- from()
TypeError: FormatOutput::from(): Argument #1 ($value) must be of type string|int, DateTimeImmutable given
--------------[ TESTCASE ]--------------
resource(5) of type (stream)
--- tryFrom()
TypeError: FormatOutput::tryFrom(): Argument #1 ($value) must be of type string|int, resource given
--- from()
TypeError: FormatOutput::from(): Argument #1 ($value) must be of type string|int, resource given```
But I expected this output instead:
- [Bug] the third testcase tryFrom(FormatOutput::Text) should have not have thrown.
- [Bug] the eighth testcase and further the tryFrom() should all return null and not throw.
### PHP Version
```plain
PHP 8.5.9 in 3v4l
Operating System
No response
Edit: Adjusted the example code of point 3 with
elseif ($value instanceof BackedEnum)Description
The PHP RFC: Enumerations states
tryFrom()will returnnullif no corresponding Enum Case is found. This is incorrect in the current implementation.null. e.gBackedEnum::tryFrom(null)throws.selfvalue instead of returning the already correct enum value. e.g.BackedEnum::tryFrom(BackedEnum::someCase)throws.The parameter for BackedEnum::tryFrom() is currently typed
int|string. It should be typedmixed, because when not being in an all-variables-are-typed environment this could be anything. Which is acknowledged in the RFC text withThe tryFrom() signature should change to:
The RFC states
Well if I have to accommodate for invalid types everywhere in my coding, how is tryFrom() than going to help me in the current
int|stringimplementation?The following code https://3v4l.org/DLGgU :
Resulted in this output:
Operating System
No response