Skip to content

Commit 0391d8c

Browse files
Merge pull request #13 from DevWizardHQ/feat/add-enum-helpers-concern
Add EnumHelpers trait for utility methods on backed enums
2 parents 64beddb + ac13c58 commit 0391d8c

8 files changed

Lines changed: 212 additions & 1 deletion

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,5 @@ testbench.yaml
3131
/docs
3232
/coverage
3333
auth.json
34+
35+
.DS_Store

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"php": "^8.2",
2222
"laravel/prompts": "^0.3",
2323
"spatie/laravel-package-tools": "^1.16",
24-
"illuminate/contracts": "^10.0||^11.0||^12.0"
24+
"illuminate/contracts": "^10.0||^11.0||^12.0",
25+
"illuminate/support": "^10.0||^11.0||^12.0"
2526
},
2627
"require-dev": {
2728
"laravel/pint": "^1.14",

phpstan.neon.dist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ parameters:
99
tmpDir: build/phpstan
1010
checkOctaneCompatibility: true
1111
checkModelProperties: true
12+
ignoreErrors:
13+
- '#Trait DevWizardHQ\\Enumify\\Concerns\\EnumHelpers is used zero times and is not analysed#'

src/Concerns/EnumHelpers.php

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DevWizardHQ\Enumify\Concerns;
6+
7+
/**
8+
* Provides common utility methods for backed enums.
9+
*
10+
* Use this trait on any string-backed or int-backed enum to gain
11+
* helpers for listing options, values, names, and checking existence.
12+
*
13+
* @mixin \BackedEnum
14+
*/
15+
trait EnumHelpers
16+
{
17+
/**
18+
* Get all enum cases as an array of value => label pairs.
19+
*
20+
* Calls the given method name on each case (defaults to "label").
21+
* Falls back to a humanized version of the case name when the method does not exist.
22+
*
23+
* Note: PHP coerces numeric-string keys to integers in arrays.
24+
* Use {@see selectOptions()} if your enum has numeric-like string values.
25+
*
26+
* @return array<string|int, string>
27+
*/
28+
public static function options(string $label = 'label'): array
29+
{
30+
return collect(self::cases())
31+
->mapWithKeys(fn (self $case): array => [
32+
$case->value => method_exists($case, $label)
33+
? $case->{$label}()
34+
: self::humanize($case->name),
35+
])
36+
->all();
37+
}
38+
39+
/**
40+
* Get all enum cases as an array of {value, label} objects for frontend selects.
41+
*
42+
* Falls back to a humanized version of the case name when the method does not exist.
43+
*
44+
* @return array<int, array{value: string|int, label: string}>
45+
*/
46+
public static function selectOptions(string $label = 'label'): array
47+
{
48+
return collect(self::cases())
49+
->map(fn (self $case): array => [
50+
'value' => $case->value,
51+
'label' => method_exists($case, $label)
52+
? $case->{$label}()
53+
: self::humanize($case->name),
54+
])
55+
->values()
56+
->all();
57+
}
58+
59+
/**
60+
* Get all enum values as an array.
61+
*
62+
* @return array<int, string|int>
63+
*/
64+
public static function values(): array
65+
{
66+
return array_column(self::cases(), 'value');
67+
}
68+
69+
/**
70+
* Get all enum names as an array.
71+
*
72+
* @return array<int, string>
73+
*/
74+
public static function names(): array
75+
{
76+
return array_column(self::cases(), 'name');
77+
}
78+
79+
/**
80+
* Check if a value exists in this enum.
81+
*/
82+
public static function hasValue(string|int $value): bool
83+
{
84+
return self::tryFrom($value) !== null;
85+
}
86+
87+
/**
88+
* Convert a SCREAMING_SNAKE_CASE name to a human-readable title.
89+
*/
90+
private static function humanize(string $name): string
91+
{
92+
return ucwords(strtolower(str_replace('_', ' ', $name)));
93+
}
94+
}

tests/Fixtures/CampusStatus.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44

55
namespace DevWizardHQ\Enumify\Tests\Fixtures;
66

7+
use DevWizardHQ\Enumify\Concerns\EnumHelpers;
8+
79
/**
810
* Fixture: Backed enum with custom methods (color, isActive, etc.).
911
* This demonstrates full method extraction for TypeScript generation.
1012
*/
1113
enum CampusStatus: string
1214
{
15+
use EnumHelpers;
1316
case ACTIVE = 'active';
1417
case SUSPENDED = 'suspended';
1518
case INACTIVE = 'inactive';

tests/Fixtures/OrderStatus.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44

55
namespace DevWizardHQ\Enumify\Tests\Fixtures;
66

7+
use DevWizardHQ\Enumify\Concerns\EnumHelpers;
8+
79
/**
810
* Fixture: Backed enum with string values.
911
*/
1012
enum OrderStatus: string
1113
{
14+
use EnumHelpers;
1215
case PENDING = 'pending';
1316
case PROCESSING = 'processing';
1417
case SHIPPED = 'shipped';

tests/Fixtures/PaymentMethod.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44

55
namespace DevWizardHQ\Enumify\Tests\Fixtures;
66

7+
use DevWizardHQ\Enumify\Concerns\EnumHelpers;
78
use DevWizardHQ\Enumify\Contracts\HasLabels;
89

910
/**
1011
* Fixture: Backed enum with per-case label() method.
1112
*/
1213
enum PaymentMethod: string implements HasLabels
1314
{
15+
use EnumHelpers;
1416
case CREDIT_CARD = 'credit_card';
1517
case DEBIT_CARD = 'debit_card';
1618
case BANK_TRANSFER = 'bank_transfer';

tests/Unit/EnumHelpersTest.php

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use DevWizardHQ\Enumify\Tests\Fixtures\CampusStatus;
6+
use DevWizardHQ\Enumify\Tests\Fixtures\OrderStatus;
7+
use DevWizardHQ\Enumify\Tests\Fixtures\PaymentMethod;
8+
9+
it('returns options as value => label pairs', function () {
10+
$options = PaymentMethod::options();
11+
12+
expect($options)->toBe([
13+
'credit_card' => 'Credit Card',
14+
'debit_card' => 'Debit Card',
15+
'bank_transfer' => 'Bank Transfer',
16+
'paypal' => 'PayPal',
17+
'crypto' => 'Cryptocurrency',
18+
]);
19+
});
20+
21+
it('returns options using a custom method name', function () {
22+
$options = CampusStatus::options('color');
23+
24+
expect($options)->toBe([
25+
'active' => 'green',
26+
'suspended' => 'red',
27+
'inactive' => 'gray',
28+
]);
29+
});
30+
31+
it('falls back to humanized case name when label method does not exist', function () {
32+
$options = OrderStatus::options();
33+
34+
expect($options)->toBe([
35+
'pending' => 'Pending',
36+
'processing' => 'Processing',
37+
'shipped' => 'Shipped',
38+
'delivered' => 'Delivered',
39+
'cancelled' => 'Cancelled',
40+
]);
41+
});
42+
43+
it('returns select options as value/label arrays', function () {
44+
$options = PaymentMethod::selectOptions();
45+
46+
expect($options)->toBe([
47+
['value' => 'credit_card', 'label' => 'Credit Card'],
48+
['value' => 'debit_card', 'label' => 'Debit Card'],
49+
['value' => 'bank_transfer', 'label' => 'Bank Transfer'],
50+
['value' => 'paypal', 'label' => 'PayPal'],
51+
['value' => 'crypto', 'label' => 'Cryptocurrency'],
52+
]);
53+
});
54+
55+
it('returns select options using a custom method name', function () {
56+
$options = CampusStatus::selectOptions('color');
57+
58+
expect($options)->toBe([
59+
['value' => 'active', 'label' => 'green'],
60+
['value' => 'suspended', 'label' => 'red'],
61+
['value' => 'inactive', 'label' => 'gray'],
62+
]);
63+
});
64+
65+
it('falls back to humanized case name in select options when method does not exist', function () {
66+
$options = OrderStatus::selectOptions();
67+
68+
expect($options)->toBe([
69+
['value' => 'pending', 'label' => 'Pending'],
70+
['value' => 'processing', 'label' => 'Processing'],
71+
['value' => 'shipped', 'label' => 'Shipped'],
72+
['value' => 'delivered', 'label' => 'Delivered'],
73+
['value' => 'cancelled', 'label' => 'Cancelled'],
74+
]);
75+
});
76+
77+
it('returns all enum values', function () {
78+
expect(PaymentMethod::values())->toBe([
79+
'credit_card',
80+
'debit_card',
81+
'bank_transfer',
82+
'paypal',
83+
'crypto',
84+
]);
85+
});
86+
87+
it('returns all enum names', function () {
88+
expect(PaymentMethod::names())->toBe([
89+
'CREDIT_CARD',
90+
'DEBIT_CARD',
91+
'BANK_TRANSFER',
92+
'PAYPAL',
93+
'CRYPTO',
94+
]);
95+
});
96+
97+
it('checks if a value exists', function () {
98+
expect(PaymentMethod::hasValue('credit_card'))->toBeTrue();
99+
expect(PaymentMethod::hasValue('nonexistent'))->toBeFalse();
100+
});
101+
102+
it('uses strict comparison for hasValue', function () {
103+
expect(PaymentMethod::hasValue(''))->toBeFalse();
104+
});

0 commit comments

Comments
 (0)