247 lines
7.0 KiB
PHP
Executable File
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;
|
|
}
|
|
}
|