Skip to content

Commit b110466

Browse files
authored
Merge pull request #5 from Carnicero90/main
fix: error on null value in EnumFrom@wrap in strict mode
2 parents 52a5136 + d9524eb commit b110466

4 files changed

Lines changed: 71 additions & 5 deletions

File tree

README.md

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,56 @@ StringBackedEnum::tryFromName('PENDING'); // StringBackedEnum::PENDING
204204
StringBackedEnum::tryFromName('MISSING'); // null
205205
```
206206

207+
#### `wrap()`
208+
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.
209+
210+
Signature:
211+
```php
212+
public static function wrap(self|string|int|null $value, bool $strict = false): ?self
213+
```
214+
215+
Behavior notes:
216+
- If an actual enum instance (same enum) is passed, it is returned unchanged.
217+
- If null is passed, the method throws an error if `$strict = true`, otherwise it returns null.
218+
- If a backing value is passed (int or string), the method attempts to resolve it using `tryFrom()` (for BackedEnum) or `tryFromName()` (for pure enums).
219+
- For int-backed enums, numeric strings (e.g. `'1'`) are accepted: they are converted to integer and processed as numeric backing values.
220+
- When a string is passed and no backing match is found, `wrap()` will try to resolve it as a case name via `tryFromName()`.
221+
- 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.
222+
223+
Examples:
224+
```php
225+
// Passing an enum instance returns it unchanged
226+
$enum = IntBackedEnum::PENDING;
227+
IntBackedEnum::wrap($enum); // IntBackedEnum::PENDING
228+
229+
// Null remains null
230+
IntBackedEnum::wrap(null); // null
231+
IntBackedEnum::wrap(null, true); // throws ValueError
232+
233+
// Backed enum by native backing value
234+
IntBackedEnum::wrap(1); // IntBackedEnum::ACCEPTED
235+
StringBackedEnum::wrap('P'); // StringBackedEnum::PENDING
236+
237+
// Numeric string for int-backed enum
238+
IntBackedEnum::wrap('1'); // IntBackedEnum::ACCEPTED
239+
240+
// Case name (pure enum)
241+
PureEnum::wrap('PENDING'); // PureEnum::PENDING
242+
243+
// Case name (backed enum)
244+
StringBackedEnum::wrap('PENDING'); // StringBackedEnum::PENDING
245+
246+
// Non matching values
247+
PureEnum::wrap('MISSING'); // null
248+
PureEnum::wrap('MISSING', true); // throws ValueError: '"MISSING" is not a valid backing value for enum "Namespace\PureEnum"'
249+
```
250+
251+
Notes on error message:
252+
- When `$strict` is true and no match is found, `wrap()` throws a `ValueError` with a message similar to:
253+
'"<value>" is not a valid backing value for enum "<FullyQualifiedClassName>"'
254+
255+
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.
256+
207257
### Inspection
208258
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()`).
209259

@@ -248,7 +298,7 @@ StringBackedEnum::hasName('A') // false
248298

249299
#### `hasValue()` and `doesntHaveValue()`
250300
`hasValue()` method permit checking if an enum has a case by passing int, string or enum instance.
251-
For convenience, there is also an `doesntHaveValue()` method which is the exact reverse of the `hasValue()` method.
301+
For convenience, there is also a `doesntHaveValue()` method which is the exact reverse of the `hasValue()` method.
252302

253303
```php
254304
PureEnum::hasValue('PENDING') // true
@@ -257,7 +307,7 @@ IntBackedEnum::hasValue('ACCEPTED') // false
257307
IntBackedEnum::hasValue(1) // true
258308
StringBackedEnum::doesntHaveValue('Z') // true
259309
StringBackedEnum::hasValue('A') // true
260-
````
310+
```
261311

262312
### Equality
263313
This helper permits to compare an enum instance (`is()`,`isNot()`) and search if it is present inside an array (`in()`,`notIn()`).
@@ -349,7 +399,7 @@ StringBackedEnum::values([StringBackedEnum::NO_RESPONSE, StringBackedEnum::DISCA
349399
IntBackedEnum::values([IntBackedEnum::NO_RESPONSE, IntBackedEnum::DISCARDED]); // [3, 2]
350400
```
351401
#### `valuesByName()`
352-
This method returns a associative array of [name => value] on `BackedEnum`, [name => name] on pure enum.
402+
This method returns an associative array of [name => value] on `BackedEnum`, [name => name] on pure enum.
353403
```php
354404
PureEnum::valuesByName(); // ['PENDING' => 'PENDING','ACCEPTED' => 'ACCEPTED','DISCARDED' => 'DISCARDED',...]
355405
StringBackedEnum::valuesByName(); // ['PENDING' => 'P','ACCEPTED' => 'A','DISCARDED' => 'D','NO_RESPONSE' => 'N']

composer.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
"test:types": "vendor/bin/phpstan analyse --ansi",
4545
"test:unit": "vendor/bin/pest --colors=always",
4646
"test": [
47-
"@test:lint",
4847
"@test:types",
4948
"@test:unit"
5049
],

src/Traits/EnumFrom.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,17 @@ trait EnumFrom
2222
*/
2323
public static function wrap(self|string|int|null $value, bool $strict = false): ?self
2424
{
25-
if ($value instanceof self || is_null($value)) {
25+
if ($value instanceof self) {
2626
return $value;
2727
}
2828

29+
if (is_null($value)) {
30+
if ($strict) {
31+
throw new ValueError('"'.$value.'" is not a valid backing value for enum "'.self::class.'"');
32+
}
33+
return null;
34+
}
35+
2936
$enum = null;
3037
if (is_string($value) && self::isIntBacked()) {
3138
if (is_numeric($value)) {

tests/EnumFromTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,21 @@
5151
->toThrow(ValueError::class);
5252
});
5353

54+
it('throws ValueError for null value in strict mode', function () {
55+
expect(fn () => StringBackedEnum::wrap(null, true))
56+
->toThrow(ValueError::class);
57+
});
58+
5459
it('returns null for invalid backing value when not in strict mode', function () {
5560
expect(StringBackedEnum::wrap('non-existent-value'))
5661
->toBeNull();
5762
});
5863

64+
it('returns null for null value when not in strict mode', function () {
65+
expect(StringBackedEnum::wrap(null))
66+
->toBeNull();
67+
});
68+
5969
it('does work with tryFrom method', function ($enumCass, $value, $result) {
6070
expect($enumCass::tryFrom($value))->toBe($result)->not->toThrow(ValueError::class);
6171
})->with([

0 commit comments

Comments
 (0)