diff --git a/lib/VM/BackedEnum.php b/lib/VM/BackedEnum.php index 8a2693935ef..50a1ce734fa 100644 --- a/lib/VM/BackedEnum.php +++ b/lib/VM/BackedEnum.php @@ -30,6 +30,16 @@ public static function caseForValue(ClassEntry $enum, Variable $value): ?EnumCas } EnumSupport::ensureBackedEnumValuesUnique($enum); $normalized = self::normalizeBackingArgument($enum, $value->resolveIndirect()); + $match = self::matchCaseForBackingValue($enum, $normalized); + if (null !== $match) { + return $match; + } + + return self::matchCaseForBackingValueFromConstants($enum, $normalized); + } + + private static function matchCaseForBackingValue(ClassEntry $enum, Variable $normalized): ?EnumCaseEntry + { foreach ($enum->enumCases as $case) { $backing = self::caseBackingScalar($enum->backedType, $case['value']); if (!self::backingValuesMatch($enum->backedType, $backing, $normalized)) { @@ -42,6 +52,31 @@ public static function caseForValue(ClassEntry $enum, Variable $value): ?EnumCas return null; } + /** + * Fallback when {@see ClassEntry::$enumCases} is empty or stale but case constants remain (#9603). + * + * @see Zend/zend_enum.c — zend_enum_from_case() backed-value hash + */ + private static function matchCaseForBackingValueFromConstants( + ClassEntry $enum, + Variable $normalized + ): ?EnumCaseEntry { + foreach ($enum->constants as $memberLc => $stored) { + $caseName = EnumSupport::enumCaseNameForConstantMember($enum, $memberLc); + if (null === $caseName) { + continue; + } + $backing = self::caseBackingScalar($enum->backedType, $stored); + if (!self::backingValuesMatch($enum->backedType, $backing, $normalized)) { + continue; + } + + return new EnumCaseEntry($enum, $caseName, clone $backing); + } + + return null; + } + /** * Canonical enum case variable for a matched case name (Zend singleton identity, #5533). */ diff --git a/lib/VM/EnumFromHandler.php b/lib/VM/EnumFromHandler.php index 91ddf399b31..8f1fb26e4c4 100644 --- a/lib/VM/EnumFromHandler.php +++ b/lib/VM/EnumFromHandler.php @@ -24,17 +24,18 @@ public function __construct( public function execute(Frame $frame): void { - EnumSupport::ensureBackedEnumValuesUnique($this->enum); + $enum = EnumSupport::resolveRuntimeEnumClass($frame->vmContext, $this->enum); + EnumSupport::ensureBackedEnumValuesUnique($enum); if (\count($frame->calledArgs) < 1) { throw new \LogicException( $this->getName().'() requires exactly 1 argument in this compiler build' ); } $arg = $frame->calledArgs[0]; - $match = BackedEnum::caseForValue($this->enum, $arg); + $match = BackedEnum::caseForValue($enum, $arg); if (null === $match) { if (!$this->try) { - throw new \ValueError(BackedEnum::valueErrorMessage($this->enum, $arg)); + throw new \ValueError(BackedEnum::valueErrorMessage($enum, $arg)); } if (null !== $frame->returnVar) { $frame->returnVar->null(); @@ -43,7 +44,7 @@ public function execute(Frame $frame): void return; } if (null !== $frame->returnVar) { - $canonical = BackedEnum::canonicalCaseVariable($this->enum, $match->caseName); + $canonical = BackedEnum::canonicalCaseVariable($enum, $match->caseName); if (null !== $canonical && EnumCaseSupport::isEnumCaseVariable($canonical)) { $frame->returnVar->copyFrom($canonical); } else { diff --git a/test/repro/issue_9603_enum_from.php b/test/repro/issue_9603_enum_from.php new file mode 100644 index 00000000000..645583d350c --- /dev/null +++ b/test/repro/issue_9603_enum_from.php @@ -0,0 +1,14 @@ +getMessage(), "\n"; +} diff --git a/test/unit/VmEnumCanonicalCaseVariableTest.php b/test/unit/VmEnumCanonicalCaseVariableTest.php index 577cddb9bf9..27e39047e95 100644 --- a/test/unit/VmEnumCanonicalCaseVariableTest.php +++ b/test/unit/VmEnumCanonicalCaseVariableTest.php @@ -40,4 +40,27 @@ enum E: string { case A = 'x'; case B = 'y'; } $this->assertNotNull($canonical); $this->assertTrue(EnumCaseSupport::isEnumCaseVariable($canonical)); } + + /** Issue #9603 — from()/tryFrom() must resolve via constants when enumCases table is empty. */ + public function testFromResolvesWhenEnumCasesTableEmpty(): void + { + $runtime = new Runtime(); + $runtime->run($runtime->parseAndCompile(<<<'PHP' +vmContext->classes['e']; + $this->assertNotEmpty($enum->enumCases); + $enum->enumCases = []; + + ob_start(); + $runtime->run($runtime->parseAndCompile(<<<'PHP' +name; +echo E::from('y')->name; +PHP, 'enum_from_empty_cases.php')); + $output = ob_get_clean(); + + $this->assertSame('AB', $output); + } } diff --git a/test/unit/VmEnumFromTest.php b/test/unit/VmEnumFromTest.php index 109a1513a02..b635a8fa639 100644 --- a/test/unit/VmEnumFromTest.php +++ b/test/unit/VmEnumFromTest.php @@ -141,4 +141,33 @@ enum Level: int { case Low = 1; } $this->expectExceptionMessage('Level::from(): Argument #1 ($value) must be of type int, string given'); $runtime->run($block); } + + /** Issue #9603 — exact maintainer repro: valid backing values resolve to enum cases. */ + public function testIssue9603EnumFromTryFromRepro(): void + { + $code = <<<'PHP' +name); +echo "\n"; +try { + E::from('a'); + echo "from ok\n"; +} catch (ValueError $e) { + echo 'from fail: ', $e->getMessage(), "\n"; +} +PHP; + $runtime = new Runtime(); + $block = $runtime->parseAndCompile($code, 'issue_9603_enum_from.php'); + ob_start(); + $runtime->run($block); + $output = ob_get_clean(); + + $this->assertSame("true\n'A'\nfrom ok\n", $output); + } }