From 0f0bcb1a0226e951950dcd16a2df93cd888ac967 Mon Sep 17 00:00:00 2001 From: IQBAL HASAN Date: Sun, 25 Jan 2026 13:21:27 +0600 Subject: [PATCH 1/2] Refactor tests for enumify: enhance Order model setup and improve path handling - Added a temporary Order model with enum cast for testing. - Created a dedicated Models directory for better organization. - Updated file path handling to use consistent concatenation style. - Improved test cases for enum refactoring, including handling of edge cases and backup functionality. - Ensured proper cleanup of temporary files after tests. - Fixed missing newline at the end of MixedCaseStatus.php file. --- README.md | 5 +- config/enumify.php | 3 + src/Commands/RefactorCommand.php | 408 +++++++++++++++++++++----- tests/Feature/RefactorCommandTest.php | 128 ++++---- tests/Fixtures/MixedCaseStatus.php | 2 +- 5 files changed, 412 insertions(+), 134 deletions(-) diff --git a/README.md b/README.md index 5205132..c6d58db 100644 --- a/README.md +++ b/README.md @@ -346,7 +346,7 @@ php artisan enumify:sync --quiet ### enumify:refactor -Scan your codebase for hardcoded enum values and refactor them to use proper enum references. This command also supports normalizing enum case names to UPPERCASE and updating all references throughout your application. +Scan your codebase for hardcoded enum values and refactor them to use proper enum references. Only columns with enum casts in models will be refactored. This command also supports normalizing enum case names to UPPERCASE and updating all references throughout your application. #### Available Options @@ -361,7 +361,6 @@ Scan your codebase for hardcoded enum values and refactor them to use proper enu | `--backup` | | Create backups before applying changes | | `--include=` | | File patterns to include (e.g., `*.php`) | | `--exclude=` | | Paths or patterns to exclude from scanning | -| `--strict` | | Strict matching (column name must match enum context) | | `--report=` | | Export report to file (formats: `json`, `csv`, `md`) | | `--detailed` | | Show detailed output with code context | | `--normalize-keys` | | Convert enum keys to UPPERCASE and fix all references | @@ -388,6 +387,8 @@ php artisan enumify:refactor --path=app/Services php artisan enumify:refactor --report=refactor-report.md ``` +**Note:** The refactor command only processes columns that have an enum cast defined in a model. If no cast is available for a column, it will skip refactoring for that column. + #### Key Normalization (UPPERCASE) The `--normalize-keys` flag converts enum case names from any case format to UPPERCASE and updates all references in your codebase: diff --git a/config/enumify.php b/config/enumify.php index f9b1454..8db2c66 100644 --- a/config/enumify.php +++ b/config/enumify.php @@ -14,6 +14,9 @@ 'paths' => [ // Directories to scan for PHP enums (relative to base_path) 'enums' => ['app/Enums'], + // Directories to scan for Laravel models (relative to base_path) + // Used by enumify:refactor to detect enum casts + 'models' => ['app/Models'], // Output directory for generated TypeScript files (relative to base_path) 'output' => 'resources/js/enums', ], diff --git a/src/Commands/RefactorCommand.php b/src/Commands/RefactorCommand.php index 8bae29e..e7c6da7 100644 --- a/src/Commands/RefactorCommand.php +++ b/src/Commands/RefactorCommand.php @@ -38,7 +38,6 @@ final class RefactorCommand extends Command {--backup : Create backup before applying changes} {--include=* : File patterns to include (e.g., *.php)} {--exclude=* : Paths/patterns to exclude} - {--strict : Strict matching (requires column name to match enum context)} {--report= : Export report to file (formats: json, csv, md)} {--detailed : Show detailed output with code context} {--normalize-keys : Convert enum keys to UPPERCASE and fix all references}'; @@ -48,7 +47,7 @@ final class RefactorCommand extends Command * * @var string */ - protected $description = 'Scan and refactor hardcoded enum values. Supports dry-run, interactive mode, key normalization, and comprehensive reporting.'; + protected $description = 'Scan and refactor hardcoded enum values based on model casts. Only columns with enum casts will be refactored.'; /** * @var array, class: string, path: string}> @@ -56,7 +55,7 @@ final class RefactorCommand extends Command private array $enums = []; /** - * @var array + * @var array */ private array $issues = []; @@ -70,6 +69,13 @@ final class RefactorCommand extends Command */ private array $backups = []; + /** + * Model casts mapping: model class => [column => enum class] + * + * @var array> + */ + private array $modelCasts = []; + /** * Patterns to detect hardcoded enum values. * @@ -79,8 +85,6 @@ final class RefactorCommand extends Command 'where' => '/->where\([\'"](\w+)[\'"]\s*,\s*[\'"]([a-zA-Z0-9_-]+)[\'"]\)/', 'orWhere' => '/->orWhere\([\'"](\w+)[\'"]\s*,\s*[\'"]([a-zA-Z0-9_-]+)[\'"]\)/', 'whereNot' => '/->whereNot\([\'"](\w+)[\'"]\s*,\s*[\'"]([a-zA-Z0-9_-]+)[\'"]\)/', - 'update' => '/->update\(\[[\'"](\w+)[\'"]\s*=>\s*[\'"]([a-zA-Z0-9_-]+)[\'"]/', - 'create' => '/->create\(\[[\'"](\w+)[\'"]\s*=>\s*[\'"]([a-zA-Z0-9_-]+)[\'"]/', 'array' => '/[\'"](status|type|state|category|priority|role|level|method|direction|source|channel)[\'"]\s*=>\s*[\'"]([a-zA-Z0-9_-]+)[\'"]/', 'comparison' => '/\$\w+->(status|type|state|category|priority|role|level)\s*===?\s*[\'"]([a-zA-Z0-9_-]+)[\'"]/', 'validation' => '/Rule::in\(\[([^\]]+)\]\)/', @@ -125,6 +129,8 @@ public function handle(): int return self::FAILURE; } + $this->loadModelCasts(); + // --fix applies changes, --dry-run previews changes, default is scan if ($isFix) { return $this->fix(dryRun: false); @@ -166,6 +172,8 @@ private function runInteractive(): int return self::FAILURE; } + $this->loadModelCasts(); + // Select mode $mode = select( label: 'What would you like to do?', @@ -398,7 +406,7 @@ private function loadEnums(): void } $count = count($this->enums); - $this->info("✅ Loaded {$count} enum".($count !== 1 ? 's' : '')); + $this->info("✅ Loaded {$count} enum" . ($count !== 1 ? 's' : '')); $this->newLine(); } @@ -407,6 +415,198 @@ private function loadEnumsWithPaths(): void $this->loadEnums(); } + /** + * Load model casts to determine which columns have enum casts. + */ + private function loadModelCasts(): void + { + /** @var array $modelPaths */ + $modelPaths = config('enumify.paths.models', ['app/Models']); + + $this->info('📦 Loading model casts...'); + + foreach ($modelPaths as $path) { + $fullPath = $this->isAbsolutePath($path) ? $path : base_path($path); + + // @codeCoverageIgnoreStart + if (! is_dir($fullPath)) { + continue; + } + // @codeCoverageIgnoreEnd + + $files = File::allFiles($fullPath); + + foreach ($files as $file) { + // @codeCoverageIgnoreStart + if ($file->getExtension() !== 'php') { + continue; + } + // @codeCoverageIgnoreEnd + + $this->extractModelCasts($file->getPathname()); + } + } + + $castCount = array_sum(array_map('count', $this->modelCasts)); + $modelCount = count($this->modelCasts); + $this->info("✅ Found {$castCount} enum cast" . ($castCount !== 1 ? 's' : '') . " in {$modelCount} model" . ($modelCount !== 1 ? 's' : '')); + $this->newLine(); + } + + /** + * Extract casts from a model file. + */ + private function extractModelCasts(string $filePath): void + { + $content = file_get_contents($filePath); + + // Get the model class name + $className = $this->getModelClassFromFile($filePath); + // @codeCoverageIgnoreStart + if (! $className) { + return; + } + // @codeCoverageIgnoreEnd + + // Check if it's a Model (extends Model or has casts) + // @codeCoverageIgnoreStart + if (! str_contains($content, 'extends Model') && ! str_contains($content, 'casts')) { + return; + } + // @codeCoverageIgnoreEnd + + $castsContent = null; + + // Try to match property style: protected $casts = [...]; + // @codeCoverageIgnoreStart + if (preg_match('/protected\s+\$casts\s*=\s*\[([^\]]+)\]/s', $content, $match)) { + $castsContent = $match[1]; + } + // @codeCoverageIgnoreEnd + // Try to match method style: protected function casts(): array { return [...]; } + elseif (preg_match('/function\s+casts\s*\(\s*\)\s*:\s*array\s*\{\s*return\s*\[([^\]]+)\]/s', $content, $match)) { + $castsContent = $match[1]; + } + + // @codeCoverageIgnoreStart + if (! $castsContent) { + return; + } + // @codeCoverageIgnoreEnd + + // Extract column => EnumClass pairs + // Matches: 'status' => StatusEnum::class or 'status' => \App\Enums\StatusEnum::class + preg_match_all('/[\'"](\w+)[\'"]\s*=>\s*([\\\\]?[\w\\\\]+)::class/', $castsContent, $matches); + + if (! empty($matches[1])) { + $this->modelCasts[$className] = []; + + foreach ($matches[1] as $index => $column) { + $enumClass = $matches[2][$index]; + + // Resolve short class name to full class name if needed + $resolvedEnum = $this->resolveEnumClass($enumClass, $content); + + if ($resolvedEnum) { + $this->modelCasts[$className][$column] = $resolvedEnum; + } + } + } + } + + /** + * Resolve an enum class name to its full class path. + */ + private function resolveEnumClass(string $enumClass, string $fileContent): ?string + { + // If already a full path (starts with \) + // @codeCoverageIgnoreStart + if (str_starts_with($enumClass, '\\')) { + return ltrim($enumClass, '\\'); + } + // @codeCoverageIgnoreEnd + + // Check if it's one of our loaded enums + foreach ($this->enums as $fullClass => $data) { + if ($data['name'] === $enumClass || $fullClass === $enumClass) { + return $fullClass; + } + } + + // Try to resolve from use statements + // @codeCoverageIgnoreStart + if (preg_match('/use\s+([\\\\]?[\w\\\\]+\\\\' . preg_quote($enumClass, '/') . ')\s*;/', $fileContent, $useMatch)) { + return ltrim($useMatch[1], '\\'); + } + + return null; + // @codeCoverageIgnoreEnd + } + + /** + * Get the enum cast for a column from a specific model or any matching model. + * + * @return array{hasCast: bool, enumClass: string|null, enumName: string|null} + */ + private function getColumnEnumCast(string $column, ?string $modelName = null): array + { + // If model name provided, search for exact match first + // @codeCoverageIgnoreStart + if ($modelName) { + foreach ($this->modelCasts as $modelClass => $casts) { + // Check if model class ends with the model name (e.g., App\Models\LibraryMember matches LibraryMember) + if (class_basename($modelClass) === $modelName && isset($casts[$column])) { + $enumClass = $casts[$column]; + $enumName = isset($this->enums[$enumClass]) ? $this->enums[$enumClass]['name'] : class_basename($enumClass); + + return ['hasCast' => true, 'enumClass' => $enumClass, 'enumName' => $enumName]; + } + } + } + // @codeCoverageIgnoreEnd + + // Fall back to any model with this column cast + foreach ($this->modelCasts as $modelClass => $casts) { + if (isset($casts[$column])) { + $enumClass = $casts[$column]; + $enumName = isset($this->enums[$enumClass]) ? $this->enums[$enumClass]['name'] : class_basename($enumClass); + + return ['hasCast' => true, 'enumClass' => $enumClass, 'enumName' => $enumName]; + } + } + + return ['hasCast' => false, 'enumClass' => null, 'enumName' => null]; + } + + /** + * Extract model name from code context. + * Detects patterns like: Model::query(), Model::where(), $model->where() + */ + private function extractModelFromContext(string $context): ?string + { + // Match static method calls: LibraryMember::query(), LibraryMember::where() + if (preg_match('/([A-Z][a-zA-Z0-9_]+)::(?:query|where|orWhere|whereNot|find|create|update)\s*\(/', $context, $match)) { + // Exclude common non-model classes + $excludes = ['Auth', 'DB', 'Cache', 'Log', 'Route', 'Request', 'Response', 'Session', 'View', 'Config', 'File', 'Storage', 'Rule']; + if (! in_array($match[1], $excludes)) { + return $match[1]; + } + } + + // Match model variable patterns: $libraryMember->where(), $member->update() + // Try to infer from variable name (e.g., $libraryMember suggests LibraryMember model) + // @codeCoverageIgnoreStart + if (preg_match('/\$(\w+)->(?:where|orWhere|whereNot|update|save)\s*\(/', $context, $match)) { + // Convert camelCase/snake_case to PascalCase + $varName = $match[1]; + + return str_replace(' ', '', ucwords(str_replace(['_', '-'], ' ', $varName))); + } + // @codeCoverageIgnoreEnd + + return null; + } + /** * Check if a path is absolute. */ @@ -422,7 +622,7 @@ private function isAbsolutePath(string $path): bool } /** - * Get FQCN from a PHP file. + * Get FQCN from a PHP file (for enums). */ private function getClassFromFile(string $path): ?string { @@ -437,7 +637,30 @@ private function getClassFromFile(string $path): ?string return null; } - return $namespaceMatch[1].'\\'.$enumMatch[1]; + return $namespaceMatch[1] . '\\' . $enumMatch[1]; + } + + /** + * Get FQCN from a PHP model file (for classes). + */ + private function getModelClassFromFile(string $path): ?string + { + $content = file_get_contents($path); + + // @codeCoverageIgnoreStart + if (! preg_match('/namespace\s+([^;]+);/', $content, $namespaceMatch)) { + return null; + } + // @codeCoverageIgnoreEnd + + // Match class declaration (handles final, abstract, readonly modifiers) + // @codeCoverageIgnoreStart + if (! preg_match('/^(?:final\s+|abstract\s+|readonly\s+)*class\s+(\w+)/m', $content, $classMatch)) { + return null; + } + // @codeCoverageIgnoreEnd + + return $namespaceMatch[1] . '\\' . $classMatch[1]; } /** @@ -480,7 +703,7 @@ private function findNonUppercaseKeys(): void private function findEnumReferences(string $enumName, string $caseName): array { $references = []; - $searchPattern = $enumName.'::'.$caseName; + $searchPattern = $enumName . '::' . $caseName; $pathOption = $this->option('path'); if ($pathOption) { @@ -506,7 +729,7 @@ private function findEnumReferences(string $enumName, string $caseName): array foreach ($lines as $lineNum => $line) { if (str_contains($line, $searchPattern)) { $references[] = [ - 'file' => str_replace(base_path().'/', '', $file->getPathname()), + 'file' => str_replace(base_path() . '/', '', $file->getPathname()), 'line' => $lineNum + 1, 'code' => trim($line), ]; @@ -522,7 +745,7 @@ private function findEnumReferences(string $enumName, string $caseName): array */ private function displayKeyNormalizationResults(): void { - $this->warn('⚠️ Found '.count($this->keyNormalizationIssues).' key(s) to normalize:'); + $this->warn('⚠️ Found ' . count($this->keyNormalizationIssues) . ' key(s) to normalize:'); $this->newLine(); $totalRefs = 0; @@ -532,7 +755,7 @@ private function displayKeyNormalizationResults(): void $totalRefs += $refCount; $this->line("{$issue['enum']}"); - $this->line(" {$issue['oldKey']} → {$issue['newKey']} ({$refCount} reference".($refCount !== 1 ? 's' : '').')'); + $this->line(" {$issue['oldKey']} → {$issue['newKey']} ({$refCount} reference" . ($refCount !== 1 ? 's' : '') . ')'); if ($this->option('detailed') && ! empty($issue['references'])) { foreach ($issue['references'] as $ref) { @@ -542,7 +765,7 @@ private function displayKeyNormalizationResults(): void } $this->newLine(); - $this->info('📊 Total: '.count($this->keyNormalizationIssues)." keys, {$totalRefs} references"); + $this->info('📊 Total: ' . count($this->keyNormalizationIssues) . " keys, {$totalRefs} references"); } /** @@ -573,14 +796,14 @@ private function applyKeyNormalization(bool $withBackup): int foreach ($issues as $issue) { // Replace case declaration: case OldKey = 'value' -> case NEW_KEY = 'value' - $pattern = '/case\s+'.preg_quote($issue['oldKey'], '/').'\s*=/'; - $replacement = 'case '.$issue['newKey'].' ='; + $pattern = '/case\s+' . preg_quote($issue['oldKey'], '/') . '\s*=/'; + $replacement = 'case ' . $issue['newKey'] . ' ='; $content = preg_replace($pattern, $replacement, $content); // Replace self references within the enum file $content = str_replace( - 'self::'.$issue['oldKey'], - 'self::'.$issue['newKey'], + 'self::' . $issue['oldKey'], + 'self::' . $issue['newKey'], $content ); @@ -590,8 +813,8 @@ private function applyKeyNormalization(bool $withBackup): int file_put_contents($filePath, $content); $filesChanged++; - $relativePath = str_replace(base_path().'/', '', $filePath); - $this->line("✓ {$relativePath} (".count($issues).' keys)'); + $relativePath = str_replace(base_path() . '/', '', $filePath); + $this->line("✓ {$relativePath} (" . count($issues) . ' keys)'); } // Update references throughout the codebase @@ -623,8 +846,8 @@ private function applyKeyNormalization(bool $withBackup): int } foreach ($refs as $ref) { - $search = $ref['enum'].'::'.$ref['oldKey']; - $replace = $ref['enum'].'::'.$ref['newKey']; + $search = $ref['enum'] . '::' . $ref['oldKey']; + $replace = $ref['enum'] . '::' . $ref['newKey']; $content = str_replace($search, $replace, $content); $refsUpdated++; } @@ -632,8 +855,8 @@ private function applyKeyNormalization(bool $withBackup): int file_put_contents($filePath, $content); $filesChanged++; - $relativePath = str_replace(base_path().'/', '', $filePath); - $this->line("✓ {$relativePath} (".count($refs).' refs)'); + $relativePath = str_replace(base_path() . '/', '', $filePath); + $this->line("✓ {$relativePath} (" . count($refs) . ' refs)'); } $this->newLine(); @@ -721,7 +944,7 @@ private function scanDirectory(string $path, ?array $targetEnums = null): void progress( label: 'Scanning files...', steps: $phpFiles, - callback: fn ($file) => $this->scanFile($file->getPathname(), $targetEnums), + callback: fn($file) => $this->scanFile($file->getPathname(), $targetEnums), ); $this->newLine(); @@ -735,7 +958,7 @@ private function scanDirectory(string $path, ?array $targetEnums = null): void private function scanFile(string $filePath, ?array $targetEnums = null): void { $content = file_get_contents($filePath); - $relativePath = str_replace(base_path().'/', '', $filePath); + $relativePath = str_replace(base_path() . '/', '', $filePath); $lines = explode("\n", $content); foreach ($this->patterns as $type => $pattern) { @@ -808,14 +1031,17 @@ private function scanValidationRules(string $file, string $content, array $lines */ private function getContext(array $lines, int $lineNumber): string { - $start = max(0, $lineNumber - 2); - $end = min(count($lines), $lineNumber + 1); + // $lineNumber is 1-based, convert to 0-based for array access + $lineIndex = $lineNumber - 1; + $start = max(0, $lineIndex - 3); // Get 3 lines before for better context + $end = min(count($lines), $lineIndex + 2); return implode("\n", array_slice($lines, $start, $end - $start)); } /** - * Check if a value matches an enum and add to issues. + * Check if a value matches an enum cast and add to issues. + * Only columns with enum casts in models will be refactored. * * @param array|null $targetEnums */ @@ -829,40 +1055,51 @@ private function checkAndAddIssue( string $context, ?array $targetEnums = null ): void { - $isStrict = $this->option('strict'); + // Extract model name from context for precise cast lookup + $modelName = $this->extractModelFromContext($context); - foreach ($this->enums as $enumClass => $enumData) { - if ($targetEnums && ! in_array($enumData['name'], $targetEnums)) { - // @codeCoverageIgnoreStart - continue; - // @codeCoverageIgnoreEnd - } + // Check if this column has an enum cast in the detected model or any model + $castInfo = $this->getColumnEnumCast($column, $modelName); - foreach ($enumData['cases'] as $caseName => $caseValue) { - if ($caseValue === $value) { - if ($isStrict) { - $enumLower = mb_strtolower($enumData['name']); - $columnLower = mb_strtolower($column); - if (! str_contains($enumLower, $columnLower) && ! str_contains($columnLower, $enumLower)) { - continue; - } - } + // Only refactor if column has an enum cast - no cast means no refactoring + if (! $castInfo['hasCast'] || ! $castInfo['enumClass']) { + return; + } - $this->issues[] = [ - 'file' => $file, - 'line' => $line, - 'type' => $type, - 'column' => $column, - 'value' => $value, - 'code' => $code, - 'enum' => $enumData['name'], - 'case' => $caseName, - 'class' => $enumClass, - 'context' => $context, - ]; + $enumClass = $castInfo['enumClass']; - return; - } + // @codeCoverageIgnoreStart + if (! isset($this->enums[$enumClass])) { + return; + } + // @codeCoverageIgnoreEnd + + $enumData = $this->enums[$enumClass]; + + // @codeCoverageIgnoreStart + if ($targetEnums && ! in_array($enumData['name'], $targetEnums)) { + return; + } + // @codeCoverageIgnoreEnd + + // Find the matching case in the cast enum + foreach ($enumData['cases'] as $caseName => $caseValue) { + if ($caseValue === $value) { + $this->issues[] = [ + 'file' => $file, + 'line' => $line, + 'type' => $type, + 'column' => $column, + 'value' => $value, + 'code' => $code, + 'enum' => $enumData['name'], + 'case' => $caseName, + 'class' => $enumClass, + 'context' => $context, + 'hasCast' => true, + ]; + + return; } } } @@ -878,7 +1115,7 @@ private function displayResults(): void return; } - $this->warn('⚠️ Found '.count($this->issues).' potential hardcoded enum value(s):'); + $this->warn('⚠️ Found ' . count($this->issues) . ' potential hardcoded enum value(s):'); $this->newLine(); $byFile = []; @@ -887,7 +1124,7 @@ private function displayResults(): void } foreach ($byFile as $file => $issues) { - $this->line("{$file} (".count($issues).' issue'.(count($issues) > 1 ? 's' : '').')'); + $this->line("{$file} (" . count($issues) . ' issue' . (count($issues) > 1 ? 's' : '') . ')'); foreach ($issues as $issue) { $suggestion = $this->generateSuggestion($issue); @@ -895,7 +1132,8 @@ private function displayResults(): void $this->line(" {$suggestion}"); if ($this->option('detailed')) { - $this->line(" Enum: {$issue['class']}::{$issue['case']}"); + $castStatus = $issue['hasCast'] ? '✓ cast' : '✗ no cast'; + $this->line(" Enum: {$issue['class']}::{$issue['case']} [{$castStatus}]"); } } @@ -948,17 +1186,35 @@ private function generateSuggestion(array $issue): string { $enum = $issue['enum']; $case = $issue['case']; + $hasCast = $issue['hasCast'] ?? false; + + // Handle comparison specially to preserve the variable name + if ($issue['type'] === 'comparison') { + // Extract the variable name from the matched code (e.g., $admissionCycle from "$admissionCycle->status === 'open'") + if (preg_match('/(\$\w+)->/', $issue['code'], $varMatch)) { + // If column has enum cast, model returns enum instance, compare directly + // If no cast, model returns string, need to use ->value + $enumRef = $hasCast ? "{$enum}::{$case}" : "{$enum}::{$case}->value"; + + return "{$varMatch[1]}->{$issue['column']} === {$enumRef}"; + } + + // @codeCoverageIgnoreStart + $enumRef = $hasCast ? "{$enum}::{$case}" : "{$enum}::{$case}->value"; + + return "\$...->{$issue['column']} === {$enumRef}"; + // @codeCoverageIgnoreEnd + } + // For Eloquent where clauses, Laravel handles enum->value conversion automatically + // For array assignments in create/update, Laravel also handles it when column is casted return match ($issue['type']) { 'where' => "->where('{$issue['column']}', {$enum}::{$case})", 'orWhere' => "->orWhere('{$issue['column']}', {$enum}::{$case})", 'whereNot' => "->whereNot('{$issue['column']}', {$enum}::{$case})", - 'update' => "->update(['{$issue['column']}' => {$enum}::{$case}])", - 'create' => "->create(['{$issue['column']}' => {$enum}::{$case}])", // @codeCoverageIgnore - 'array' => "['{$issue['column']}' => {$enum}::{$case}]", - 'comparison' => "\$...->{$issue['column']} === {$enum}::{$case}", - 'validation' => "Rule::enum({$enum}::class)", - default => "{$enum}::{$case}", + 'array' => "'{$issue['column']}' => {$enum}::{$case}", + 'validation' => "Rule::enum({$enum}::class)", // @codeCoverageIgnore + default => "{$enum}::{$case}", // @codeCoverageIgnore }; } @@ -1075,7 +1331,7 @@ private function applyChanges(bool $withBackup): int file_put_contents($fullPath, $content); $filesChanged++; - $this->line("✓ {$file} (".count($issues).' changes)'); + $this->line("✓ {$file} (" . count($issues) . ' changes)'); } $this->newLine(); @@ -1093,7 +1349,7 @@ private function applyChanges(bool $withBackup): int */ private function createBackup(string $fullPath, string $content): void { - $backupDir = storage_path('app/enumify-refactor-backups/'.date('Y-m-d_His')); + $backupDir = storage_path('app/enumify-refactor-backups/' . date('Y-m-d_His')); if (! is_dir($backupDir)) { mkdir($backupDir, 0755, true); @@ -1104,7 +1360,7 @@ private function createBackup(string $fullPath, string $content): void $normalizedBasePath = str_replace('\\', '/', base_path()); // Get relative path or use basename if file is outside base_path - if (str_starts_with($normalizedFullPath, $normalizedBasePath.'/')) { + if (str_starts_with($normalizedFullPath, $normalizedBasePath . '/')) { $relativePath = substr($normalizedFullPath, strlen($normalizedBasePath) + 1); // @codeCoverageIgnore } else { // File is outside base_path (e.g., temp directory in tests) @@ -1113,7 +1369,7 @@ private function createBackup(string $fullPath, string $content): void // Create safe filename by replacing path separators and removing invalid chars $safeFilename = str_replace(['/', '\\', ':'], '_', $relativePath); - $backupPath = $backupDir.'/'.$safeFilename; + $backupPath = $backupDir . '/' . $safeFilename; file_put_contents($backupPath, $content); $this->backups[$fullPath] = $backupPath; @@ -1153,10 +1409,10 @@ private function addImports(string $content, array $imports): string $lastUse = end($useMatches[0]); $insertPos = $namespaceEnd + $lastUse[1] + mb_strlen($lastUse[0]); - return mb_substr($content, 0, $insertPos)."\n".implode("\n", $newImports).mb_substr($content, $insertPos); + return mb_substr($content, 0, $insertPos) . "\n" . implode("\n", $newImports) . mb_substr($content, $insertPos); } - return mb_substr($content, 0, $namespaceEnd)."\n\n".implode("\n", $newImports).$afterNamespace; + return mb_substr($content, 0, $namespaceEnd) . "\n\n" . implode("\n", $newImports) . $afterNamespace; } /** @@ -1236,12 +1492,12 @@ private function generateMarkdownReport(): string $lines = [ '# Enumify Refactor Report', '', - 'Generated: '.date('Y-m-d H:i:s'), + 'Generated: ' . date('Y-m-d H:i:s'), '', '## Summary', '', - '- **Total Issues:** '.count($this->issues), - '- **Enums Scanned:** '.count($this->enums), + '- **Total Issues:** ' . count($this->issues), + '- **Enums Scanned:** ' . count($this->enums), '', '## Issues by File', '', diff --git a/tests/Feature/RefactorCommandTest.php b/tests/Feature/RefactorCommandTest.php index 85391b5..016c858 100644 --- a/tests/Feature/RefactorCommandTest.php +++ b/tests/Feature/RefactorCommandTest.php @@ -6,15 +6,39 @@ beforeEach(function () { // Create temp directories for testing - $this->tempDir = sys_get_temp_dir().'/enumify-refactor-test-'.uniqid(); - $this->appDir = $this->tempDir.'/app'; + $this->tempDir = sys_get_temp_dir() . '/enumify-refactor-test-' . uniqid(); + $this->appDir = $this->tempDir . '/app'; + $this->modelsDir = $this->tempDir . '/app/Models'; $this->backupDir = storage_path('app/enumify-refactor-backups'); // Use the real fixtures path (already autoloaded) - $this->enumPath = realpath(__DIR__.'/../Fixtures'); + $this->enumPath = realpath(__DIR__ . '/../Fixtures'); mkdir($this->tempDir, 0755, true); mkdir($this->appDir, 0755, true); + mkdir($this->modelsDir, 0755, true); + + // Create model with enum cast so refactoring will work + $orderModel = <<<'PHP' + OrderStatus::class, + ]; + } +} +PHP; + + file_put_contents($this->modelsDir . '/Order.php', $orderModel); // Create test files with hardcoded enum values using existing fixture enums // Note: The refactor command patterns match ->where() not ::where() (method chains, not static calls) @@ -47,7 +71,7 @@ public function check($order) } PHP; - file_put_contents($this->appDir.'/OrderController.php', $testController); + file_put_contents($this->appDir . '/OrderController.php', $testController); // Create file with array and validation patterns $testRequest = <<<'PHP' @@ -75,7 +99,7 @@ public function defaults() } PHP; - file_put_contents($this->appDir.'/OrderRequest.php', $testRequest); + file_put_contents($this->appDir . '/OrderRequest.php', $testRequest); // Create file that references the mixed-case enum $testService = <<<'PHP' @@ -100,10 +124,11 @@ public function getInProgress() } PHP; - file_put_contents($this->appDir.'/StatusService.php', $testService); + file_put_contents($this->appDir . '/StatusService.php', $testService); - // Configure enumify to use the real fixtures path + // Configure enumify to use the real fixtures path and models path config()->set('enumify.paths.enums', [$this->enumPath]); + config()->set('enumify.paths.models', [$this->modelsDir]); config()->set('enumify.refactor.exclude', []); }); @@ -119,7 +144,7 @@ public function getInProgress() } // Restore the MixedCaseStatus enum if it was modified - $mixedCaseEnumPath = __DIR__.'/../Fixtures/MixedCaseStatus.php'; + $mixedCaseEnumPath = __DIR__ . '/../Fixtures/MixedCaseStatus.php'; $originalContent = <<<'PHP' tempDir.'/empty'; + $emptyDir = $this->tempDir . '/empty'; mkdir($emptyDir, 0755, true); - file_put_contents($emptyDir.'/Clean.php', 'artisan('enumify:refactor', [ '--path' => $emptyDir, @@ -167,7 +192,7 @@ public function isDefault(): bool }); it('fails when no enums are found', function () { - config()->set('enumify.paths.enums', [$this->tempDir.'/nonexistent']); + config()->set('enumify.paths.enums', [$this->tempDir . '/nonexistent']); $this->artisan('enumify:refactor', [ '--path' => $this->appDir, @@ -194,13 +219,6 @@ public function isDefault(): bool ])->assertSuccessful(); }); - it('supports strict mode with --strict option', function () { - $this->artisan('enumify:refactor', [ - '--path' => $this->appDir, - '--strict' => true, - ])->assertSuccessful(); - }); - it('shows detailed output with --detailed option', function () { $this->artisan('enumify:refactor', [ '--path' => $this->appDir, @@ -209,7 +227,7 @@ public function isDefault(): bool }); it('warns when no PHP files found', function () { - $emptyDir = $this->tempDir.'/no-php'; + $emptyDir = $this->tempDir . '/no-php'; mkdir($emptyDir, 0755, true); $this->artisan('enumify:refactor', [ @@ -229,7 +247,7 @@ public function isDefault(): bool describe('enumify:refactor report export', function () { it('exports JSON report', function () { - $reportPath = $this->tempDir.'/report.json'; + $reportPath = $this->tempDir . '/report.json'; $this->artisan('enumify:refactor', [ '--path' => $this->appDir, @@ -242,7 +260,7 @@ public function isDefault(): bool }); it('exports CSV report', function () { - $reportPath = $this->tempDir.'/report.csv'; + $reportPath = $this->tempDir . '/report.csv'; $this->artisan('enumify:refactor', [ '--path' => $this->appDir, @@ -254,7 +272,7 @@ public function isDefault(): bool }); it('exports Markdown report', function () { - $reportPath = $this->tempDir.'/report.md'; + $reportPath = $this->tempDir . '/report.md'; $this->artisan('enumify:refactor', [ '--path' => $this->appDir, @@ -266,7 +284,7 @@ public function isDefault(): bool }); it('defaults to JSON for unknown extension', function () { - $reportPath = $this->tempDir.'/report.txt'; + $reportPath = $this->tempDir . '/report.txt'; $this->artisan('enumify:refactor', [ '--path' => $this->appDir, @@ -279,7 +297,7 @@ public function isDefault(): bool describe('enumify:refactor dry-run mode', function () { it('previews changes without applying with --dry-run', function () { - $originalContent = file_get_contents($this->appDir.'/OrderController.php'); + $originalContent = file_get_contents($this->appDir . '/OrderController.php'); $this->artisan('enumify:refactor', [ '--path' => $this->appDir, @@ -287,7 +305,7 @@ public function isDefault(): bool ])->assertSuccessful(); // File should remain unchanged - expect(file_get_contents($this->appDir.'/OrderController.php'))->toBe($originalContent); + expect(file_get_contents($this->appDir . '/OrderController.php'))->toBe($originalContent); }); }); @@ -298,7 +316,7 @@ public function isDefault(): bool '--fix' => true, ])->assertSuccessful(); - $content = file_get_contents($this->appDir.'/OrderController.php'); + $content = file_get_contents($this->appDir . '/OrderController.php'); // The refactor command should replace hardcoded values with enum references // Check that at least one pattern was replaced (could be any matching enum) expect($content)->toMatch('/[A-Za-z]+Status::[A-Za-z_]+/'); @@ -320,14 +338,14 @@ public function isDefault(): bool '--fix' => true, ])->assertSuccessful(); - $content = file_get_contents($this->appDir.'/OrderController.php'); + $content = file_get_contents($this->appDir . '/OrderController.php'); expect($content)->toContain('use DevWizardHQ\Enumify\Tests\Fixtures\OrderStatus;'); }); it('handles files with no issues to fix', function () { - $cleanDir = $this->tempDir.'/clean'; + $cleanDir = $this->tempDir . '/clean'; mkdir($cleanDir, 0755, true); - file_put_contents($cleanDir.'/Clean.php', 'artisan('enumify:refactor', [ '--path' => $cleanDir, @@ -345,7 +363,7 @@ public function isDefault(): bool }); it('previews key normalization with --normalize-keys --dry-run', function () { - $enumPath = __DIR__.'/../Fixtures/MixedCaseStatus.php'; + $enumPath = __DIR__ . '/../Fixtures/MixedCaseStatus.php'; $originalContent = file_get_contents($enumPath); $this->artisan('enumify:refactor', [ @@ -365,7 +383,7 @@ public function isDefault(): bool '--path' => $this->appDir, ])->assertSuccessful(); - $enumPath = __DIR__.'/../Fixtures/MixedCaseStatus.php'; + $enumPath = __DIR__ . '/../Fixtures/MixedCaseStatus.php'; $content = file_get_contents($enumPath); expect($content)->toContain('case PENDING ='); expect($content)->toContain('case INPROGRESS ='); @@ -379,7 +397,7 @@ public function isDefault(): bool '--path' => $this->appDir, ])->assertSuccessful(); - $content = file_get_contents($this->appDir.'/StatusService.php'); + $content = file_get_contents($this->appDir . '/StatusService.php'); expect($content)->toContain('MixedCaseStatus::PENDING'); expect($content)->toContain('MixedCaseStatus::INPROGRESS'); }); @@ -415,7 +433,7 @@ public function isDefault(): bool }); it('fails normalize-keys when no enums found', function () { - config()->set('enumify.paths.enums', [$this->tempDir.'/nonexistent']); + config()->set('enumify.paths.enums', [$this->tempDir . '/nonexistent']); $this->artisan('enumify:refactor', [ '--normalize-keys' => true, @@ -429,7 +447,7 @@ public function isDefault(): bool '--path' => $this->appDir, ])->assertSuccessful(); - $enumPath = __DIR__.'/../Fixtures/MixedCaseStatus.php'; + $enumPath = __DIR__ . '/../Fixtures/MixedCaseStatus.php'; $content = file_get_contents($enumPath); expect($content)->toContain('self::PENDING'); }); @@ -495,9 +513,9 @@ public function isDefault(): bool it('supports relative paths converted to absolute', function () { // Create a temp directory with relative path structure - $relativeDir = $this->tempDir.'/relative-test'; + $relativeDir = $this->tempDir . '/relative-test'; mkdir($relativeDir, 0755, true); - file_put_contents($relativeDir.'/Test.php', 'artisan('enumify:refactor', [ '--path' => $relativeDir, @@ -524,14 +542,14 @@ public function pending() } PHP; - file_put_contents($this->appDir.'/OrderService.php', $fileWithImport); + file_put_contents($this->appDir . '/OrderService.php', $fileWithImport); $this->artisan('enumify:refactor', [ '--path' => $this->appDir, '--fix' => true, ])->assertSuccessful(); - $content = file_get_contents($this->appDir.'/OrderService.php'); + $content = file_get_contents($this->appDir . '/OrderService.php'); // Count occurrences of the import - should be exactly 1 $count = substr_count($content, 'use DevWizardHQ\Enumify\Tests\Fixtures\OrderStatus;'); expect($count)->toBe(1); @@ -543,7 +561,7 @@ public function pending() '--fix' => true, ])->assertSuccessful(); - $content = file_get_contents($this->appDir.'/OrderController.php'); + $content = file_get_contents($this->appDir . '/OrderController.php'); expect($content)->toContain('namespace App\Http\Controllers;'); expect($content)->toContain('use DevWizardHQ\Enumify\Tests\Fixtures\OrderStatus;'); }); @@ -556,7 +574,7 @@ public function pending() $orders = Order::where('status', 'pending')->get(); PHP; - file_put_contents($this->appDir.'/NoNamespace.php', $noNamespaceFile); + file_put_contents($this->appDir . '/NoNamespace.php', $noNamespaceFile); $this->artisan('enumify:refactor', [ '--path' => $this->appDir, @@ -575,14 +593,14 @@ public function pending() }); it('handles non-PHP files in enum directory', function () { - file_put_contents($this->enumPath.'/readme.txt', 'This is not PHP'); + file_put_contents($this->enumPath . '/readme.txt', 'This is not PHP'); $this->artisan('enumify:refactor', [ '--path' => $this->appDir, ])->assertSuccessful(); // Clean up - @unlink($this->enumPath.'/readme.txt'); + @unlink($this->enumPath . '/readme.txt'); }); it('handles PHP files without namespace in enum directory', function () { @@ -596,14 +614,14 @@ enum SimpleEnum: string } PHP; - file_put_contents($this->enumPath.'/NoNamespaceEnum.php', $noNamespaceEnum); + file_put_contents($this->enumPath . '/NoNamespaceEnum.php', $noNamespaceEnum); $this->artisan('enumify:refactor', [ '--path' => $this->appDir, ])->assertSuccessful(); // Clean up - @unlink($this->enumPath.'/NoNamespaceEnum.php'); + @unlink($this->enumPath . '/NoNamespaceEnum.php'); }); it('handles PHP files with namespace but no enum in enum directory', function () { @@ -619,21 +637,21 @@ class NotAnEnumHelper } PHP; - file_put_contents($this->enumPath.'/NotAnEnumHelper.php', $classFile); + file_put_contents($this->enumPath . '/NotAnEnumHelper.php', $classFile); $this->artisan('enumify:refactor', [ '--path' => $this->appDir, ])->assertSuccessful(); // Clean up - @unlink($this->enumPath.'/NotAnEnumHelper.php'); + @unlink($this->enumPath . '/NotAnEnumHelper.php'); }); it('respects default excludes', function () { // Create a vendor directory - should be excluded by default - $vendorDir = $this->appDir.'/vendor'; + $vendorDir = $this->appDir . '/vendor'; mkdir($vendorDir, 0755, true); - file_put_contents($vendorDir.'/VendorFile.php', 'artisan('enumify:refactor', [ '--path' => $this->appDir, @@ -649,7 +667,7 @@ class NotAnEnumHelper $orders = $query->where('status', 'pending')->get(); PHP; - file_put_contents($this->appDir.'/NoNamespaceQuery.php', $noNamespaceFile); + file_put_contents($this->appDir . '/NoNamespaceQuery.php', $noNamespaceFile); $this->artisan('enumify:refactor', [ '--path' => $this->appDir, @@ -657,7 +675,7 @@ class NotAnEnumHelper ])->assertSuccessful(); // The file should be modified but no imports added - $content = file_get_contents($this->appDir.'/NoNamespaceQuery.php'); + $content = file_get_contents($this->appDir . '/NoNamespaceQuery.php'); expect($content)->not->toContain('use '); }); }); @@ -680,7 +698,7 @@ class NotAnEnumHelper '--backup' => true, ])->assertSuccessful(); - $backupDirs = glob($this->backupDir.'/*'); + $backupDirs = glob($this->backupDir . '/*'); expect($backupDirs)->not->toBeEmpty(); }); @@ -692,10 +710,10 @@ class NotAnEnumHelper ])->assertSuccessful(); // Find backup file - $backupDirs = glob($this->backupDir.'/*'); + $backupDirs = glob($this->backupDir . '/*'); expect($backupDirs)->not->toBeEmpty(); - $backupFiles = glob($backupDirs[0].'/*'); + $backupFiles = glob($backupDirs[0] . '/*'); expect($backupFiles)->not->toBeEmpty(); }); }); @@ -708,12 +726,12 @@ class NotAnEnumHelper ])->assertSuccessful(); // Controller should have method call patterns fixed - $controllerContent = file_get_contents($this->appDir.'/OrderController.php'); + $controllerContent = file_get_contents($this->appDir . '/OrderController.php'); // Check that at least one pattern was replaced with an enum reference expect($controllerContent)->toMatch('/[A-Za-z]+Status::[A-Za-z_]+/'); // Request should have array patterns fixed - $requestContent = file_get_contents($this->appDir.'/OrderRequest.php'); + $requestContent = file_get_contents($this->appDir . '/OrderRequest.php'); // Check that the status array pattern was replaced (could be mixed case) expect($requestContent)->toMatch('/[A-Za-z]+Status::[A-Za-z_]+/'); }); 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 From 6db7958c6da0605339ebe4d9a48b88e38784dcce Mon Sep 17 00:00:00 2001 From: iqbalhasandev <39612205+iqbalhasandev@users.noreply.github.com> Date: Sun, 25 Jan 2026 07:21:50 +0000 Subject: [PATCH 2/2] Fix styling --- src/Commands/RefactorCommand.php | 66 +++++++++--------- tests/Feature/RefactorCommandTest.php | 98 +++++++++++++-------------- tests/Fixtures/MixedCaseStatus.php | 2 +- 3 files changed, 83 insertions(+), 83 deletions(-) diff --git a/src/Commands/RefactorCommand.php b/src/Commands/RefactorCommand.php index e7c6da7..b2a4701 100644 --- a/src/Commands/RefactorCommand.php +++ b/src/Commands/RefactorCommand.php @@ -406,7 +406,7 @@ private function loadEnums(): void } $count = count($this->enums); - $this->info("✅ Loaded {$count} enum" . ($count !== 1 ? 's' : '')); + $this->info("✅ Loaded {$count} enum".($count !== 1 ? 's' : '')); $this->newLine(); } @@ -449,7 +449,7 @@ private function loadModelCasts(): void $castCount = array_sum(array_map('count', $this->modelCasts)); $modelCount = count($this->modelCasts); - $this->info("✅ Found {$castCount} enum cast" . ($castCount !== 1 ? 's' : '') . " in {$modelCount} model" . ($modelCount !== 1 ? 's' : '')); + $this->info("✅ Found {$castCount} enum cast".($castCount !== 1 ? 's' : '')." in {$modelCount} model".($modelCount !== 1 ? 's' : '')); $this->newLine(); } @@ -535,7 +535,7 @@ private function resolveEnumClass(string $enumClass, string $fileContent): ?stri // Try to resolve from use statements // @codeCoverageIgnoreStart - if (preg_match('/use\s+([\\\\]?[\w\\\\]+\\\\' . preg_quote($enumClass, '/') . ')\s*;/', $fileContent, $useMatch)) { + if (preg_match('/use\s+([\\\\]?[\w\\\\]+\\\\'.preg_quote($enumClass, '/').')\s*;/', $fileContent, $useMatch)) { return ltrim($useMatch[1], '\\'); } @@ -637,7 +637,7 @@ private function getClassFromFile(string $path): ?string return null; } - return $namespaceMatch[1] . '\\' . $enumMatch[1]; + return $namespaceMatch[1].'\\'.$enumMatch[1]; } /** @@ -660,7 +660,7 @@ private function getModelClassFromFile(string $path): ?string } // @codeCoverageIgnoreEnd - return $namespaceMatch[1] . '\\' . $classMatch[1]; + return $namespaceMatch[1].'\\'.$classMatch[1]; } /** @@ -703,7 +703,7 @@ private function findNonUppercaseKeys(): void private function findEnumReferences(string $enumName, string $caseName): array { $references = []; - $searchPattern = $enumName . '::' . $caseName; + $searchPattern = $enumName.'::'.$caseName; $pathOption = $this->option('path'); if ($pathOption) { @@ -729,7 +729,7 @@ private function findEnumReferences(string $enumName, string $caseName): array foreach ($lines as $lineNum => $line) { if (str_contains($line, $searchPattern)) { $references[] = [ - 'file' => str_replace(base_path() . '/', '', $file->getPathname()), + 'file' => str_replace(base_path().'/', '', $file->getPathname()), 'line' => $lineNum + 1, 'code' => trim($line), ]; @@ -745,7 +745,7 @@ private function findEnumReferences(string $enumName, string $caseName): array */ private function displayKeyNormalizationResults(): void { - $this->warn('⚠️ Found ' . count($this->keyNormalizationIssues) . ' key(s) to normalize:'); + $this->warn('⚠️ Found '.count($this->keyNormalizationIssues).' key(s) to normalize:'); $this->newLine(); $totalRefs = 0; @@ -755,7 +755,7 @@ private function displayKeyNormalizationResults(): void $totalRefs += $refCount; $this->line("{$issue['enum']}"); - $this->line(" {$issue['oldKey']} → {$issue['newKey']} ({$refCount} reference" . ($refCount !== 1 ? 's' : '') . ')'); + $this->line(" {$issue['oldKey']} → {$issue['newKey']} ({$refCount} reference".($refCount !== 1 ? 's' : '').')'); if ($this->option('detailed') && ! empty($issue['references'])) { foreach ($issue['references'] as $ref) { @@ -765,7 +765,7 @@ private function displayKeyNormalizationResults(): void } $this->newLine(); - $this->info('📊 Total: ' . count($this->keyNormalizationIssues) . " keys, {$totalRefs} references"); + $this->info('📊 Total: '.count($this->keyNormalizationIssues)." keys, {$totalRefs} references"); } /** @@ -796,14 +796,14 @@ private function applyKeyNormalization(bool $withBackup): int foreach ($issues as $issue) { // Replace case declaration: case OldKey = 'value' -> case NEW_KEY = 'value' - $pattern = '/case\s+' . preg_quote($issue['oldKey'], '/') . '\s*=/'; - $replacement = 'case ' . $issue['newKey'] . ' ='; + $pattern = '/case\s+'.preg_quote($issue['oldKey'], '/').'\s*=/'; + $replacement = 'case '.$issue['newKey'].' ='; $content = preg_replace($pattern, $replacement, $content); // Replace self references within the enum file $content = str_replace( - 'self::' . $issue['oldKey'], - 'self::' . $issue['newKey'], + 'self::'.$issue['oldKey'], + 'self::'.$issue['newKey'], $content ); @@ -813,8 +813,8 @@ private function applyKeyNormalization(bool $withBackup): int file_put_contents($filePath, $content); $filesChanged++; - $relativePath = str_replace(base_path() . '/', '', $filePath); - $this->line("✓ {$relativePath} (" . count($issues) . ' keys)'); + $relativePath = str_replace(base_path().'/', '', $filePath); + $this->line("✓ {$relativePath} (".count($issues).' keys)'); } // Update references throughout the codebase @@ -846,8 +846,8 @@ private function applyKeyNormalization(bool $withBackup): int } foreach ($refs as $ref) { - $search = $ref['enum'] . '::' . $ref['oldKey']; - $replace = $ref['enum'] . '::' . $ref['newKey']; + $search = $ref['enum'].'::'.$ref['oldKey']; + $replace = $ref['enum'].'::'.$ref['newKey']; $content = str_replace($search, $replace, $content); $refsUpdated++; } @@ -855,8 +855,8 @@ private function applyKeyNormalization(bool $withBackup): int file_put_contents($filePath, $content); $filesChanged++; - $relativePath = str_replace(base_path() . '/', '', $filePath); - $this->line("✓ {$relativePath} (" . count($refs) . ' refs)'); + $relativePath = str_replace(base_path().'/', '', $filePath); + $this->line("✓ {$relativePath} (".count($refs).' refs)'); } $this->newLine(); @@ -944,7 +944,7 @@ private function scanDirectory(string $path, ?array $targetEnums = null): void progress( label: 'Scanning files...', steps: $phpFiles, - callback: fn($file) => $this->scanFile($file->getPathname(), $targetEnums), + callback: fn ($file) => $this->scanFile($file->getPathname(), $targetEnums), ); $this->newLine(); @@ -958,7 +958,7 @@ private function scanDirectory(string $path, ?array $targetEnums = null): void private function scanFile(string $filePath, ?array $targetEnums = null): void { $content = file_get_contents($filePath); - $relativePath = str_replace(base_path() . '/', '', $filePath); + $relativePath = str_replace(base_path().'/', '', $filePath); $lines = explode("\n", $content); foreach ($this->patterns as $type => $pattern) { @@ -1115,7 +1115,7 @@ private function displayResults(): void return; } - $this->warn('⚠️ Found ' . count($this->issues) . ' potential hardcoded enum value(s):'); + $this->warn('⚠️ Found '.count($this->issues).' potential hardcoded enum value(s):'); $this->newLine(); $byFile = []; @@ -1124,7 +1124,7 @@ private function displayResults(): void } foreach ($byFile as $file => $issues) { - $this->line("{$file} (" . count($issues) . ' issue' . (count($issues) > 1 ? 's' : '') . ')'); + $this->line("{$file} (".count($issues).' issue'.(count($issues) > 1 ? 's' : '').')'); foreach ($issues as $issue) { $suggestion = $this->generateSuggestion($issue); @@ -1331,7 +1331,7 @@ private function applyChanges(bool $withBackup): int file_put_contents($fullPath, $content); $filesChanged++; - $this->line("✓ {$file} (" . count($issues) . ' changes)'); + $this->line("✓ {$file} (".count($issues).' changes)'); } $this->newLine(); @@ -1349,7 +1349,7 @@ private function applyChanges(bool $withBackup): int */ private function createBackup(string $fullPath, string $content): void { - $backupDir = storage_path('app/enumify-refactor-backups/' . date('Y-m-d_His')); + $backupDir = storage_path('app/enumify-refactor-backups/'.date('Y-m-d_His')); if (! is_dir($backupDir)) { mkdir($backupDir, 0755, true); @@ -1360,7 +1360,7 @@ private function createBackup(string $fullPath, string $content): void $normalizedBasePath = str_replace('\\', '/', base_path()); // Get relative path or use basename if file is outside base_path - if (str_starts_with($normalizedFullPath, $normalizedBasePath . '/')) { + if (str_starts_with($normalizedFullPath, $normalizedBasePath.'/')) { $relativePath = substr($normalizedFullPath, strlen($normalizedBasePath) + 1); // @codeCoverageIgnore } else { // File is outside base_path (e.g., temp directory in tests) @@ -1369,7 +1369,7 @@ private function createBackup(string $fullPath, string $content): void // Create safe filename by replacing path separators and removing invalid chars $safeFilename = str_replace(['/', '\\', ':'], '_', $relativePath); - $backupPath = $backupDir . '/' . $safeFilename; + $backupPath = $backupDir.'/'.$safeFilename; file_put_contents($backupPath, $content); $this->backups[$fullPath] = $backupPath; @@ -1409,10 +1409,10 @@ private function addImports(string $content, array $imports): string $lastUse = end($useMatches[0]); $insertPos = $namespaceEnd + $lastUse[1] + mb_strlen($lastUse[0]); - return mb_substr($content, 0, $insertPos) . "\n" . implode("\n", $newImports) . mb_substr($content, $insertPos); + return mb_substr($content, 0, $insertPos)."\n".implode("\n", $newImports).mb_substr($content, $insertPos); } - return mb_substr($content, 0, $namespaceEnd) . "\n\n" . implode("\n", $newImports) . $afterNamespace; + return mb_substr($content, 0, $namespaceEnd)."\n\n".implode("\n", $newImports).$afterNamespace; } /** @@ -1492,12 +1492,12 @@ private function generateMarkdownReport(): string $lines = [ '# Enumify Refactor Report', '', - 'Generated: ' . date('Y-m-d H:i:s'), + 'Generated: '.date('Y-m-d H:i:s'), '', '## Summary', '', - '- **Total Issues:** ' . count($this->issues), - '- **Enums Scanned:** ' . count($this->enums), + '- **Total Issues:** '.count($this->issues), + '- **Enums Scanned:** '.count($this->enums), '', '## Issues by File', '', diff --git a/tests/Feature/RefactorCommandTest.php b/tests/Feature/RefactorCommandTest.php index 016c858..002e46e 100644 --- a/tests/Feature/RefactorCommandTest.php +++ b/tests/Feature/RefactorCommandTest.php @@ -6,13 +6,13 @@ beforeEach(function () { // Create temp directories for testing - $this->tempDir = sys_get_temp_dir() . '/enumify-refactor-test-' . uniqid(); - $this->appDir = $this->tempDir . '/app'; - $this->modelsDir = $this->tempDir . '/app/Models'; + $this->tempDir = sys_get_temp_dir().'/enumify-refactor-test-'.uniqid(); + $this->appDir = $this->tempDir.'/app'; + $this->modelsDir = $this->tempDir.'/app/Models'; $this->backupDir = storage_path('app/enumify-refactor-backups'); // Use the real fixtures path (already autoloaded) - $this->enumPath = realpath(__DIR__ . '/../Fixtures'); + $this->enumPath = realpath(__DIR__.'/../Fixtures'); mkdir($this->tempDir, 0755, true); mkdir($this->appDir, 0755, true); @@ -38,7 +38,7 @@ protected function casts(): array } PHP; - file_put_contents($this->modelsDir . '/Order.php', $orderModel); + file_put_contents($this->modelsDir.'/Order.php', $orderModel); // Create test files with hardcoded enum values using existing fixture enums // Note: The refactor command patterns match ->where() not ::where() (method chains, not static calls) @@ -71,7 +71,7 @@ public function check($order) } PHP; - file_put_contents($this->appDir . '/OrderController.php', $testController); + file_put_contents($this->appDir.'/OrderController.php', $testController); // Create file with array and validation patterns $testRequest = <<<'PHP' @@ -99,7 +99,7 @@ public function defaults() } PHP; - file_put_contents($this->appDir . '/OrderRequest.php', $testRequest); + file_put_contents($this->appDir.'/OrderRequest.php', $testRequest); // Create file that references the mixed-case enum $testService = <<<'PHP' @@ -124,7 +124,7 @@ public function getInProgress() } PHP; - file_put_contents($this->appDir . '/StatusService.php', $testService); + file_put_contents($this->appDir.'/StatusService.php', $testService); // Configure enumify to use the real fixtures path and models path config()->set('enumify.paths.enums', [$this->enumPath]); @@ -144,7 +144,7 @@ public function getInProgress() } // Restore the MixedCaseStatus enum if it was modified - $mixedCaseEnumPath = __DIR__ . '/../Fixtures/MixedCaseStatus.php'; + $mixedCaseEnumPath = __DIR__.'/../Fixtures/MixedCaseStatus.php'; $originalContent = <<<'PHP' tempDir . '/empty'; + $emptyDir = $this->tempDir.'/empty'; mkdir($emptyDir, 0755, true); - file_put_contents($emptyDir . '/Clean.php', 'artisan('enumify:refactor', [ '--path' => $emptyDir, @@ -192,7 +192,7 @@ public function isDefault(): bool }); it('fails when no enums are found', function () { - config()->set('enumify.paths.enums', [$this->tempDir . '/nonexistent']); + config()->set('enumify.paths.enums', [$this->tempDir.'/nonexistent']); $this->artisan('enumify:refactor', [ '--path' => $this->appDir, @@ -227,7 +227,7 @@ public function isDefault(): bool }); it('warns when no PHP files found', function () { - $emptyDir = $this->tempDir . '/no-php'; + $emptyDir = $this->tempDir.'/no-php'; mkdir($emptyDir, 0755, true); $this->artisan('enumify:refactor', [ @@ -247,7 +247,7 @@ public function isDefault(): bool describe('enumify:refactor report export', function () { it('exports JSON report', function () { - $reportPath = $this->tempDir . '/report.json'; + $reportPath = $this->tempDir.'/report.json'; $this->artisan('enumify:refactor', [ '--path' => $this->appDir, @@ -260,7 +260,7 @@ public function isDefault(): bool }); it('exports CSV report', function () { - $reportPath = $this->tempDir . '/report.csv'; + $reportPath = $this->tempDir.'/report.csv'; $this->artisan('enumify:refactor', [ '--path' => $this->appDir, @@ -272,7 +272,7 @@ public function isDefault(): bool }); it('exports Markdown report', function () { - $reportPath = $this->tempDir . '/report.md'; + $reportPath = $this->tempDir.'/report.md'; $this->artisan('enumify:refactor', [ '--path' => $this->appDir, @@ -284,7 +284,7 @@ public function isDefault(): bool }); it('defaults to JSON for unknown extension', function () { - $reportPath = $this->tempDir . '/report.txt'; + $reportPath = $this->tempDir.'/report.txt'; $this->artisan('enumify:refactor', [ '--path' => $this->appDir, @@ -297,7 +297,7 @@ public function isDefault(): bool describe('enumify:refactor dry-run mode', function () { it('previews changes without applying with --dry-run', function () { - $originalContent = file_get_contents($this->appDir . '/OrderController.php'); + $originalContent = file_get_contents($this->appDir.'/OrderController.php'); $this->artisan('enumify:refactor', [ '--path' => $this->appDir, @@ -305,7 +305,7 @@ public function isDefault(): bool ])->assertSuccessful(); // File should remain unchanged - expect(file_get_contents($this->appDir . '/OrderController.php'))->toBe($originalContent); + expect(file_get_contents($this->appDir.'/OrderController.php'))->toBe($originalContent); }); }); @@ -316,7 +316,7 @@ public function isDefault(): bool '--fix' => true, ])->assertSuccessful(); - $content = file_get_contents($this->appDir . '/OrderController.php'); + $content = file_get_contents($this->appDir.'/OrderController.php'); // The refactor command should replace hardcoded values with enum references // Check that at least one pattern was replaced (could be any matching enum) expect($content)->toMatch('/[A-Za-z]+Status::[A-Za-z_]+/'); @@ -338,14 +338,14 @@ public function isDefault(): bool '--fix' => true, ])->assertSuccessful(); - $content = file_get_contents($this->appDir . '/OrderController.php'); + $content = file_get_contents($this->appDir.'/OrderController.php'); expect($content)->toContain('use DevWizardHQ\Enumify\Tests\Fixtures\OrderStatus;'); }); it('handles files with no issues to fix', function () { - $cleanDir = $this->tempDir . '/clean'; + $cleanDir = $this->tempDir.'/clean'; mkdir($cleanDir, 0755, true); - file_put_contents($cleanDir . '/Clean.php', 'artisan('enumify:refactor', [ '--path' => $cleanDir, @@ -363,7 +363,7 @@ public function isDefault(): bool }); it('previews key normalization with --normalize-keys --dry-run', function () { - $enumPath = __DIR__ . '/../Fixtures/MixedCaseStatus.php'; + $enumPath = __DIR__.'/../Fixtures/MixedCaseStatus.php'; $originalContent = file_get_contents($enumPath); $this->artisan('enumify:refactor', [ @@ -383,7 +383,7 @@ public function isDefault(): bool '--path' => $this->appDir, ])->assertSuccessful(); - $enumPath = __DIR__ . '/../Fixtures/MixedCaseStatus.php'; + $enumPath = __DIR__.'/../Fixtures/MixedCaseStatus.php'; $content = file_get_contents($enumPath); expect($content)->toContain('case PENDING ='); expect($content)->toContain('case INPROGRESS ='); @@ -397,7 +397,7 @@ public function isDefault(): bool '--path' => $this->appDir, ])->assertSuccessful(); - $content = file_get_contents($this->appDir . '/StatusService.php'); + $content = file_get_contents($this->appDir.'/StatusService.php'); expect($content)->toContain('MixedCaseStatus::PENDING'); expect($content)->toContain('MixedCaseStatus::INPROGRESS'); }); @@ -433,7 +433,7 @@ public function isDefault(): bool }); it('fails normalize-keys when no enums found', function () { - config()->set('enumify.paths.enums', [$this->tempDir . '/nonexistent']); + config()->set('enumify.paths.enums', [$this->tempDir.'/nonexistent']); $this->artisan('enumify:refactor', [ '--normalize-keys' => true, @@ -447,7 +447,7 @@ public function isDefault(): bool '--path' => $this->appDir, ])->assertSuccessful(); - $enumPath = __DIR__ . '/../Fixtures/MixedCaseStatus.php'; + $enumPath = __DIR__.'/../Fixtures/MixedCaseStatus.php'; $content = file_get_contents($enumPath); expect($content)->toContain('self::PENDING'); }); @@ -513,9 +513,9 @@ public function isDefault(): bool it('supports relative paths converted to absolute', function () { // Create a temp directory with relative path structure - $relativeDir = $this->tempDir . '/relative-test'; + $relativeDir = $this->tempDir.'/relative-test'; mkdir($relativeDir, 0755, true); - file_put_contents($relativeDir . '/Test.php', 'artisan('enumify:refactor', [ '--path' => $relativeDir, @@ -542,14 +542,14 @@ public function pending() } PHP; - file_put_contents($this->appDir . '/OrderService.php', $fileWithImport); + file_put_contents($this->appDir.'/OrderService.php', $fileWithImport); $this->artisan('enumify:refactor', [ '--path' => $this->appDir, '--fix' => true, ])->assertSuccessful(); - $content = file_get_contents($this->appDir . '/OrderService.php'); + $content = file_get_contents($this->appDir.'/OrderService.php'); // Count occurrences of the import - should be exactly 1 $count = substr_count($content, 'use DevWizardHQ\Enumify\Tests\Fixtures\OrderStatus;'); expect($count)->toBe(1); @@ -561,7 +561,7 @@ public function pending() '--fix' => true, ])->assertSuccessful(); - $content = file_get_contents($this->appDir . '/OrderController.php'); + $content = file_get_contents($this->appDir.'/OrderController.php'); expect($content)->toContain('namespace App\Http\Controllers;'); expect($content)->toContain('use DevWizardHQ\Enumify\Tests\Fixtures\OrderStatus;'); }); @@ -574,7 +574,7 @@ public function pending() $orders = Order::where('status', 'pending')->get(); PHP; - file_put_contents($this->appDir . '/NoNamespace.php', $noNamespaceFile); + file_put_contents($this->appDir.'/NoNamespace.php', $noNamespaceFile); $this->artisan('enumify:refactor', [ '--path' => $this->appDir, @@ -593,14 +593,14 @@ public function pending() }); it('handles non-PHP files in enum directory', function () { - file_put_contents($this->enumPath . '/readme.txt', 'This is not PHP'); + file_put_contents($this->enumPath.'/readme.txt', 'This is not PHP'); $this->artisan('enumify:refactor', [ '--path' => $this->appDir, ])->assertSuccessful(); // Clean up - @unlink($this->enumPath . '/readme.txt'); + @unlink($this->enumPath.'/readme.txt'); }); it('handles PHP files without namespace in enum directory', function () { @@ -614,14 +614,14 @@ enum SimpleEnum: string } PHP; - file_put_contents($this->enumPath . '/NoNamespaceEnum.php', $noNamespaceEnum); + file_put_contents($this->enumPath.'/NoNamespaceEnum.php', $noNamespaceEnum); $this->artisan('enumify:refactor', [ '--path' => $this->appDir, ])->assertSuccessful(); // Clean up - @unlink($this->enumPath . '/NoNamespaceEnum.php'); + @unlink($this->enumPath.'/NoNamespaceEnum.php'); }); it('handles PHP files with namespace but no enum in enum directory', function () { @@ -637,21 +637,21 @@ class NotAnEnumHelper } PHP; - file_put_contents($this->enumPath . '/NotAnEnumHelper.php', $classFile); + file_put_contents($this->enumPath.'/NotAnEnumHelper.php', $classFile); $this->artisan('enumify:refactor', [ '--path' => $this->appDir, ])->assertSuccessful(); // Clean up - @unlink($this->enumPath . '/NotAnEnumHelper.php'); + @unlink($this->enumPath.'/NotAnEnumHelper.php'); }); it('respects default excludes', function () { // Create a vendor directory - should be excluded by default - $vendorDir = $this->appDir . '/vendor'; + $vendorDir = $this->appDir.'/vendor'; mkdir($vendorDir, 0755, true); - file_put_contents($vendorDir . '/VendorFile.php', 'artisan('enumify:refactor', [ '--path' => $this->appDir, @@ -667,7 +667,7 @@ class NotAnEnumHelper $orders = $query->where('status', 'pending')->get(); PHP; - file_put_contents($this->appDir . '/NoNamespaceQuery.php', $noNamespaceFile); + file_put_contents($this->appDir.'/NoNamespaceQuery.php', $noNamespaceFile); $this->artisan('enumify:refactor', [ '--path' => $this->appDir, @@ -675,7 +675,7 @@ class NotAnEnumHelper ])->assertSuccessful(); // The file should be modified but no imports added - $content = file_get_contents($this->appDir . '/NoNamespaceQuery.php'); + $content = file_get_contents($this->appDir.'/NoNamespaceQuery.php'); expect($content)->not->toContain('use '); }); }); @@ -698,7 +698,7 @@ class NotAnEnumHelper '--backup' => true, ])->assertSuccessful(); - $backupDirs = glob($this->backupDir . '/*'); + $backupDirs = glob($this->backupDir.'/*'); expect($backupDirs)->not->toBeEmpty(); }); @@ -710,10 +710,10 @@ class NotAnEnumHelper ])->assertSuccessful(); // Find backup file - $backupDirs = glob($this->backupDir . '/*'); + $backupDirs = glob($this->backupDir.'/*'); expect($backupDirs)->not->toBeEmpty(); - $backupFiles = glob($backupDirs[0] . '/*'); + $backupFiles = glob($backupDirs[0].'/*'); expect($backupFiles)->not->toBeEmpty(); }); }); @@ -726,12 +726,12 @@ class NotAnEnumHelper ])->assertSuccessful(); // Controller should have method call patterns fixed - $controllerContent = file_get_contents($this->appDir . '/OrderController.php'); + $controllerContent = file_get_contents($this->appDir.'/OrderController.php'); // Check that at least one pattern was replaced with an enum reference expect($controllerContent)->toMatch('/[A-Za-z]+Status::[A-Za-z_]+/'); // Request should have array patterns fixed - $requestContent = file_get_contents($this->appDir . '/OrderRequest.php'); + $requestContent = file_get_contents($this->appDir.'/OrderRequest.php'); // Check that the status array pattern was replaced (could be mixed case) expect($requestContent)->toMatch('/[A-Za-z]+Status::[A-Za-z_]+/'); }); 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 +}