From 659325c01da655bb115c882f4027664a931811ab Mon Sep 17 00:00:00 2001 From: idevakk <219866223+idevakk@users.noreply.github.com> Date: Sun, 30 Nov 2025 09:27:37 -0800 Subject: [PATCH] feat(seeder): implement interactive database seeder with auto-discovery - Add dynamic seeder discovery from database/seeders directory - Create interactive checkbox interface for Unix systems with arrow key navigation - Implement Windows-compatible fallback mode with number-based selection - Add cross-platform terminal detection and appropriate interface handling - Include descriptive information and default selection for each seeder - Support bulk operations (select all/none) and individual toggling --- database/seeders/DatabaseSeeder.php | 450 ++++++++++++++++++++++++- database/seeders/NewSettingsSeeder.php | 176 ++++++++++ 2 files changed, 621 insertions(+), 5 deletions(-) create mode 100644 database/seeders/NewSettingsSeeder.php diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index ceda2e2..cd30bda 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -7,15 +7,455 @@ use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { + /** + * Available seeders with descriptions. + */ + private array $availableSeeders = []; + + public function __construct() + { + $this->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', + '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->call([ - AdminSeeder::class, - MetaSeeder::class, - SettingsSeeder::class, - ]); + $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)); + } } } diff --git a/database/seeders/NewSettingsSeeder.php b/database/seeders/NewSettingsSeeder.php new file mode 100644 index 0000000..c3596c8 --- /dev/null +++ b/database/seeders/NewSettingsSeeder.php @@ -0,0 +1,176 @@ +command->info('Seeding new settings system...'); + + // Clear only the settings groups we're about to insert + DB::table('db_config')->whereIn('group', ['website', 'imap', 'configuration'])->delete(); + + // Website Settings (based on WebsiteSettings.php) + $websiteSettings = [ + 'app_name' => 'ZEmailnator', + 'app_version' => '1.0', + 'app_base_url' => 'https://zemailnator.test', + 'app_admin' => config('app.admin_email', 'admin@zemail.me'), + 'app_contact' => 'support@zemail.me', + 'app_title' => 'ZEmailnator - Temporary Email Service', + 'app_description' => 'Free temporary email service for protecting your privacy', + 'app_keyword' => 'temporary email, disposable email, fake email', + 'app_meta' => [ + 'author' => 'ZEmailnator', + 'version' => '1.0', + 'generator' => 'ZEmailnator v1.0', + ], + 'app_social' => [ + ['icon' => 'fab-twitter', 'url' => 'https://twitter.com/zemailnator'], + ['icon' => 'fab-github', 'url' => 'https://github.com/zemailnator'], + ['icon' => 'fab-discord', 'url' => 'https://discord.gg/zemailnator'], + ], + 'app_header' => '', + 'app_footer' => '', + 'ads_settings' => [ + 'one' => '', + 'two' => '', + 'three' => '', + 'four' => '', + 'five' => '', + ], + ]; + + // IMAP Settings (based on ImapSettings.php) + $imapSettings = [ + 'public' => [ + 'host' => 'test.com', + 'port' => '587', + 'encryption' => 'ssl', + 'validate_cert' => false, + 'username' => 'user', + 'password' => 'pass', + 'default_account' => 'default', + 'protocol' => 'imap', + 'cc_check' => false, + ], + 'premium' => [ + 'host' => 'imap.gmail.com', + 'port' => '993', + 'encryption' => 'ssl', + 'validate_cert' => true, + 'username' => 'premium@yourdomain.com', + 'password' => 'premium-app-password', + 'default_account' => 'default', + 'protocol' => 'imap', + 'cc_check' => true, + ], + ]; + + // Configuration Settings (based on ConfigurationSettings.php) + $configurationSettings = [ + 'enable_masking_external_link' => true, + 'disable_mailbox_slug' => false, + 'enable_create_from_url' => true, + 'enable_ad_block_detector' => true, + 'font_family' => [ + 'head' => 'Poppins', + 'body' => 'Inter', + ], + 'default_language' => 'en', + 'add_mail_in_title' => false, + 'disable_used_email' => false, + 'fetch_seconds' => 15, + 'email_limit' => 10, + 'fetch_messages_limit' => 15, + 'cron_password' => Str::random(20), + 'date_format' => 'd M Y h:i A', + 'custom_username_length_min' => 3, + 'custom_username_length_max' => 20, + 'random_username_length_min' => 6, + 'random_username_length_max' => 12, + 'after_last_email_delete' => 'redirect_to_homepage', + 'forbidden_ids' => [ + ['forbidden_id' => 'admin'], + ['forbidden_id' => 'root'], + ['forbidden_id' => 'test'], + ['forbidden_id' => 'api'], + ['forbidden_id' => 'mail'], + ['forbidden_id' => 'support'], + ['forbidden_id' => 'noreply'], + ['forbidden_id' => 'info'], + ], + 'blocked_domains' => [ + ['blocked_domain' => 'spam.com'], + ['blocked_domain' => 'malware.net'], + ['blocked_domain' => 'phishing.site'], + ['blocked_domain' => 'blocked.com'], + ], + ]; + + // Insert settings into db_config table + $this->seedWebsiteSettings($websiteSettings); + $this->seedImapSettings($imapSettings); + $this->seedConfigurationSettings($configurationSettings); + + $this->command->info('✅ New settings system seeded successfully!'); + $this->command->info('📧 Website: '.count($websiteSettings).' settings'); + $this->command->info('🔧 IMAP: '.count($imapSettings['public']) + count($imapSettings['premium']).' settings'); + $this->command->info('⚙️ Configuration: '.count($configurationSettings).' settings'); + } + + /** + * Seed website settings. + */ + private function seedWebsiteSettings(array $settings): void + { + foreach ($settings as $key => $value) { + $this->insertSetting('website', $key, $value); + } + } + + /** + * Seed IMAP settings. + */ + private function seedImapSettings(array $settings): void + { + foreach ($settings as $key => $value) { + $this->insertSetting('imap', $key, $value); + } + } + + /** + * Seed configuration settings. + */ + private function seedConfigurationSettings(array $settings): void + { + foreach ($settings as $key => $value) { + $this->insertSetting('configuration', $key, $value); + } + } + + /** + * Insert a single setting using raw SQL. + */ + private function insertSetting(string $group, string $key, $value): void + { + // Always JSON encode to satisfy the JSON_VALID constraint + $encodedValue = json_encode($value); + $now = now()->toDateTimeString(); + + // Use raw SQL for insert + DB::statement( + 'INSERT INTO `db_config` (`group`, `key`, `settings`, `created_at`, `updated_at`) + VALUES (?, ?, ?, ?, ?)', + [$group, $key, $encodedValue, $now, $now] + ); + } +}