discoverSeeders(); } /** * Automatically discover all seeders in the database/seeders directory. */ private function discoverSeeders(): void { $seederPath = database_path('seeders'); $seederFiles = glob($seederPath.'/*Seeder.php'); foreach ($seederFiles as $file) { $className = basename($file, '.php'); $fullClassName = "Database\\Seeders\\{$className}"; // Skip DatabaseSeeder itself to avoid recursion if ($className === 'DatabaseSeeder') { continue; } // Check if class exists and is instantiable if (class_exists($fullClassName) && is_subclass_of($fullClassName, Seeder::class)) { $this->availableSeeders[$className] = [ 'class' => $fullClassName, 'description' => $this->generateSeederDescription($className), 'default' => $this->getDefaultSelection($className), ]; } } } /** * Generate description for seeder based on its name. */ private function generateSeederDescription(string $className): string { // Common patterns for seeder descriptions $descriptions = [ 'AdminSeeder' => 'Create admin user with interactive password prompt', 'MetaSeeder' => 'Seed meta tags and SEO data', 'NewSettingsSeeder' => 'Seed Filament settings (website, IMAP, configuration)', 'UserSeeder' => 'Seed sample users', 'PlanSeeder' => 'Seed subscription plans', 'CategorySeeder' => 'Seed categories', 'PostSeeder' => 'Seed blog posts', 'ProductSeeder' => 'Seed products', 'OrderSeeder' => 'Seed sample orders', 'PermissionSeeder' => 'Seed permissions and roles', 'CountrySeeder' => 'Seed countries data', 'LanguageSeeder' => 'Seed languages data', 'CurrencySeeder' => 'Seed currencies data', 'PaymentSeeder' => 'Seed payment methods and data', 'PaymentProviderSeeder' => 'Seed payment providers (Stripe, Lemon Squeezy, Polar, etc.)', 'EmailSeeder' => 'Seed email templates', 'NotificationSeeder' => 'Seed notification templates', 'SettingsSeeder' => 'Seed application settings', 'TestSeeder' => 'Seed test data', 'DemoSeeder' => 'Seed demo data', 'SampleSeeder' => 'Seed sample data', ]; if (isset($descriptions[$className])) { return $descriptions[$className]; } // Generate description based on class name pattern $name = strtolower(str_replace('Seeder', '', $className)); // Simple pluralization if (str_ends_with($name, 'y')) { $name = substr($name, 0, -1).'ies'; } elseif (! str_ends_with($name, 's')) { $name .= 's'; } return "Seed {$name}"; } /** * Determine if a seeder should be selected by default. */ private function getDefaultSelection(string $className): bool { // Core/essential seeders that should run by default $essentialSeeders = [ 'AdminSeeder', 'NewSettingsSeeder', 'SettingsSeeder', 'MetaSeeder', ]; return in_array($className, $essentialSeeders); } /** * Seed the application's database. */ public function run(): void { $this->command->info('🌱 Welcome to Interactive Database Seeder!'); $this->command->info(''); $this->command->info('Use arrow keys to navigate, spacebar to toggle selection, Enter to run:'); $this->command->info(''); $selectedSeeders = $this->runInteractiveSelection(); if (empty($selectedSeeders)) { $this->command->warn('❌ No seeders selected. Exiting...'); return; } $this->command->info(''); $this->command->info('🚀 Running '.count($selectedSeeders).' selected seeders:'); // Run selected seeders foreach ($selectedSeeders as $seederName) { $seederInfo = $this->availableSeeders[$seederName]; $this->command->info(" 📦 Running {$seederName}..."); try { $this->call($seederInfo['class']); $this->command->info(" ✅ {$seederName} completed successfully"); } catch (\Exception $e) { $this->command->error(" ❌ {$seederName} failed: {$e->getMessage()}"); } } $this->command->info(''); $this->command->info('🎉 Database seeding completed!'); } /** * Run interactive checkbox selection. */ private function runInteractiveSelection(): array { // Initialize selection with defaults $selection = []; foreach ($this->availableSeeders as $name => $info) { $selection[$name] = $info['default']; } $seederNames = array_keys($this->availableSeeders); $currentIndex = 0; // Check if we're on Windows or have limited terminal capabilities if ($this->isWindows() || ! $this->supportsInteractiveTerminal()) { return $this->runFallbackSelection($seederNames, $selection); } // Set up terminal for interactive input (Unix-like systems) if (function_exists('readline_callback_handler_install')) { readline_callback_handler_install('', function () {}); } // Set terminal to raw mode for character input shell_exec('stty -icanon -echo'); try { while (true) { // Clear screen and redraw $this->clearScreen(); $this->displayCheckboxInterface($seederNames, $currentIndex, $selection); // Read single character $read = [STDIN]; $write = []; $except = []; $n = stream_select($read, $write, $except, null); if ($n && in_array(STDIN, $read)) { $char = fgetc(STDIN); switch ($char) { case "\033": // Escape sequence (arrow keys) $this->handleArrowKey($currentIndex, $seederNames, $currentIndex); break; case ' ': // Spacebar - toggle selection $currentSeeder = $seederNames[$currentIndex]; $selection[$currentSeeder] = ! $selection[$currentSeeder]; break; case "\n": // Enter - confirm selection case "\r": $this->restoreTerminal(); if (function_exists('readline_callback_handler_remove')) { readline_callback_handler_remove(); } return array_keys(array_filter($selection)); case 'a': // Select all case 'A': foreach ($selection as $key => $value) { $selection[$key] = true; } break; case 'n': // Select none case 'N': foreach ($selection as $key => $value) { $selection[$key] = false; } break; case 'q': // Quit case 'Q': case "\x03": // Ctrl+C $this->restoreTerminal(); if (function_exists('readline_callback_handler_remove')) { readline_callback_handler_remove(); } return []; } } } } finally { $this->restoreTerminal(); if (function_exists('readline_callback_handler_remove')) { readline_callback_handler_remove(); } } } /** * Handle arrow key sequences. */ private function handleArrowKey(int &$currentIndex, array $seederNames, int &$newIndex): void { // Read the next two characters of the escape sequence $char1 = fgetc(STDIN); $char2 = fgetc(STDIN); if ($char1 === '[') { switch ($char2) { case 'A': // Up arrow $currentIndex = max(0, $currentIndex - 1); break; case 'B': // Down arrow $currentIndex = min(count($seederNames) - 1, $currentIndex + 1); break; } } $newIndex = $currentIndex; } /** * Display the checkbox interface. */ private function displayCheckboxInterface(array $seederNames, int $currentIndex, array $selection): void { $this->command->info('┌─────────────────────────────────────────────────────────────┐'); $this->command->info('│ Select Seeders │'); $this->command->info('│ │'); $this->command->info('│ ↑↓ : Navigate Space : Toggle A : All N : None │'); $this->command->info('│ Enter : Run Q : Quit │'); $this->command->info('└─────────────────────────────────────────────────────────────┘'); $this->command->info(''); foreach ($seederNames as $index => $seederName) { $info = $this->availableSeeders[$seederName]; $isSelected = $selection[$seederName]; $isCurrent = $index === $currentIndex; $checkbox = $isSelected ? '✅' : '⭕'; $marker = $isCurrent ? '►' : ' '; $description = $info['description']; // Truncate description if too long if (strlen($description) > 50) { $description = substr($description, 0, 47).'...'; } $line = sprintf(' %s %s %-20s %s', $marker, $checkbox, $seederName, $description); if ($isCurrent) { $this->command->line($line); } else { $this->command->info($line); } } $this->command->info(''); $selectedCount = count(array_filter($selection)); $totalCount = count($seederNames); $this->command->info("Selected: {$selectedCount}/{$totalCount} seeders"); } /** * Clear the terminal screen. */ private function clearScreen(): void { // ANSI escape code to clear screen and move cursor to top-left echo "\033[2J\033[H"; } /** * Check if running on Windows. */ private function isWindows(): bool { return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; } /** * Check if terminal supports interactive features. */ private function supportsInteractiveTerminal(): bool { return function_exists('shell_exec') && shell_exec('which stty') !== null; } /** * Restore terminal settings. */ private function restoreTerminal(): void { if (! $this->isWindows() && $this->supportsInteractiveTerminal()) { shell_exec('stty icanon echo'); } } /** * Fallback selection method for Windows or limited terminals. */ private function runFallbackSelection(array $seederNames, array $selection): array { $this->command->info('🌱 Available Seeders (Windows/Basic Mode):'); $this->command->info(''); foreach ($seederNames as $index => $seederName) { $info = $this->availableSeeders[$seederName]; $isSelected = $selection[$seederName]; $status = $isSelected ? '✅' : '⭕'; $description = $info['description']; if (strlen($description) > 50) { $description = substr($description, 0, 47).'...'; } $this->command->line(sprintf(' [%d] %s %s - %s', $index + 1, $status, $seederName, $description)); } $this->command->info(''); $this->command->info('Options:'); $this->command->line(' • Enter numbers (e.g., "1 3 5") to toggle selection'); $this->command->line(' • Type "all" to select all'); $this->command->line(' • Type "none" to deselect all'); $this->command->line(' • Type "list" to show current selection'); $this->command->line(' • Press Enter to run selected seeders'); $this->command->line(' • Type "exit" to cancel'); $this->command->info(''); while (true) { $input = trim($this->command->ask('Enter your choice (or press Enter to run)')); if (empty($input)) { break; // Run with current selection } $input = strtolower($input); switch ($input) { case 'exit': case 'quit': $this->command->warn('👋 Exiting seeder...'); return []; case 'all': foreach ($selection as $key => $value) { $selection[$key] = true; } $this->displayCurrentSelection($selection); break; case 'none': foreach ($selection as $key => $value) { $selection[$key] = false; } $this->displayCurrentSelection($selection); break; case 'list': $this->displayCurrentSelection($selection); break; default: // Toggle by numbers $numbers = preg_split('/\s+/', $input); $toggled = false; foreach ($numbers as $num) { if (is_numeric($num)) { $index = (int) $num - 1; if (isset($seederNames[$index])) { $seederName = $seederNames[$index]; $selection[$seederName] = ! $selection[$seederName]; $status = $selection[$seederName] ? 'selected' : 'deselected'; $this->command->line(" ✓ {$seederName} {$status}"); $toggled = true; } } } if (! $toggled) { $this->command->warn(" ⚠️ Invalid input: {$input}"); $this->command->line(' Try numbers (1, 2, 3) or commands: all, none, list, exit'); } break; } } return array_keys(array_filter($selection)); } /** * Display current selection for fallback mode. */ private function displayCurrentSelection(array $selection): void { $selected = array_keys(array_filter($selection)); if (empty($selected)) { $this->command->line(' ⭕ No seeders selected'); } else { $this->command->line(' ✅ Selected: '.implode(', ', $selected)); } } /** * Display current selection. */ private function displaySelection(array $selected): void { if (empty($selected)) { $this->command->line(' ⭕ No seeders selected'); } else { $this->command->line(' ✅ Currently selected: '.implode(', ', $selected)); } } }