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/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/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#' diff --git a/src/Concerns/EnumHelpers.php b/src/Concerns/EnumHelpers.php new file mode 100644 index 0000000..6b51a18 --- /dev/null +++ b/src/Concerns/EnumHelpers.php @@ -0,0 +1,94 @@ + label pairs. + * + * Calls the given method name on each case (defaults to "label"). + * 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 => 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 + { + return collect(self::cases()) + ->map(fn (self $case): array => [ + 'value' => $case->value, + 'label' => method_exists($case, $label) + ? $case->{$label}() + : self::humanize($case->name), + ]) + ->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 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/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/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/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..9504b32 --- /dev/null +++ b/tests/Unit/EnumHelpersTest.php @@ -0,0 +1,104 @@ + 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('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(); + + 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('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', + '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(); +});