From 3f1a52a15e10f8f234cf01358b2db1937cd17c67 Mon Sep 17 00:00:00 2001 From: IQBAL HASAN Date: Tue, 10 Mar 2026 11:56:04 +0600 Subject: [PATCH 1/5] Add EnumHelpers trait for utility methods on backed enums --- .gitignore | 2 + src/Concerns/EnumHelpers.php | 75 ++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 src/Concerns/EnumHelpers.php diff --git a/.gitignore b/.gitignore index 11b10e6..34bb581 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ testbench.yaml /docs /coverage auth.json + +.DS_Store diff --git a/src/Concerns/EnumHelpers.php b/src/Concerns/EnumHelpers.php new file mode 100644 index 0000000..633f6cb --- /dev/null +++ b/src/Concerns/EnumHelpers.php @@ -0,0 +1,75 @@ + label pairs. + * + * Calls the given method name on each case (defaults to "label"). + * Requires the enum to implement a public instance method with that name. + * + * @return array + */ + public static function options(string $label = 'label'): array + { + return collect(self::cases()) + ->mapWithKeys(fn (self $case): array => [$case->value => $case->{$label}()]) + ->all(); + } + + /** + * Get all enum cases as an array of {value, label} objects for frontend selects. + * + * @return array + */ + public static function selectOptions(string $label = 'label'): array + { + return collect(self::cases()) + ->map(fn (self $case): array => [ + 'value' => $case->value, + 'label' => $case->{$label}(), + ]) + ->values() + ->all(); + } + + /** + * Get all enum values as an array. + * + * @return array + */ + public static function values(): array + { + return array_column(self::cases(), 'value'); + } + + /** + * Get all enum names as an array. + * + * @return array + */ + public static function names(): array + { + return array_column(self::cases(), 'name'); + } + + /** + * Check if a value exists in this enum. + */ + public static function hasValue(string|int $value): bool + { + return in_array($value, self::values(), true); + } +} From d364368609e8b15d361a07add6ccf7b89c5998df Mon Sep 17 00:00:00 2001 From: IQBAL HASAN Date: Tue, 10 Mar 2026 11:59:42 +0600 Subject: [PATCH 2/5] Add ignoreErrors configuration for EnumHelpers trait in PHPStan --- phpstan.neon.dist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 4a0559d..cacf1ee 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -9,3 +9,5 @@ parameters: tmpDir: build/phpstan checkOctaneCompatibility: true checkModelProperties: true + ignoreErrors: + - '#Trait DevWizardHQ\\Enumify\\Concerns\\EnumHelpers is used zero times and is not analysed#' From 5acfc8430f783317101b21b8d767700186e45510 Mon Sep 17 00:00:00 2001 From: IQBAL HASAN Date: Tue, 10 Mar 2026 12:03:07 +0600 Subject: [PATCH 3/5] Add EnumHelpers trait to CampusStatus and PaymentMethod enums; create EnumHelpersTest for utility methods --- tests/Fixtures/CampusStatus.php | 3 ++ tests/Fixtures/MixedCaseStatus.php | 2 +- tests/Fixtures/PaymentMethod.php | 2 + tests/Unit/EnumHelpersTest.php | 79 ++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 tests/Unit/EnumHelpersTest.php diff --git a/tests/Fixtures/CampusStatus.php b/tests/Fixtures/CampusStatus.php index 14cedfa..89567a0 100644 --- a/tests/Fixtures/CampusStatus.php +++ b/tests/Fixtures/CampusStatus.php @@ -4,12 +4,15 @@ namespace DevWizardHQ\Enumify\Tests\Fixtures; +use DevWizardHQ\Enumify\Concerns\EnumHelpers; + /** * Fixture: Backed enum with custom methods (color, isActive, etc.). * This demonstrates full method extraction for TypeScript generation. */ enum CampusStatus: string { + use EnumHelpers; case ACTIVE = 'active'; case SUSPENDED = 'suspended'; case INACTIVE = 'inactive'; diff --git a/tests/Fixtures/MixedCaseStatus.php b/tests/Fixtures/MixedCaseStatus.php index 17f5869..91da290 100644 --- a/tests/Fixtures/MixedCaseStatus.php +++ b/tests/Fixtures/MixedCaseStatus.php @@ -18,4 +18,4 @@ public function isDefault(): bool { return $this === self::pending; } -} +} \ No newline at end of file diff --git a/tests/Fixtures/PaymentMethod.php b/tests/Fixtures/PaymentMethod.php index 59bda02..288ebdb 100644 --- a/tests/Fixtures/PaymentMethod.php +++ b/tests/Fixtures/PaymentMethod.php @@ -4,6 +4,7 @@ namespace DevWizardHQ\Enumify\Tests\Fixtures; +use DevWizardHQ\Enumify\Concerns\EnumHelpers; use DevWizardHQ\Enumify\Contracts\HasLabels; /** @@ -11,6 +12,7 @@ */ enum PaymentMethod: string implements HasLabels { + use EnumHelpers; case CREDIT_CARD = 'credit_card'; case DEBIT_CARD = 'debit_card'; case BANK_TRANSFER = 'bank_transfer'; diff --git a/tests/Unit/EnumHelpersTest.php b/tests/Unit/EnumHelpersTest.php new file mode 100644 index 0000000..cd96ebd --- /dev/null +++ b/tests/Unit/EnumHelpersTest.php @@ -0,0 +1,79 @@ + label pairs', function () { + $options = PaymentMethod::options(); + + expect($options)->toBe([ + 'credit_card' => 'Credit Card', + 'debit_card' => 'Debit Card', + 'bank_transfer' => 'Bank Transfer', + 'paypal' => 'PayPal', + 'crypto' => 'Cryptocurrency', + ]); +}); + +it('returns options using a custom method name', function () { + $options = CampusStatus::options('color'); + + expect($options)->toBe([ + 'active' => 'green', + 'suspended' => 'red', + 'inactive' => 'gray', + ]); +}); + +it('returns select options as value/label arrays', function () { + $options = PaymentMethod::selectOptions(); + + expect($options)->toBe([ + ['value' => 'credit_card', 'label' => 'Credit Card'], + ['value' => 'debit_card', 'label' => 'Debit Card'], + ['value' => 'bank_transfer', 'label' => 'Bank Transfer'], + ['value' => 'paypal', 'label' => 'PayPal'], + ['value' => 'crypto', 'label' => 'Cryptocurrency'], + ]); +}); + +it('returns select options using a custom method name', function () { + $options = CampusStatus::selectOptions('color'); + + expect($options)->toBe([ + ['value' => 'active', 'label' => 'green'], + ['value' => 'suspended', 'label' => 'red'], + ['value' => 'inactive', 'label' => 'gray'], + ]); +}); + +it('returns all enum values', function () { + expect(PaymentMethod::values())->toBe([ + 'credit_card', + 'debit_card', + 'bank_transfer', + 'paypal', + 'crypto', + ]); +}); + +it('returns all enum names', function () { + expect(PaymentMethod::names())->toBe([ + 'CREDIT_CARD', + 'DEBIT_CARD', + 'BANK_TRANSFER', + 'PAYPAL', + 'CRYPTO', + ]); +}); + +it('checks if a value exists', function () { + expect(PaymentMethod::hasValue('credit_card'))->toBeTrue(); + expect(PaymentMethod::hasValue('nonexistent'))->toBeFalse(); +}); + +it('uses strict comparison for hasValue', function () { + expect(PaymentMethod::hasValue(''))->toBeFalse(); +}); From eef03bcfb7ef27fc0cd95108362521564aac1acf Mon Sep 17 00:00:00 2001 From: IQBAL HASAN Date: Tue, 10 Mar 2026 12:06:08 +0600 Subject: [PATCH 4/5] Enhance EnumHelpers trait with fallback for missing label methods and add tests for humanized case names --- composer.json | 3 ++- src/Concerns/EnumHelpers.php | 27 +++++++++++++++++++++++---- tests/Fixtures/OrderStatus.php | 3 +++ tests/Unit/EnumHelpersTest.php | 25 +++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 0ab38b0..87088aa 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,8 @@ "php": "^8.2", "laravel/prompts": "^0.3", "spatie/laravel-package-tools": "^1.16", - "illuminate/contracts": "^10.0||^11.0||^12.0" + "illuminate/contracts": "^10.0||^11.0||^12.0", + "illuminate/support": "^10.0||^11.0||^12.0" }, "require-dev": { "laravel/pint": "^1.14", diff --git a/src/Concerns/EnumHelpers.php b/src/Concerns/EnumHelpers.php index 633f6cb..6b51a18 100644 --- a/src/Concerns/EnumHelpers.php +++ b/src/Concerns/EnumHelpers.php @@ -18,20 +18,29 @@ trait EnumHelpers * Get all enum cases as an array of value => label pairs. * * Calls the given method name on each case (defaults to "label"). - * Requires the enum to implement a public instance method with that name. + * Falls back to a humanized version of the case name when the method does not exist. + * + * Note: PHP coerces numeric-string keys to integers in arrays. + * Use {@see selectOptions()} if your enum has numeric-like string values. * * @return array */ public static function options(string $label = 'label'): array { return collect(self::cases()) - ->mapWithKeys(fn (self $case): array => [$case->value => $case->{$label}()]) + ->mapWithKeys(fn (self $case): array => [ + $case->value => method_exists($case, $label) + ? $case->{$label}() + : self::humanize($case->name), + ]) ->all(); } /** * Get all enum cases as an array of {value, label} objects for frontend selects. * + * Falls back to a humanized version of the case name when the method does not exist. + * * @return array */ public static function selectOptions(string $label = 'label'): array @@ -39,7 +48,9 @@ public static function selectOptions(string $label = 'label'): array return collect(self::cases()) ->map(fn (self $case): array => [ 'value' => $case->value, - 'label' => $case->{$label}(), + 'label' => method_exists($case, $label) + ? $case->{$label}() + : self::humanize($case->name), ]) ->values() ->all(); @@ -70,6 +81,14 @@ public static function names(): array */ public static function hasValue(string|int $value): bool { - return in_array($value, self::values(), true); + return self::tryFrom($value) !== null; + } + + /** + * Convert a SCREAMING_SNAKE_CASE name to a human-readable title. + */ + private static function humanize(string $name): string + { + return ucwords(strtolower(str_replace('_', ' ', $name))); } } diff --git a/tests/Fixtures/OrderStatus.php b/tests/Fixtures/OrderStatus.php index a00a044..683520c 100644 --- a/tests/Fixtures/OrderStatus.php +++ b/tests/Fixtures/OrderStatus.php @@ -4,11 +4,14 @@ namespace DevWizardHQ\Enumify\Tests\Fixtures; +use DevWizardHQ\Enumify\Concerns\EnumHelpers; + /** * Fixture: Backed enum with string values. */ enum OrderStatus: string { + use EnumHelpers; case PENDING = 'pending'; case PROCESSING = 'processing'; case SHIPPED = 'shipped'; diff --git a/tests/Unit/EnumHelpersTest.php b/tests/Unit/EnumHelpersTest.php index cd96ebd..9504b32 100644 --- a/tests/Unit/EnumHelpersTest.php +++ b/tests/Unit/EnumHelpersTest.php @@ -3,6 +3,7 @@ declare(strict_types=1); use DevWizardHQ\Enumify\Tests\Fixtures\CampusStatus; +use DevWizardHQ\Enumify\Tests\Fixtures\OrderStatus; use DevWizardHQ\Enumify\Tests\Fixtures\PaymentMethod; it('returns options as value => label pairs', function () { @@ -27,6 +28,18 @@ ]); }); +it('falls back to humanized case name when label method does not exist', function () { + $options = OrderStatus::options(); + + expect($options)->toBe([ + 'pending' => 'Pending', + 'processing' => 'Processing', + 'shipped' => 'Shipped', + 'delivered' => 'Delivered', + 'cancelled' => 'Cancelled', + ]); +}); + it('returns select options as value/label arrays', function () { $options = PaymentMethod::selectOptions(); @@ -49,6 +62,18 @@ ]); }); +it('falls back to humanized case name in select options when method does not exist', function () { + $options = OrderStatus::selectOptions(); + + expect($options)->toBe([ + ['value' => 'pending', 'label' => 'Pending'], + ['value' => 'processing', 'label' => 'Processing'], + ['value' => 'shipped', 'label' => 'Shipped'], + ['value' => 'delivered', 'label' => 'Delivered'], + ['value' => 'cancelled', 'label' => 'Cancelled'], + ]); +}); + it('returns all enum values', function () { expect(PaymentMethod::values())->toBe([ 'credit_card', From ac13c5810314ce84cf0f7eacc96bb98b0ec5a999 Mon Sep 17 00:00:00 2001 From: iqbalhasandev <39612205+iqbalhasandev@users.noreply.github.com> Date: Tue, 10 Mar 2026 06:07:00 +0000 Subject: [PATCH 5/5] Fix styling --- tests/Fixtures/MixedCaseStatus.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Fixtures/MixedCaseStatus.php b/tests/Fixtures/MixedCaseStatus.php index 91da290..17f5869 100644 --- a/tests/Fixtures/MixedCaseStatus.php +++ b/tests/Fixtures/MixedCaseStatus.php @@ -18,4 +18,4 @@ public function isDefault(): bool { return $this === self::pending; } -} \ No newline at end of file +}