dbEnabled) { return null; } try { $pdo = self::createFromArray($config->db); self::ensureKeaSchema($pdo, [ 'auto_init' => $config->dbAutoInit, 'init_cmd' => $config->dbInitCmd, 'init_script' => $config->dbInitScript, 'kea_db_version' => $config->keaDbVersion, ]); return $pdo; } catch (\PDOException $e) { http_response_code(500); $dbDebug = defined('APP_DB_DEBUG') && APP_DB_DEBUG; $details = 'Database connection error.'; if ($dbDebug) { $details .= ' ' . $e->getMessage(); } error_log('[DB] ' . $e->getMessage()); echo $details; exit; } } public static function createBasePdo(Config $config): ?\PDO { if (!$config->baseDbEnabled || empty($config->baseDb)) { return null; } try { return self::createFromArray($config->baseDb); } catch (\PDOException $e) { http_response_code(500); $details = 'Base database connection error.'; error_log('[Base DB] ' . $e->getMessage()); echo $details; exit; } } public static function createFromArray(array $db): \PDO { $driver = (string)($db['driver'] ?? ''); if ($driver === '') { throw new \RuntimeException('DB config missing "driver"'); } $dsn = match ($driver) { 'mysql' => self::buildMysqlDsn($db), 'pgsql' => self::buildPgsqlDsn($db), 'sqlite' => self::buildSqliteDsn($db), default => throw new \RuntimeException('Unsupported PDO driver: ' . $driver), }; $pdo = new \PDO( $dsn, (string)($db['user'] ?? ''), (string)($db['password'] ?? ''), (array)($db['options'] ?? []) ); if ($driver === 'pgsql' && !empty($db['schema'])) { $schema = preg_replace('/[^a-zA-Z0-9_]/', '', (string)$db['schema']); if ($schema !== '') { $pdo->exec('SET search_path TO ' . $schema); } } return $pdo; } public static function ensureKeaSchema(\PDO $pdo, array $options): void { $driver = (string)$pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); if ($driver !== 'pgsql') { return; } if (!self::tableExists($pdo, 'hosts')) { if (!empty($options['auto_init'])) { self::initKeaSchema($pdo, $options); } } } private static function tableExists(\PDO $pdo, string $table): bool { $stmt = $pdo->prepare( "SELECT 1 FROM information_schema.tables WHERE table_schema = current_schema() AND table_name = :table LIMIT 1" ); $stmt->execute(['table' => $table]); return (bool)$stmt->fetchColumn(); } private static function initKeaSchema(\PDO $pdo, array $options): void { if (!empty($options['init_cmd'])) { self::runInitCommand((string)$options['init_cmd']); return; } $script = $options['init_script'] ?? null; if (!$script && !empty($options['kea_db_version'])) { $script = __DIR__ . '/../../tools/sql/kea/' . $options['kea_db_version'] . '/dhcpdb_create.pgsql'; } if ($script) { self::execSqlFile($pdo, (string)$script); return; } throw new \RuntimeException( 'DB auto-init enabled but no APP_DB_INIT_CMD or APP_DB_INIT_SCRIPT configured.' ); } private static function runInitCommand(string $cmd): void { $descriptor = [ 1 => ['pipe', 'w'], 2 => ['pipe', 'w'], ]; $process = proc_open($cmd, $descriptor, $pipes); if (!is_resource($process)) { throw new \RuntimeException('Failed to start DB init command.'); } $stdout = stream_get_contents($pipes[1]); $stderr = stream_get_contents($pipes[2]); foreach ($pipes as $pipe) { fclose($pipe); } $code = proc_close($process); if ($code !== 0) { throw new \RuntimeException('DB init command failed: ' . trim($stderr ?: $stdout)); } } private static function execSqlFile(\PDO $pdo, string $path): void { if (!is_readable($path)) { throw new \RuntimeException('DB init script not readable: ' . $path); } $sql = file_get_contents($path); if ($sql === false) { throw new \RuntimeException('Failed to read DB init script: ' . $path); } // Strip psql meta-commands (lines starting with backslash) $sql = preg_replace('/^\\\\.+$/m', '', $sql); $pdo->exec($sql); } private static function buildMysqlDsn(array $db): string { if (empty($db['dbname'])) { throw new \RuntimeException('MySQL config missing "dbname"'); } $charset = (string)($db['charset'] ?? 'utf8mb4'); // Unix socket takes precedence if (!empty($db['unix_socket'])) { return sprintf( 'mysql:unix_socket=%s;dbname=%s;charset=%s', (string)$db['unix_socket'], (string)$db['dbname'], $charset ); } $host = (string)($db['host'] ?? 'localhost'); $port = (int)($db['port'] ?? 3306); return sprintf( 'mysql:host=%s;port=%d;dbname=%s;charset=%s', $host, $port, (string)$db['dbname'], $charset ); } private static function buildPgsqlDsn(array $db): string { if (empty($db['dbname'])) { throw new \RuntimeException('PostgreSQL config missing "dbname"'); } $host = (string)($db['host'] ?? 'localhost'); $port = (int)($db['port'] ?? 5432); // Hinweis: charset gehört bei pgsql nicht in den DSN return sprintf( 'pgsql:host=%s;port=%d;dbname=%s', $host, $port, (string)$db['dbname'] ); } private static function buildSqliteDsn(array $db): string { // SQLite kann :memory: oder einen Pfad nutzen $path = (string)($db['path'] ?? ''); if ($path === '') { // Default: Memory-DB $path = ':memory:'; } // Wenn es ein Pfad ist, stelle sicher, dass das Verzeichnis existiert. if ($path !== ':memory:') { $dir = \dirname($path); if ($dir && !is_dir($dir)) { @mkdir($dir, 0775, true); } } return 'sqlite:' . $path; } }