Skip to content

BackedEnum::tryFrom() invalidly throws on wrong types and throws on a "self" type. #23392

Description

@MircoBabin

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:
  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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions