feat: Add install-support.php and improve installer error handling
This commit is contained in:
+1
-281
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user