Files
mariavel/backend/app/Services/Database/MySqlDriver.php
T

266 lines
9.4 KiB
PHP

<?php
namespace App\Services\Database;
use App\Contracts\DatabaseDriverInterface;
use App\Contracts\SchemaDiscoveryInterface;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Config;
class MySqlDriver implements DatabaseDriverInterface, SchemaDiscoveryInterface
{
protected string $connectionName = 'mariavel_dynamic';
public function connect(array $config): bool
{
Config::set("database.connections.{$this->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
{
return $this->query("SELECT * FROM `{$table}` LIMIT ? OFFSET ?", [$limit, $offset]);
}
public function getTableCount(string $table): int
{
$result = $this->query("SELECT COUNT(*) as count FROM `{$table}`");
return (int) ($result[0]->count ?? 0);
}
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
{
$filename = 'backup-' . ($config['database'] ?? 'all') . '-' . date('Y-m-d-H-i-s') . '.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');
// Ensure we have a username
$username = $config['username'] ?? 'root';
$password = $config['password'] ?? '';
$host = $config['host'] ?? '127.0.0.1';
$port = $config['port'] ?? '3306';
$database = $config['database'] ?? '';
$table = $config['table'] ?? '';
// 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;
}
}