feat: Add install-support.php and improve installer error handling

This commit is contained in:
2026-03-21 20:25:31 +01:00
parent cb6e4e7dcb
commit 2d2f80fe76
4 changed files with 332 additions and 286 deletions
+1 -281
View File
@@ -2,284 +2,4 @@
declare(strict_types=1);
function scripts_project_root(): string
{
return dirname(__DIR__);
}
function scripts_saas_app_path(): string
{
return scripts_project_root() . DIRECTORY_SEPARATOR . 'saas-app';
}
function scripts_env_example_path(): string
{
return scripts_saas_app_path() . DIRECTORY_SEPARATOR . '.env.example';
}
function scripts_env_path(): string
{
return scripts_saas_app_path() . DIRECTORY_SEPARATOR . '.env';
}
function scripts_bundle_output_path(): string
{
return scripts_saas_app_path()
. DIRECTORY_SEPARATOR . 'database'
. DIRECTORY_SEPARATOR . 'migrations'
. DIRECTORY_SEPARATOR . 'generated'
. DIRECTORY_SEPARATOR . 'all-migrations.sql';
}
function scripts_installer_lock_path(): string
{
return scripts_saas_app_path() . DIRECTORY_SEPARATOR . '.installer.lock';
}
function scripts_check_command(string $command): bool
{
$where = stripos(PHP_OS_FAMILY, 'Windows') === 0 ? 'where' : 'command -v';
$output = [];
$exitCode = 0;
$redirect = stripos(PHP_OS_FAMILY, 'Windows') === 0 ? ' 2>NUL' : ' 2>/dev/null';
@exec($where . ' ' . escapeshellarg($command) . $redirect, $output, $exitCode);
return $exitCode === 0;
}
function scripts_parse_options(array $argv): array
{
$options = [];
foreach (array_slice($argv, 1) as $arg) {
if (!str_starts_with($arg, '--')) {
continue;
}
$arg = substr($arg, 2);
if (str_contains($arg, '=')) {
[$key, $value] = explode('=', $arg, 2);
$options[$key] = $value;
continue;
}
$options[$arg] = true;
}
return $options;
}
function scripts_read_env_file(string $path): array
{
if (!is_file($path)) {
return [];
}
$values = [];
foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) {
$trimmed = trim($line);
if ($trimmed === '' || str_starts_with($trimmed, '#') || !str_contains($trimmed, '=')) {
continue;
}
[$key, $value] = explode('=', $trimmed, 2);
$values[trim($key)] = trim($value, " \t\n\r\0\x0B\"");
}
return $values;
}
function scripts_write_env_file(array $values, ?string $targetPath = null): string
{
$targetPath ??= scripts_env_path();
$templateLines = file(scripts_env_example_path(), FILE_IGNORE_NEW_LINES);
if ($templateLines === false) {
throw new RuntimeException('Die .env.example konnte nicht gelesen werden.');
}
$output = [];
foreach ($templateLines as $line) {
if (!str_contains($line, '=')) {
$output[] = $line;
continue;
}
[$key] = explode('=', $line, 2);
$trimmedKey = trim($key);
if (!array_key_exists($trimmedKey, $values)) {
$output[] = $line;
continue;
}
$output[] = $trimmedKey . '=' . scripts_format_env_value((string) $values[$trimmedKey]);
unset($values[$trimmedKey]);
}
foreach ($values as $key => $value) {
$output[] = $key . '=' . scripts_format_env_value((string) $value);
}
if (file_put_contents($targetPath, implode(PHP_EOL, $output) . PHP_EOL) === false) {
throw new RuntimeException('Die .env konnte nicht geschrieben werden: ' . $targetPath);
}
return $targetPath;
}
function scripts_format_env_value(string $value): string
{
if ($value === '') {
return '';
}
if (preg_match('/\s|#|"|=/', $value) === 1) {
return '"' . addcslashes($value, "\\\"") . '"';
}
return $value;
}
function scripts_build_migration_bundle(?string $outputPath = null): string
{
$projectRoot = scripts_project_root();
$migrationDir = $projectRoot . DIRECTORY_SEPARATOR . 'saas-app' . DIRECTORY_SEPARATOR . 'database' . DIRECTORY_SEPARATOR . 'migrations';
$outputPath ??= scripts_bundle_output_path();
if (!is_dir($migrationDir)) {
throw new RuntimeException("Migrationsverzeichnis nicht gefunden: {$migrationDir}");
}
$files = glob($migrationDir . DIRECTORY_SEPARATOR . '*.php');
sort($files, SORT_STRING);
if ($files === []) {
throw new RuntimeException('Keine Migrationsdateien gefunden.');
}
$outputDir = dirname($outputPath);
if (!is_dir($outputDir) && !mkdir($outputDir, 0777, true) && !is_dir($outputDir)) {
throw new RuntimeException("Ausgabeverzeichnis konnte nicht erstellt werden: {$outputDir}");
}
$bundle = [];
$bundle[] = '-- Generated migration bundle for Kaffeeliste SaaS';
$bundle[] = '-- Generated at ' . date('c');
$bundle[] = 'SET XACT_ABORT ON;';
$bundle[] = 'BEGIN TRANSACTION;';
foreach ($files as $file) {
$sql = require $file;
if (!is_string($sql) || trim($sql) === '') {
throw new RuntimeException("Ungueltige Migration: {$file}");
}
$bundle[] = '';
$bundle[] = '-- Migration: ' . basename($file);
$bundle[] = trim($sql);
}
$bundle[] = '';
$bundle[] = 'COMMIT TRANSACTION;';
$bundle[] = '';
if (file_put_contents($outputPath, implode(PHP_EOL, $bundle)) === false) {
throw new RuntimeException("Das SQL-Bundle konnte nicht geschrieben werden: {$outputPath}");
}
return $outputPath;
}
function scripts_run_sql_migrations(array $config): array
{
$server = $config['server'] ?? null;
$database = $config['database'] ?? null;
$port = (string) ($config['port'] ?? '1433');
$username = $config['username'] ?? null;
$password = $config['password'] ?? null;
if ($server === null || $database === null) {
throw new RuntimeException('Bitte Server und Datenbank angeben.');
}
if (!extension_loaded('pdo_sqlsrv')) {
throw new RuntimeException('Die PHP-Erweiterung pdo_sqlsrv ist nicht geladen.');
}
$migrationDir = scripts_project_root() . DIRECTORY_SEPARATOR . 'saas-app' . DIRECTORY_SEPARATOR . 'database' . DIRECTORY_SEPARATOR . 'migrations';
$files = glob($migrationDir . DIRECTORY_SEPARATOR . '*.php');
sort($files, SORT_STRING);
if ($files === []) {
throw new RuntimeException('Keine Migrationsdateien gefunden.');
}
$dsn = sprintf('sqlsrv:Server=%s,%s;Database=%s;TrustServerCertificate=1', $server, $port, $database);
$pdo = new PDO($dsn, $username, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$pdo->beginTransaction();
$executed = [];
try {
foreach ($files as $file) {
$sql = require $file;
if (!is_string($sql) || trim($sql) === '') {
throw new RuntimeException('Ungueltige Migration: ' . basename($file));
}
$pdo->exec($sql);
$executed[] = basename($file);
}
$pdo->commit();
} catch (Throwable $exception) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
throw $exception;
}
return $executed;
}
function scripts_installer_is_locked(): bool
{
return is_file(scripts_installer_lock_path());
}
function scripts_installer_lock(array $meta = []): string
{
$payload = json_encode([
'locked_at' => date('c'),
'meta' => $meta,
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
$path = scripts_installer_lock_path();
if (file_put_contents($path, $payload . PHP_EOL) === false) {
throw new RuntimeException('Die Installer-Sperrdatei konnte nicht geschrieben werden.');
}
return $path;
}
function scripts_stdout(string $message): void
{
fwrite(STDOUT, $message . PHP_EOL);
}
function scripts_stderr(string $message, int $exitCode = 1): never
{
fwrite(STDERR, $message . PHP_EOL);
exit($exitCode);
}
require_once dirname(__DIR__) . DIRECTORY_SEPARATOR . 'saas-app' . DIRECTORY_SEPARATOR . 'install-support.php';