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
This commit is contained in:
idevakk
2025-11-30 09:27:37 -08:00
parent a84a4a0c15
commit 659325c01d
2 changed files with 621 additions and 5 deletions

View File

@@ -7,15 +7,455 @@ use Illuminate\Database\Seeder;
class DatabaseSeeder extends 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. * Seed the application's database.
*/ */
public function run(): void public function run(): void
{ {
$this->call([ $this->command->info('🌱 Welcome to Interactive Database Seeder!');
AdminSeeder::class, $this->command->info('');
MetaSeeder::class, $this->command->info('Use arrow keys to navigate, spacebar to toggle selection, Enter to run:');
SettingsSeeder::class, $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));
}
} }
} }

View File

@@ -0,0 +1,176 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class NewSettingsSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$this->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]
);
}
}