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:
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
176
database/seeders/NewSettingsSeeder.php
Normal file
176
database/seeders/NewSettingsSeeder.php
Normal 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]
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user