connectionName}", [ 'driver' => 'mysql', 'host' => $config['host'] ?? '127.0.0.1', 'port' => $config['port'] ?? '3306', 'database' => $config['database'] ?? null, 'username' => $config['username'] ?? 'root', 'password' => $config['password'] ?? '', 'charset' => 'utf8mb4', 'collation' => 'utf8mb4_unicode_ci', 'prefix' => '', 'strict' => false, 'engine' => null, ]); try { DB::purge($this->connectionName); DB::connection($this->connectionName)->getPdo(); return true; } catch (\Exception $e) { return false; } } public function query(string $sql, array $bindings = []): array { return DB::connection($this->connectionName)->select($sql, $bindings); } public function getConnection() { return DB::connection($this->connectionName); } public function getDatabases(): array { $results = $this->query('SHOW DATABASES'); return array_map(fn($db) => $db->Database, $results); } public function getTables(): array { $results = $this->query('SHOW TABLES'); $key = "Tables_in_" . DB::connection($this->connectionName)->getDatabaseName(); return array_map(fn($table) => $table->$key, $results); } public function getTableSchema(string $table): array { return $this->query("DESCRIBE `{$table}`"); } public function getTableData(string $table, int $limit = 100, int $offset = 0, array $filters = []): array { $query = DB::connection($this->connectionName)->table($table); $this->applyFilters($query, $filters); return $query->limit($limit)->offset($offset)->get()->all(); } public function getTableCount(string $table, array $filters = []): int { $query = DB::connection($this->connectionName)->table($table); $this->applyFilters($query, $filters); return $query->count(); } protected function applyFilters($query, array $filters) { if (empty($filters)) return $query; foreach ($filters as $filter) { $field = $filter['field'] ?? ($filter['columnField'] ?? null); $operator = $filter['operator'] ?? ($filter['operatorValue'] ?? null); $value = $filter['value'] ?? null; if (!$field) continue; switch ($operator) { case 'contains': $query->where($field, 'LIKE', "%{$value}%"); break; case 'equals': case '=': $query->where($field, '=', $value); break; case 'startsWith': $query->where($field, 'LIKE', "{$value}%"); break; case 'endsWith': $query->where($field, 'LIKE', "%{$value}"); break; case 'isEmpty': $query->where(function($q) use ($field) { $q->whereNull($field)->orWhere($field, ''); }); break; case 'isNotEmpty': $query->whereNotNull($field)->where($field, '!=', ''); break; case 'isAnyOf': if (is_array($value)) { $query->whereIn($field, $value); } break; case '>': case '<': case '>=': case '<=': case '!=': $query->where($field, $operator, $value); break; } } return $query; } public function getForeignKeys(string $table): array { $sql = " SELECT COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_NAME = ? AND REFERENCED_TABLE_NAME IS NOT NULL AND TABLE_SCHEMA = ? "; $dbName = DB::connection($this->connectionName)->getDatabaseName(); return $this->query($sql, [$table, $dbName]); } public function export(array $config): string { $database = $config['database'] ?? ''; $table = $config['table'] ?? ''; $filename = !empty($table) ? "{$table}-" . date('Y-m-d') . ".sql" : "backup-" . ($database ?: 'all') . "-" . date('Y-m-d') . ".sql"; $directory = storage_path('app/backups'); if (!is_dir($directory)) { mkdir($directory, 0755, true); } $path = $directory . DIRECTORY_SEPARATOR . $filename; $errorPath = $directory . DIRECTORY_SEPARATOR . $filename . '.err'; $mysqldumpPath = env('MYSQLDUMP_PATH', 'mysqldump'); $username = $config['username'] ?? 'root'; $password = $config['password'] ?? ''; $host = $config['host'] ?? '127.0.0.1'; $port = $config['port'] ?? '3306'; // Build command with flags for a complete and resilient export $passwordPart = !empty($password) ? "-p" . escapeshellarg($password) : ""; if (!empty($table) && !empty($database)) { $dbPart = escapeshellarg($database) . " " . escapeshellarg($table); } else { $dbPart = !empty($database) ? escapeshellarg($database) : "--all-databases"; } // --single-transaction: for InnoDB tables, ensures consistency without locking // --skip-lock-tables: avoids issues with views/definers that might prevent locking // --routines, --triggers, --events: ensures all database objects are included // --quick: useful for large tables $flags = "--single-transaction --skip-lock-tables --routines --triggers --events --quick"; $command = sprintf( '%s -u %s %s -h %s -P %s %s %s > %s 2> %s', $mysqldumpPath === 'mysqldump' ? 'mysqldump' : escapeshellarg($mysqldumpPath), escapeshellarg($username), $passwordPart, escapeshellarg($host), escapeshellarg($port), $flags, $dbPart, escapeshellarg($path), escapeshellarg($errorPath) ); shell_exec($command); if (file_exists($errorPath)) { $errors = file_get_contents($errorPath); unlink($errorPath); // Some errors are fatal even if some output was produced (like the definer issue) if (!empty(trim($errors)) && (str_contains(strtolower($errors), 'error') || !file_exists($path) || filesize($path) < 1000)) { // If it's just a warning (like SSL), we might want to continue, // but if it's a "Got error", we should probably fail. if (str_contains(strtolower($errors), 'error')) { throw new \Exception("Export failed: " . $errors); } } } if (!file_exists($path) || filesize($path) === 0) { $instruction = "The 'mysqldump' binary was not found. Please set MYSQLDUMP_PATH in your .env file to the absolute path of the mysqldump executable."; throw new \Exception("Export failed: Binary not found.\n\nInstructions: " . $instruction); } return $path; } public function import(array $config, string $filePath): bool { $directory = storage_path('app/backups'); if (!is_dir($directory)) { mkdir($directory, 0755, true); } $errorPath = $directory . DIRECTORY_SEPARATOR . 'import_error.err'; $mysqlPath = env('MYSQL_BINARY_PATH', 'mysql'); $username = $config['username'] ?? 'root'; $password = $config['password'] ?? ''; $host = $config['host'] ?? '127.0.0.1'; $port = $config['port'] ?? '3306'; $database = $config['database'] ?? ''; $passwordPart = !empty($password) ? "-p" . escapeshellarg($password) : ""; $dbPart = !empty($database) ? escapeshellarg($database) : ""; $command = sprintf( '%s -u %s %s -h %s -P %s %s < %s 2> %s', $mysqlPath === 'mysql' ? 'mysql' : escapeshellarg($mysqlPath), escapeshellarg($username), $passwordPart, escapeshellarg($host), escapeshellarg($port), $dbPart, escapeshellarg($filePath), escapeshellarg($errorPath) ); shell_exec($command); if (file_exists($errorPath)) { $errors = file_get_contents($errorPath); unlink($errorPath); if (!empty(trim($errors))) { $instruction = "Please check your .env file and ensure MYSQL_BINARY_PATH is correct. Example: MYSQL_BINARY_PATH=C:\\xampp\\mysql\\bin\\mysql.exe"; throw new \Exception("Import failed: " . $errors . "\n\nInstructions: " . $instruction); } } return true; } public function getDatabaseMetadata(string $database): array { $sql = " SELECT s.DEFAULT_CHARACTER_SET_NAME as charset, s.DEFAULT_COLLATION_NAME as collation, (SELECT SUM(data_length + index_length) FROM information_schema.TABLES WHERE table_schema = s.SCHEMA_NAME) as size_bytes, (SELECT COUNT(*) FROM information_schema.TABLES WHERE table_schema = s.SCHEMA_NAME) as table_count, (SELECT COUNT(*) FROM information_schema.VIEWS WHERE table_schema = s.SCHEMA_NAME) as view_count FROM information_schema.SCHEMATA s WHERE s.SCHEMA_NAME = ? "; $results = $this->query($sql, [$database]); return (array) ($results[0] ?? []); } public function getTableMetadata(string $database, string $table): array { $sql = " SELECT ENGINE as `engine`, TABLE_ROWS as `rows`, DATA_LENGTH as `data_length`, INDEX_LENGTH as `index_length`, DATA_FREE as `data_free`, AUTO_INCREMENT as `auto_increment`, CREATE_TIME as `create_time`, UPDATE_TIME as `update_time`, TABLE_COLLATION as `collation`, TABLE_COMMENT as `comment` FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? "; $results = $this->query($sql, [$database, $table]); return (array) ($results[0] ?? []); } public function truncateTable(string $table): bool { DB::connection($this->connectionName)->statement("TRUNCATE TABLE `{$table}`"); return true; } public function dropTable(string $table): bool { DB::connection($this->connectionName)->statement("DROP TABLE `{$table}`"); return true; } public function optimizeTable(string $table): bool { DB::connection($this->connectionName)->statement("OPTIMIZE TABLE `{$table}`"); return true; } public function getTablesMetadata(string $database): array { $sql = " SELECT TABLE_NAME as `name`, ENGINE as `engine`, TABLE_ROWS as `rows`, DATA_LENGTH as `data_length`, INDEX_LENGTH as `index_length`, DATA_FREE as `data_free`, AUTO_INCREMENT as `auto_increment`, CREATE_TIME as `create_time`, UPDATE_TIME as `update_time`, TABLE_COLLATION as `collation`, TABLE_COMMENT as `comment` FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME ASC "; return $this->query($sql, [$database]); } public function batchUpdate(string $table, array $changes): bool { $connection = $this->getConnection(); // Find primary key $schema = $this->getTableSchema($table); $primaryKey = 'id'; // default foreach ($schema as $col) { if (($col->Key ?? '') === 'PRI') { $primaryKey = $col->Field; break; } } $connection->beginTransaction(); try { foreach ($changes as $change) { if (!isset($change[$primaryKey])) { continue; } $id = $change[$primaryKey]; $updateData = $change; unset($updateData[$primaryKey]); if (empty($updateData)) continue; $connection->table($table)->where($primaryKey, $id)->update($updateData); } $connection->commit(); return true; } catch (\Exception $e) { $connection->rollBack(); throw $e; } } }