[php-src] Issue #23392: BackedEnum::tryFrom() invalidly throws on wrong types and throws on a "self" type.

[email protected] (MircoBabin)
Newsgroups php.bugs
Message-ID <[email protected]>
Issue: https://github.com/php/php-src/issues/23392
Author: MircoBabin

### Description

1)
The [PHP RFC: Enumerations](https://wiki.php.net/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.

2)
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)
```


3)

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);
} else {
    $value = null;
}
```
---

The following code https://3v4l.org/DLGgU :

```php
<?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:
```
1) [Bug] the third testcase tryFrom(FormatOutput::Text) should have not have thrown.
2) [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_
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.