Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ testbench.yaml
/docs
/coverage
auth.json

.DS_Store
390 changes: 390 additions & 0 deletions resources/boost/skills/enumify/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,390 @@
---
name: enumify
description: Generate TypeScript enums from PHP enums, sync enum values between backend and frontend, and refactor hardcoded enum strings.
---

# Enumify — PHP-to-TypeScript Enum Sync

## When to use this skill

Activate this skill when:
- Creating or modifying PHP enums in `app/Enums/`
- A model uses enum casting
- Building frontend UI that displays enum values, labels, colors, or badges
- Comparing or filtering by enum values in queries or Blade templates
- Writing TypeScript/JavaScript that references status, type, role, or category values

## Strict Rules

ALWAYS follow these rules when implementing with Enumify:

### Backend Rules

1. **ALWAYS place enums in `app/Enums/`**. This is the scanned directory. Enums elsewhere will not be discovered.

2. **ALWAYS use `SCREAMING_SNAKE_CASE` for case names**:
```php
// CORRECT
case PENDING_PAYMENT = 'pending_payment';
case IN_PROGRESS = 'in_progress';

// WRONG — will cause inconsistency with TypeScript output
case pendingPayment = 'pending_payment';
case Pending = 'pending';
```

3. **ALWAYS use string-backed enums** when the value will be stored in a database or sent to a frontend. Use integer-backed enums only for numeric codes. Use unit enums only for purely internal logic.

4. **ALWAYS cast enums in models** when a column stores an enum value:
```php
// In your model
protected function casts(): array
{
return [
'status' => OrderStatus::class,
'role' => UserRole::class,
];
}
```

5. **NEVER hardcode enum string values** in queries, comparisons, or assignments. Always reference the enum class:
```php
// CORRECT
$orders = Order::where('status', OrderStatus::PENDING)->get();
$order->status = OrderStatus::SHIPPED;
if ($order->status === OrderStatus::DELIVERED) { ... }

// WRONG — defeats the purpose of enums
$orders = Order::where('status', 'pending')->get();
$order->status = 'shipped';
if ($order->status === 'delivered') { ... }
```

6. **NEVER hardcode enum values in validation**. Use `Rule::enum()`:
```php
// CORRECT
'status' => ['required', Rule::enum(OrderStatus::class)],

// WRONG
'status' => ['required', Rule::in(['pending', 'shipped', 'delivered'])],
```

7. **ALWAYS implement `HasLabels`** when an enum needs human-readable display text. Use `match()` for the `label()` method:
```php
use DevWizardHQ\Enumify\Concerns\EnumHelpers;
use DevWizardHQ\Enumify\Contracts\HasLabels;

enum OrderStatus: string implements HasLabels
{
use EnumHelpers;

case PENDING = 'pending';
case IN_PROGRESS = 'in_progress';
case SHIPPED = 'shipped';

public function label(): string
{
return match ($this) {
self::PENDING => 'Pending',
self::IN_PROGRESS => 'In Progress',
self::SHIPPED => 'Shipped',
};
}
}
```

8. **ALWAYS cover every case** in `match()` for `label()` and custom methods. Missing cases will cause runtime errors and break TypeScript generation.

9. **Custom methods MUST follow these constraints** or they will be silently skipped during TypeScript generation:
- MUST be `public` (not `protected` or `private`)
- MUST NOT be `static`
- MUST have zero required parameters
- MUST declare a return type
- Return type MUST be `string`, `int`, `float`, `bool`, `null`, or nullable/union of these
- MUST NOT throw exceptions on any case

10. **ALWAYS use the `EnumHelpers` trait** on backed enums to get utility methods for free:
```php
use DevWizardHQ\Enumify\Concerns\EnumHelpers;

enum OrderStatus: string implements HasLabels
{
use EnumHelpers;

case PENDING = 'pending';
case SHIPPED = 'shipped';
// ...
}
```

The trait provides these static methods:
- `OrderStatus::options()` — returns `['pending' => 'Pending', 'shipped' => 'Shipped']` (value => label pairs, uses the `label()` method by default)
- `OrderStatus::options('color')` — same format but calls `color()` instead of `label()`
- `OrderStatus::selectOptions()` — returns `[['value' => 'pending', 'label' => 'Pending'], ...]` (ideal for frontend `<select>` dropdowns via Inertia)
- `OrderStatus::values()` — returns `['pending', 'shipped']` (all raw values)
- `OrderStatus::names()` — returns `['PENDING', 'SHIPPED']` (all case names)
- `OrderStatus::hasValue('pending')` — returns `true` if the value exists in the enum

Use these helpers instead of manually looping over `::cases()`:
```php
// CORRECT — use the trait helpers
$options = OrderStatus::options();
$selectData = OrderStatus::selectOptions();
if (OrderStatus::hasValue($input)) { ... }
$validValues = OrderStatus::values();

// WRONG — manual loops that the trait already handles
$options = [];
foreach (OrderStatus::cases() as $case) {
$options[$case->value] = $case->label();
}
```

11. **ALWAYS run `php artisan enumify:sync`** after creating or modifying any enum to regenerate TypeScript files.

### Frontend Rules

12. **NEVER manually create TypeScript enum files**. They are auto-generated in `resources/js/enums/` and will be overwritten.

13. **ALWAYS import from the barrel file** `@/enums` (maps to `resources/js/enums/index.ts`):
```typescript
// CORRECT
import { OrderStatus, OrderStatusUtils } from '@/enums';

// WRONG — bypasses barrel file, fragile path
import { OrderStatus } from '@/enums/order-status';
```

14. **ALWAYS use the const object for comparisons**, never raw string values:
```typescript
// CORRECT
if (order.status === OrderStatus.PENDING) { ... }

// WRONG — hardcoded string, no type safety
if (order.status === 'pending') { ... }
```

15. **ALWAYS use `Utils` methods** for display values (labels, colors, badges). Never duplicate these mappings in frontend code:
```typescript
// CORRECT
const label = OrderStatusUtils.label(order.status);
const color = OrderStatusUtils.color(order.status);
const allOptions = OrderStatusUtils.options();

// WRONG — duplicates backend logic, goes out of sync
const label = order.status === 'pending' ? 'Pending' : 'Shipped';
```

16. **When localization is enabled** (`react` or `vue` mode), ALWAYS use the hook function instead of the static Utils:
```typescript
// With localization.mode = 'react'
const { label, color, options } = useOrderStatusUtils();

// With localization.mode = 'vue'
const { label, color, options } = useOrderStatusUtils();
```
Comment on lines +178 to +185
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule 16 in the documentation shows identical code snippets for both react and vue localization modes. The React and Vue hook usage patterns are identical, which is either intentional (and should be noted in the doc) or a copy-paste error where the Vue example should demonstrate a Vue-specific usage pattern (e.g. using ref / computed if the hook returns reactive values, or showing the Composition API style). This makes the section misleading to developers who need to understand the difference between the two modes.

Copilot uses AI. Check for mistakes.

17. **ALWAYS use `options()` to populate `<select>` dropdowns and filter lists**, never manually define option arrays:
```tsx
// CORRECT — React example
<select>
{OrderStatusUtils.options().map(status => (
<option key={status} value={status}>
{OrderStatusUtils.label(status)}
</option>
))}
</select>

// CORRECT — Vue example
<select>
<option v-for="status in OrderStatusUtils.options()" :key="status" :value="status">
{{ OrderStatusUtils.label(status) }}
</option>
</select>
```

## Complete Backend Implementation Pattern

When creating a new feature that uses an enum, follow this exact sequence:

### Step 1: Create the enum

```php
// app/Enums/TaskPriority.php
namespace App\Enums;

use DevWizardHQ\Enumify\Concerns\EnumHelpers;
use DevWizardHQ\Enumify\Contracts\HasLabels;

enum TaskPriority: string implements HasLabels
{
use EnumHelpers;

case LOW = 'low';
case MEDIUM = 'medium';
case HIGH = 'high';
case URGENT = 'urgent';

public function label(): string
{
return match ($this) {
self::LOW => 'Low',
self::MEDIUM => 'Medium',
self::HIGH => 'High',
self::URGENT => 'Urgent',
};
}

public function color(): string
{
return match ($this) {
self::LOW => 'gray',
self::MEDIUM => 'blue',
self::HIGH => 'orange',
self::URGENT => 'red',
};
}

public function isEscalated(): bool
{
return in_array($this, [self::HIGH, self::URGENT]);
Comment on lines +248 to +250
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isEscalated() example in the documentation calls in_array($this, [self::HIGH, self::URGENT]) without the strict (true) third argument. This is inconsistent with the codebase's own hasValue() in the trait (line 73), which correctly uses true for strict comparison, and could mislead developers into using loose comparisons when implementing similar methods.

Copilot uses AI. Check for mistakes.
}
}
```

### Step 2: Cast in the model

```php
// app/Models/Task.php
protected function casts(): array
{
return [
'priority' => TaskPriority::class,
];
}
```

### Step 3: Use enum references everywhere in backend

```php
// Migration
$table->string('priority')->default(TaskPriority::LOW->value);

// Controller
$request->validate([
'priority' => ['required', Rule::enum(TaskPriority::class)],
]);

// Query scopes
Task::where('priority', TaskPriority::URGENT)->get();

// Blade
<span class="text-{{ $task->priority->color() }}">
{{ $task->priority->label() }}
</span>
```

### Step 4: Sync to TypeScript

```bash
php artisan enumify:sync
```

### Step 5: Use in frontend

```typescript
import { TaskPriority, TaskPriorityUtils } from '@/enums';

// Display
const label = TaskPriorityUtils.label(task.priority);
const color = TaskPriorityUtils.color(task.priority);

// Filter
const urgentTasks = tasks.filter(t => t.priority === TaskPriority.URGENT);

// Select dropdown
TaskPriorityUtils.options().map(p => ({
value: p,
label: TaskPriorityUtils.label(p),
}));
```

## Configuration Reference

```php
// config/enumify.php
return [
'paths' => [
'enums' => ['app/Enums'], // Directories scanned for PHP enums
'models' => ['app/Models'], // Directories scanned for model casts (refactor command)
'output' => 'resources/js/enums', // TypeScript output directory
],
'naming' => [
'file_case' => 'kebab', // 'kebab' | 'camel' | 'pascal' | 'snake'
],
'features' => [
'generate_union_types' => true,
'generate_label_maps' => true,
'generate_method_maps' => true,
'generate_index_barrel' => true,
],
'localization' => [
'mode' => 'none', // 'none' | 'react' | 'vue'
],
'filters' => [
'include' => [],
'exclude' => [],
],
];
```

## Artisan Commands

```bash
php artisan enumify:sync # Sync all enums to TypeScript
php artisan enumify:sync --force # Force regenerate all files
php artisan enumify:sync --dry-run # Preview without writing
php artisan enumify:sync --only="App\Enums\TaskPriority" # Sync a single enum
php artisan enumify:refactor --dry-run # Scan for hardcoded enum strings
php artisan enumify:refactor --fix # Auto-fix hardcoded strings
php artisan enumify:refactor --fix --backup # Fix with backup
php artisan enumify:refactor --normalize-keys # Normalize case names to UPPERCASE
```

## Generated TypeScript Structure

Each enum generates a file with three exports — the const object, the union type, and the Utils object:

```typescript
// AUTO-GENERATED — DO NOT EDIT MANUALLY
export const TaskPriority = {
LOW: "low",
MEDIUM: "medium",
HIGH: "high",
URGENT: "urgent",
} as const;

export type TaskPriority = typeof TaskPriority[keyof typeof TaskPriority];

export const TaskPriorityUtils = {
label(priority: TaskPriority): string { ... },
color(priority: TaskPriority): string { ... },
isEscalated(priority: TaskPriority): boolean { ... },
options(): TaskPriority[] { return Object.values(TaskPriority); },
};
```

## Common Anti-Patterns to Avoid

| Anti-Pattern | Correct Pattern |
|---|---|
| `->where('status', 'pending')` | `->where('status', OrderStatus::PENDING)` |
| `'status' => 'active'` in factories/seeders | `'status' => UserStatus::ACTIVE` |
| `Rule::in(['low', 'medium', 'high'])` | `Rule::enum(TaskPriority::class)` |
| `if ($status === 'pending')` in TS/JS | `if (status === OrderStatus.PENDING)` |
| Manually typing label maps in frontend | Use `OrderStatusUtils.label(status)` |
| Defining `['Pending', 'Active']` option arrays | Use `OrderStatusUtils.options()` |
| Creating `.ts` files in `resources/js/enums/` manually | Run `php artisan enumify:sync` |
| `foreach (Enum::cases() as $c) { $opts[$c->value] = $c->label(); }` | `Enum::options()` via `EnumHelpers` trait |
| `array_column(Enum::cases(), 'value')` | `Enum::values()` via `EnumHelpers` trait |
| Manual `in_array` check against enum values | `Enum::hasValue($value)` via `EnumHelpers` trait |
Loading
Loading