Refactor installation scripts and update documentation

- Replaced PowerShell scripts with PHP scripts for checking prerequisites and preparing the environment.
- Added new PHP scripts: `check-prerequisites.php`, `prepare-saas-env.php`, `install-saas.php`, `build-migration-bundle.php`, and `run-sql-migrations.php`.
- Updated README and installation documentation to reflect the new PHP scripts and installation steps.
- Created a generated SQL migration bundle file structure and added SQL migration scripts.
- Enhanced the public index page with navigation and installation steps.
- Removed obsolete PowerShell scripts.
This commit is contained in:
2026-03-21 19:49:52 +01:00
parent f298802c38
commit 70e6d59c63
13 changed files with 840 additions and 129 deletions
+55
View File
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
$projectRoot = dirname(__DIR__);
$migrationDir = $projectRoot . DIRECTORY_SEPARATOR . 'saas-app' . DIRECTORY_SEPARATOR . 'database' . DIRECTORY_SEPARATOR . 'migrations';
$outputPath = $argv[1] ?? (
$projectRoot . DIRECTORY_SEPARATOR . 'saas-app' . DIRECTORY_SEPARATOR . 'database' . DIRECTORY_SEPARATOR . 'migrations' . DIRECTORY_SEPARATOR . 'generated' . DIRECTORY_SEPARATOR . 'all-migrations.sql'
);
if (!is_dir($migrationDir)) {
fwrite(STDERR, "Migrationsverzeichnis nicht gefunden: {$migrationDir}" . PHP_EOL);
exit(1);
}
$files = glob($migrationDir . DIRECTORY_SEPARATOR . '*.php');
sort($files, SORT_STRING);
if ($files === []) {
fwrite(STDERR, "Keine Migrationsdateien gefunden." . PHP_EOL);
exit(1);
}
$outputDir = dirname($outputPath);
if (!is_dir($outputDir) && !mkdir($outputDir, 0777, true) && !is_dir($outputDir)) {
fwrite(STDERR, "Ausgabeverzeichnis konnte nicht erstellt werden: {$outputDir}" . PHP_EOL);
exit(1);
}
$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) === '') {
fwrite(STDERR, "Ungueltige Migration: {$file}" . PHP_EOL);
exit(1);
}
$bundle[] = '';
$bundle[] = '-- Migration: ' . basename($file);
$bundle[] = trim($sql);
}
$bundle[] = '';
$bundle[] = 'COMMIT TRANSACTION;';
$bundle[] = '';
file_put_contents($outputPath, implode(PHP_EOL, $bundle));
fwrite(STDOUT, $outputPath . PHP_EOL);
+34
View File
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/support.php';
$phpVersion = PHP_VERSION;
$composerExists = scripts_check_command('composer');
$gitExists = scripts_check_command('git');
scripts_stdout('Pruefe lokale Voraussetzungen fuer Kaffeeliste SaaS...');
scripts_stdout('Projektwurzel: ' . scripts_project_root());
scripts_stdout('');
$checks = [
['PHP', true, $phpVersion],
['Composer', $composerExists, $composerExists ? 'verfuegbar' : 'nicht gefunden'],
['Git', $gitExists, $gitExists ? 'verfuegbar' : 'nicht gefunden'],
['SaaS-App', is_dir(scripts_saas_app_path()), scripts_saas_app_path()],
['.env.example', is_file(scripts_env_example_path()), scripts_env_example_path()],
['.env', is_file(scripts_env_path()), is_file(scripts_env_path()) ? 'vorhanden' : 'noch nicht angelegt'],
];
foreach ($checks as [$label, $passed, $detail]) {
$prefix = $passed ? '[OK]' : '[FEHLT]';
scripts_stdout(sprintf('%s %s - %s', $prefix, $label, $detail));
}
scripts_stdout('');
scripts_stdout('Naechste Schritte:');
scripts_stdout('1. Falls Composer fehlt, zuerst Composer installieren.');
scripts_stdout('2. Mit `php scripts/prepare-saas-env.php` eine lokale .env anlegen.');
scripts_stdout('3. Mit `php scripts/install-saas.php` das SQL-Bundle erzeugen.');
scripts_stdout('4. Optional `php scripts/run-sql-migrations.php --server=... --database=...` fuer die direkte SQL-Ausfuehrung verwenden.');
-48
View File
@@ -1,48 +0,0 @@
param(
[string]$ProjectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
)
$ErrorActionPreference = 'Stop'
$saasAppPath = Join-Path $ProjectRoot 'saas-app'
$envExamplePath = Join-Path $saasAppPath '.env.example'
function Test-Command {
param([string]$Name)
return $null -ne (Get-Command $Name -ErrorAction SilentlyContinue)
}
function Write-Check {
param(
[string]$Label,
[bool]$Passed,
[string]$Detail
)
$status = if ($Passed) { '[OK]' } else { '[FEHLT]' }
Write-Host "$status $Label - $Detail"
}
Write-Host 'Pruefe lokale Voraussetzungen fuer Kaffeeliste SaaS...'
Write-Host "Projektwurzel: $ProjectRoot"
Write-Host ''
$phpExists = Test-Command 'php'
$composerExists = Test-Command 'composer'
$gitExists = Test-Command 'git'
Write-Check -Label 'PHP' -Passed $phpExists -Detail ($(if ($phpExists) { (php -r "echo PHP_VERSION;") } else { 'nicht gefunden' }))
Write-Check -Label 'Composer' -Passed $composerExists -Detail ($(if ($composerExists) { (composer --version | Select-Object -First 1) } else { 'nicht gefunden' }))
Write-Check -Label 'Git' -Passed $gitExists -Detail ($(if ($gitExists) { 'verfuegbar' } else { 'nicht gefunden' }))
Write-Check -Label 'SaaS-App' -Passed (Test-Path $saasAppPath) -Detail $saasAppPath
Write-Check -Label '.env.example' -Passed (Test-Path $envExamplePath) -Detail $envExamplePath
$envPath = Join-Path $saasAppPath '.env'
Write-Check -Label '.env' -Passed (Test-Path $envPath) -Detail ($(if (Test-Path $envPath) { 'vorhanden' } else { 'noch nicht angelegt' }))
Write-Host ''
Write-Host 'Naechste Schritte:'
Write-Host '1. Falls Composer fehlt, zuerst Composer installieren.'
Write-Host '2. Mit scripts/prepare-saas-env.ps1 eine lokale .env anlegen.'
Write-Host '3. Danach saas-app konfigurieren und die Installationsanleitung in docs/installationshandbuch.md befolgen.'
+42
View File
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/support.php';
$options = scripts_parse_options($argv);
$forceEnv = isset($options['force-env']);
$prepareEnv = isset($options['prepare-env']) || !is_file(scripts_env_path());
scripts_stdout('Starte SaaS-Installationsvorbereitung...');
scripts_stdout('');
require __DIR__ . '/check-prerequisites.php';
if ($prepareEnv) {
$command = 'php ' . escapeshellarg(__DIR__ . '/prepare-saas-env.php');
if ($forceEnv) {
$command .= ' --force';
}
passthru($command, $exitCode);
if ($exitCode !== 0) {
exit($exitCode);
}
}
$bundlePath = scripts_project_root() . DIRECTORY_SEPARATOR . 'saas-app' . DIRECTORY_SEPARATOR . 'database' . DIRECTORY_SEPARATOR . 'migrations' . DIRECTORY_SEPARATOR . 'generated' . DIRECTORY_SEPARATOR . 'all-migrations.sql';
$command = 'php ' . escapeshellarg(__DIR__ . '/build-migration-bundle.php') . ' ' . escapeshellarg($bundlePath);
passthru($command, $exitCode);
if ($exitCode !== 0) {
exit($exitCode);
}
scripts_stdout('');
scripts_stdout('Naechste Schritte:');
scripts_stdout('1. saas-app/.env mit echten DB-, Mail- und Tenancy-Werten fuellen.');
scripts_stdout('2. Das SQL-Bundle manuell importieren oder `php scripts/run-sql-migrations.php --server=... --database=...` verwenden.');
scripts_stdout('3. Danach ersten Tenant, ersten Benutzer und erste Member-Zuordnung anlegen.');
+26
View File
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/support.php';
$options = scripts_parse_options($argv);
$force = isset($options['force']);
$sourcePath = scripts_env_example_path();
$targetPath = scripts_env_path();
if (!is_file($sourcePath)) {
scripts_stderr('Quelle nicht gefunden: ' . $sourcePath);
}
if (is_file($targetPath) && !$force) {
scripts_stderr('.env existiert bereits. Nutze `php scripts/prepare-saas-env.php --force` fuer ein Ueberschreiben.');
}
if (!copy($sourcePath, $targetPath)) {
scripts_stderr('Die .env konnte nicht angelegt werden.');
}
scripts_stdout('Lokale .env wurde angelegt: ' . $targetPath);
scripts_stdout('Passe jetzt DB-, Mail-, Tenancy- und OIDC-Werte an.');
-23
View File
@@ -1,23 +0,0 @@
param(
[string]$ProjectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path,
[switch]$Force
)
$ErrorActionPreference = 'Stop'
$saasAppPath = Join-Path $ProjectRoot 'saas-app'
$sourcePath = Join-Path $saasAppPath '.env.example'
$targetPath = Join-Path $saasAppPath '.env'
if (-not (Test-Path $sourcePath)) {
throw "Quelle nicht gefunden: $sourcePath"
}
if ((Test-Path $targetPath) -and -not $Force) {
throw ".env existiert bereits. Nutze -Force, wenn sie neu erzeugt werden soll."
}
Copy-Item -Path $sourcePath -Destination $targetPath -Force
Write-Host "Lokale .env wurde angelegt: $targetPath"
Write-Host 'Passe jetzt DB-, Mail-, Tenancy- und OIDC-Werte an.'
+60
View File
@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/support.php';
$options = scripts_parse_options($argv);
$env = scripts_read_env_file(scripts_env_path());
$server = $options['server'] ?? $env['DB_HOST'] ?? null;
$database = $options['database'] ?? $env['DB_DATABASE'] ?? null;
$port = $options['port'] ?? $env['DB_PORT'] ?? '1433';
$username = $options['username'] ?? $env['DB_USERNAME'] ?? null;
$password = $options['password'] ?? $env['DB_PASSWORD'] ?? null;
if ($server === null || $database === null) {
scripts_stderr('Bitte --server und --database angeben oder eine passende saas-app/.env pflegen.');
}
if (!extension_loaded('pdo_sqlsrv')) {
scripts_stderr('Die PHP-Erweiterung pdo_sqlsrv ist nicht geladen. Fuehre das Bundle manuell aus oder aktiviere den Treiber.');
}
$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 === []) {
scripts_stderr('Keine Migrationsdateien gefunden.');
}
$dsn = sprintf('sqlsrv:Server=%s,%s;Database=%s;TrustServerCertificate=1', $server, $port, $database);
try {
$pdo = new PDO($dsn, $username, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$pdo->beginTransaction();
foreach ($files as $file) {
$sql = require $file;
if (!is_string($sql) || trim($sql) === '') {
throw new RuntimeException('Ungueltige Migration: ' . basename($file));
}
scripts_stdout('Fuehre aus: ' . basename($file));
$pdo->exec($sql);
}
$pdo->commit();
scripts_stdout('Migrationen wurden erfolgreich ausgefuehrt.');
} catch (Throwable $exception) {
if (isset($pdo) && $pdo instanceof PDO && $pdo->inTransaction()) {
$pdo->rollBack();
}
scripts_stderr('Migration fehlgeschlagen: ' . $exception->getMessage());
}
+90
View File
@@ -0,0 +1,90 @@
<?php
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_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_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);
}