From ffa772062fa8bda2917050ea78cc02e4ed0dd805 Mon Sep 17 00:00:00 2001 From: calavera Date: Sat, 20 Dec 2025 10:14:51 +0100 Subject: [PATCH 1/3] fix: error on null value in EnumFrom@wrap in strict mode --- src/Traits/EnumFrom.php | 9 ++++++++- tests/EnumFromTest.php | 10 ++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Traits/EnumFrom.php b/src/Traits/EnumFrom.php index 51b75a5..88aeb6c 100644 --- a/src/Traits/EnumFrom.php +++ b/src/Traits/EnumFrom.php @@ -22,10 +22,17 @@ trait EnumFrom */ public static function wrap(self|string|int|null $value, bool $strict = false): ?self { - if ($value instanceof self || is_null($value)) { + if ($value instanceof self) { return $value; } + if (is_null($value)) { + if ($strict) { + throw new ValueError('"'.$value.'" is not a valid backing value for enum "'.self::class.'"'); + } + return null; + } + $enum = null; if (is_string($value) && self::isIntBacked()) { if (is_numeric($value)) { diff --git a/tests/EnumFromTest.php b/tests/EnumFromTest.php index f278282..97ef720 100644 --- a/tests/EnumFromTest.php +++ b/tests/EnumFromTest.php @@ -51,11 +51,21 @@ ->toThrow(ValueError::class); }); +it('throws ValueError for null value in strict mode', function () { + expect(fn () => StringBackedEnum::wrap(null, true)) + ->toThrow(ValueError::class); +}); + it('returns null for invalid backing value when not in strict mode', function () { expect(StringBackedEnum::wrap('non-existent-value')) ->toBeNull(); }); +it('returns null for null value when not in strict mode', function () { + expect(StringBackedEnum::wrap(null)) + ->toBeNull(); +}); + it('does work with tryFrom method', function ($enumCass, $value, $result) { expect($enumCass::tryFrom($value))->toBe($result)->not->toThrow(ValueError::class); })->with([ From 6216650aede4d5704857cfff41df38060d02d36e Mon Sep 17 00:00:00 2001 From: calavera Date: Mon, 29 Dec 2025 07:48:09 +0100 Subject: [PATCH 2/3] fix: add README section form wrap method --- README.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++--- composer.json | 1 - 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b471ef5..e81f719 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,56 @@ StringBackedEnum::tryFromName('PENDING'); // StringBackedEnum::PENDING StringBackedEnum::tryFromName('MISSING'); // null ``` +#### `wrap()` +This method is a convenience helper to "wrap" a value (or an enum instance) into the corresponding enum instance. It is especially useful when you accept mixed input (an enum instance, a case name, a backing value, or null) and want to normalize it into the enum instance. + +Signature: +```php +public static function wrap(self|string|int|null $value, bool $strict = false): ?self +``` + +Behavior notes: +- If an actual enum instance (same enum) is passed, it is returned unchanged. +- If null is passed, the method throws an error if `$strict = true`, otherwise it returns null. +- If a backing value is passed (int or string), the method attempts to resolve it using `tryFrom()` (for BackedEnum) or `tryFromName()` (for pure enums). +- For int-backed enums, numeric strings (e.g. `'1'`) are accepted: they are converted to integer and processed as numeric backing values. +- When a string is passed and no backing match is found, `wrap()` will try to resolve it as a case name via `tryFromName()`. +- By default (`$strict = false`) the method returns null if no enum case matches. When `$strict = true`, the method throws a `ValueError` if the value cannot be converted to a valid enum instance. + +Examples: +```php +// Passing an enum instance returns it unchanged +$enum = IntBackedEnum::PENDING; +IntBackedEnum::wrap($enum); // IntBackedEnum::PENDING + +// Null remains null +IntBackedEnum::wrap(null); // null +IntBackedEnum::wrap(null, true); // throws ValueError + +// Backed enum by native backing value +IntBackedEnum::wrap(1); // IntBackedEnum::ACCEPTED +StringBackedEnum::wrap('P'); // StringBackedEnum::PENDING + +// Numeric string for int-backed enum +IntBackedEnum::wrap('1'); // IntBackedEnum::ACCEPTED + +// Case name (pure enum) +PureEnum::wrap('PENDING'); // PureEnum::PENDING + +// Case name (backed enum) +StringBackedEnum::wrap('PENDING'); // StringBackedEnum::PENDING + +// Non matching values +PureEnum::wrap('MISSING'); // null +PureEnum::wrap('MISSING', true); // throws ValueError: '"MISSING" is not a valid backing value for enum "Namespace\PureEnum"' +``` + +Notes on error message: +- When `$strict` is true and no match is found, `wrap()` throws a `ValueError` with a message similar to: + '"" is not a valid backing value for enum ""' + +This helper is useful in input normalization flows (e.g. DTOs, HTTP handlers, form processors) where you want to accept several forms of enum input and consistently obtain an enum instance or a null. + ### Inspection This helper permits check the type of enum (`isPure()`,`isBacked()`) and if enum contains a case name or value (`has()`, `doesntHave()`, `hasName()`, `doesntHaveName()`, `hasValue()`, `doesntHaveValue()`). @@ -248,9 +298,9 @@ StringBackedEnum::hasName('A') // false #### `hasValue()` and `doesntHaveValue()` `hasValue()` method permit checking if an enum has a case by passing int, string or enum instance. -For convenience, there is also an `doesntHaveValue()` method which is the exact reverse of the `hasValue()` method. +For convenience, there is also a `doesntHaveValue()` method which is the exact reverse of the `hasValue()` method. -```php +```enum-helper/README.md#L123-130 PureEnum::hasValue('PENDING') // true PureEnum::hasValue('P') // false IntBackedEnum::hasValue('ACCEPTED') // false @@ -349,7 +399,7 @@ StringBackedEnum::values([StringBackedEnum::NO_RESPONSE, StringBackedEnum::DISCA IntBackedEnum::values([IntBackedEnum::NO_RESPONSE, IntBackedEnum::DISCARDED]); // [3, 2] ``` #### `valuesByName()` -This method returns a associative array of [name => value] on `BackedEnum`, [name => name] on pure enum. +This method returns an associative array of [name => value] on `BackedEnum`, [name => name] on pure enum. ```php PureEnum::valuesByName(); // ['PENDING' => 'PENDING','ACCEPTED' => 'ACCEPTED','DISCARDED' => 'DISCARDED',...] StringBackedEnum::valuesByName(); // ['PENDING' => 'P','ACCEPTED' => 'A','DISCARDED' => 'D','NO_RESPONSE' => 'N'] diff --git a/composer.json b/composer.json index c915775..9280d79 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,6 @@ "test:types": "vendor/bin/phpstan analyse --ansi", "test:unit": "vendor/bin/pest --colors=always", "test": [ - "@test:lint", "@test:types", "@test:unit" ], From d9524eb80cc468e179f9f5b9770c0eee0597c48d Mon Sep 17 00:00:00 2001 From: Alberto Peripolli Date: Mon, 29 Dec 2025 09:53:59 +0100 Subject: [PATCH 3/3] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e81f719..9d73191 100644 --- a/README.md +++ b/README.md @@ -300,14 +300,14 @@ StringBackedEnum::hasName('A') // false `hasValue()` method permit checking if an enum has a case by passing int, string or enum instance. For convenience, there is also a `doesntHaveValue()` method which is the exact reverse of the `hasValue()` method. -```enum-helper/README.md#L123-130 +```php PureEnum::hasValue('PENDING') // true PureEnum::hasValue('P') // false IntBackedEnum::hasValue('ACCEPTED') // false IntBackedEnum::hasValue(1) // true StringBackedEnum::doesntHaveValue('Z') // true StringBackedEnum::hasValue('A') // true -```` +``` ### Equality This helper permits to compare an enum instance (`is()`,`isNot()`) and search if it is present inside an array (`in()`,`notIn()`).