Files
nexus/src/App/Database.php
Lars Gebhardt-Kusche 677f9314f5
All checks were successful
Deploy / deploy-staging (push) Successful in 6s
Deploy / deploy-production (push) Has been skipped
KEA Setup
2026-04-13 02:13:43 +02:00

247 lines
7.0 KiB
PHP
Executable File

<?php
declare(strict_types=1);
namespace App;
final class Database
{
public static function createPdo(Config $config): ?\PDO
{
if (!$config->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;
}
}