diff --git a/README.md b/README.md index a8518ff..80d85ef 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ unter `saas-app/public/install/`. - `scripts/prepare-saas-env.php` legt aus `.env.example` eine lokale `.env` an. - `scripts/install-saas.php` fuehrt den lokalen Setup-Grundlauf aus. - `scripts/build-migration-bundle.php` baut die SQL-Migrationen zu einer Datei. -- `scripts/run-sql-migrations.php` fuehrt die SQL-Migrationen direkt per PDO SQL Server aus. +- `scripts/run-sql-migrations.php` fuehrt die SQL-Migrationen direkt per PDO fuer den konfigurierten DB-Treiber aus. ## Hinweise Zum Umbau diff --git a/docs/installationshandbuch.md b/docs/installationshandbuch.md index 833cfda..814df4f 100644 --- a/docs/installationshandbuch.md +++ b/docs/installationshandbuch.md @@ -15,7 +15,7 @@ Der Installer kann: - die `.env` speichern - das SQL-Bundle erzeugen -- Migrationen direkt per PHP ausfuehren, wenn `pdo_sqlsrv` verfuegbar ist +- Migrationen direkt per PHP ausfuehren, wenn die zum Treiber passende PDO-Erweiterung verfuegbar ist - sich nach erfolgreicher Einrichtung sperren Die CLI-Skripte unter `scripts/*.php` bleiben als Alternative fuer lokale @@ -25,7 +25,7 @@ Vorbereitung oder Server mit Shell-Zugriff erhalten. - PHP 8.2 oder neuer - Composer fuer den spaeteren Laravel-Bootstrap -- SQL Server oder eine kompatible Datenbank fuer das Zielsystem +- MySQL oder MariaDB fuer das Zielsystem - Webserver mit Document-Root auf `public/` des Zielprojekts - Cron-Zugang - optional: SMTP-Zugang fuer Mails @@ -38,7 +38,7 @@ Vorbereitung oder Server mit Shell-Zugriff erhalten. 3. Mit `php scripts/prepare-saas-env.php` eine `.env` aus `.env.example` anlegen. 4. Datenbankzugang, URL, Mail und Tenancy-Werte in `saas-app/.env` eintragen. 5. Mit `php scripts/install-saas.php` das SQL-Bundle erzeugen. -6. Das SQL-Bundle manuell in SQL Server ausfuehren oder mit `php scripts/run-sql-migrations.php --server= --database= --username= --password=` direkt einspielen. +6. Das SQL-Bundle manuell in MySQL/MariaDB importieren oder mit `php scripts/run-sql-migrations.php --connection=mysql --server= --database= --username= --password=` direkt einspielen. 7. Einen ersten Mandanten anlegen. 8. Einen ersten Benutzer und eine Mitgliedszuordnung anlegen. 9. Spaeter Composer und das eigentliche Laravel-Bootstrap nachziehen. @@ -49,12 +49,12 @@ Die aktuellen Dateien unter `saas-app/database/migrations/` sind SQL-Skizzen in PHP-Dateien, keine Laravel-Migrationsklassen. Deshalb gibt es aktuell bewusst kein `php artisan migrate`. -Stattdessen wird die SQL-Datei ueber Skripte erzeugt: +Stattdessen wird die SQL-Datei passend zum konfigurierten DB-Treiber ueber Skripte erzeugt: - Installationsvorbereitung inklusive SQL-Bundle: `php scripts/install-saas.php` -- SQL direkt gegen SQL Server ausfuehren: - `php scripts/run-sql-migrations.php --server=localhost --database=kaffeeliste_saas --username=sa --password=` +- SQL direkt gegen MySQL/MariaDB ausfuehren: + `php scripts/run-sql-migrations.php --connection=mysql --server=localhost --database=kaffeeliste_saas --username=root --password=` Die erzeugte SQL-Datei liegt anschliessend unter: @@ -76,6 +76,7 @@ Die wichtigsten Werte liegen in `saas-app/.env.example` und muessen fuer die eigene Umgebung angepasst werden: - `APP_URL` +- `DB_CONNECTION` - `DB_HOST`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD` - `TENANCY_MODE` - `TENANCY_CENTRAL_DOMAINS` @@ -88,9 +89,22 @@ eigene Umgebung angepasst werden: 1. Den Inhalt von `saas-app/` auf den Zielserver hochladen. 2. Das Document-Root auf `public/` zeigen lassen. 3. Schreibrechte fuer `saas-app/`, `.env`, `.installer.lock` und `database/migrations/generated/` sicherstellen. -4. Den Installer unter `/install/` aufrufen und die Einrichtung durchfuehren. -5. Nach erfolgreicher Einrichtung den Installer sperren. -6. Die Anwendung einmal per Browser aufrufen und die Grundseiten pruefen. +4. Sicherstellen, dass `open_basedir` nicht nur auf `public/` eingeschraenkt ist. PHP muss mindestens auf den kompletten Ordner `saas-app/` zugreifen duerfen, obwohl der Document-Root auf `public/` zeigt. +5. Den Installer unter `/install/` aufrufen und die Einrichtung durchfuehren. +6. Nach erfolgreicher Einrichtung den Installer sperren. +7. Die Anwendung einmal per Browser aufrufen und die Grundseiten pruefen. + +## MySQL Und MariaDB + +Der aktuelle Installer ist jetzt auf MySQL/MariaDB ausgelegt: + +- Standardwert `DB_CONNECTION=mysql` +- Standardport `3306` +- direkte PHP-Migration ueber `pdo_mysql` +- SQL-Bundle im MySQL/MariaDB-Dialekt + +Wenn `pdo_mysql` auf dem Hosting nicht geladen ist, kann das SQL-Bundle weiter +manuell im Datenbank-Tool des Hosters importiert werden. ## Cron- und Batch-Betrieb diff --git a/saas-app/.env.example b/saas-app/.env.example index bfd7fe0..2d8629e 100644 --- a/saas-app/.env.example +++ b/saas-app/.env.example @@ -7,9 +7,9 @@ APP_URL=http://localhost LOG_CHANNEL=stack LOG_LEVEL=debug -DB_CONNECTION=sqlsrv +DB_CONNECTION=mysql DB_HOST=127.0.0.1 -DB_PORT=1433 +DB_PORT=3306 DB_DATABASE=kaffeeliste_saas DB_USERNAME=saas_user DB_PASSWORD= diff --git a/saas-app/README.md b/saas-app/README.md index 3c7cfd7..ee2a8fe 100644 --- a/saas-app/README.md +++ b/saas-app/README.md @@ -31,7 +31,7 @@ Alternative mit PHP-CLI: 1. `php ../scripts/check-prerequisites.php` 2. `php ../scripts/install-saas.php` -3. `php ../scripts/run-sql-migrations.php --server=... --database=...` +3. `php ../scripts/run-sql-migrations.php --connection=mysql --server=... --database=...` ## Migrationen @@ -42,8 +42,8 @@ Skripte verarbeitet: - `..\scripts\build-migration-bundle.php` - `..\scripts\run-sql-migrations.php` -Wenn `pdo_sqlsrv` lokal nicht verfuegbar ist, muss das erzeugte SQL-Bundle -manuell gegen SQL Server ausgefuehrt werden. +Wenn `pdo_mysql` lokal oder auf dem Hosting nicht verfuegbar ist, muss das +erzeugte SQL-Bundle manuell gegen MySQL/MariaDB ausgefuehrt werden. ## Migration Aus Dem Legacy-System diff --git a/saas-app/database/migrations/generated/all-migrations.sql b/saas-app/database/migrations/generated/all-migrations.sql index d7e6140..a7815df 100644 --- a/saas-app/database/migrations/generated/all-migrations.sql +++ b/saas-app/database/migrations/generated/all-migrations.sql @@ -1,7 +1,8 @@ -- Generated migration bundle for Kaffeeliste SaaS --- Generated at 2026-03-21T19:24:13+00:00 -SET XACT_ABORT ON; -BEGIN TRANSACTION; +-- Generated at 2026-03-21T20:39:08+00:00 +-- Target: MySQL +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS=0; -- Migration: 2026_03_20_000001_create_tenants_table.php CREATE TABLE tenants ( @@ -12,7 +13,7 @@ CREATE TABLE tenants ( created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, UNIQUE (tenant_key) -); +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Migration: 2026_03_20_000002_create_users_table.php CREATE TABLE users ( @@ -20,11 +21,11 @@ CREATE TABLE users ( email VARCHAR(255) NOT NULL, password_hash VARCHAR(255) NULL, display_name VARCHAR(255) NOT NULL, - is_platform_admin BIT NOT NULL DEFAULT 0, + is_platform_admin TINYINT(1) NOT NULL DEFAULT 0, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, UNIQUE (email) -); +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Migration: 2026_03_20_000003_create_tenant_users_table.php CREATE TABLE tenant_users ( @@ -37,7 +38,7 @@ CREATE TABLE tenant_users ( UNIQUE (tenant_id, user_id), FOREIGN KEY (tenant_id) REFERENCES tenants(id), FOREIGN KEY (user_id) REFERENCES users(id) -); +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Migration: 2026_03_20_000004_create_roles_table.php CREATE TABLE roles ( @@ -48,7 +49,7 @@ CREATE TABLE roles ( created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, UNIQUE (role_key, scope) -); +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Migration: 2026_03_20_000005_create_tenant_user_roles_table.php CREATE TABLE tenant_user_roles ( @@ -59,7 +60,7 @@ CREATE TABLE tenant_user_roles ( UNIQUE (tenant_user_id, role_id), FOREIGN KEY (tenant_user_id) REFERENCES tenant_users(id), FOREIGN KEY (role_id) REFERENCES roles(id) -); +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Migration: 2026_03_20_000006_create_members_table.php CREATE TABLE members ( @@ -75,7 +76,7 @@ CREATE TABLE members ( UNIQUE (tenant_id, tenant_user_id), FOREIGN KEY (tenant_id) REFERENCES tenants(id), FOREIGN KEY (tenant_user_id) REFERENCES tenant_users(id) -); +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Migration: 2026_03_20_000006_create_password_reset_tokens_table.php CREATE TABLE password_reset_tokens ( @@ -83,7 +84,7 @@ CREATE TABLE password_reset_tokens ( token VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL, PRIMARY KEY (email) -); +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Migration: 2026_03_20_000007_create_coffee_entries_table.php CREATE TABLE coffee_entries ( @@ -99,7 +100,7 @@ CREATE TABLE coffee_entries ( updated_at DATETIME NOT NULL, FOREIGN KEY (tenant_id) REFERENCES tenants(id), FOREIGN KEY (member_id) REFERENCES members(id) -); +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Migration: 2026_03_20_000007_create_tenant_identity_providers_table.php CREATE TABLE tenant_identity_providers ( @@ -116,12 +117,12 @@ CREATE TABLE tenant_identity_providers ( userinfo_url VARCHAR(1024) NULL, redirect_uri VARCHAR(1024) NOT NULL, scopes TEXT NULL, - is_enabled BIT NOT NULL DEFAULT 1, + is_enabled TINYINT(1) NOT NULL DEFAULT 1, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, UNIQUE (tenant_id, provider_key), FOREIGN KEY (tenant_id) REFERENCES tenants(id) -); +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Migration: 2026_03_20_000008_create_payment_entries_table.php CREATE TABLE payment_entries ( @@ -135,7 +136,7 @@ CREATE TABLE payment_entries ( updated_at DATETIME NOT NULL, FOREIGN KEY (tenant_id) REFERENCES tenants(id), FOREIGN KEY (member_id) REFERENCES members(id) -); +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Migration: 2026_03_20_000008_create_user_identities_table.php CREATE TABLE user_identities ( @@ -150,7 +151,7 @@ CREATE TABLE user_identities ( UNIQUE (provider_key, external_subject), FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (tenant_id) REFERENCES tenants(id) -); +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Migration: 2026_03_20_000009_create_ledger_entries_table.php CREATE TABLE ledger_entries ( @@ -166,7 +167,7 @@ CREATE TABLE ledger_entries ( updated_at DATETIME NOT NULL, FOREIGN KEY (tenant_id) REFERENCES tenants(id), FOREIGN KEY (member_id) REFERENCES members(id) -); +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Migration: 2026_03_20_000010_create_announcements_table.php CREATE TABLE announcements ( @@ -175,11 +176,11 @@ CREATE TABLE announcements ( title VARCHAR(255) NOT NULL, message TEXT NOT NULL, visible_until DATETIME NULL, - is_active BIT NOT NULL DEFAULT 1, + is_active TINYINT(1) NOT NULL DEFAULT 1, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, FOREIGN KEY (tenant_id) REFERENCES tenants(id) -); +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Migration: 2026_03_20_000011_create_faq_items_table.php CREATE TABLE faq_items ( @@ -188,11 +189,11 @@ CREATE TABLE faq_items ( question VARCHAR(255) NOT NULL, answer TEXT NOT NULL, sort_order INT NOT NULL DEFAULT 0, - is_active BIT NOT NULL DEFAULT 1, + is_active TINYINT(1) NOT NULL DEFAULT 1, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, FOREIGN KEY (tenant_id) REFERENCES tenants(id) -); +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Migration: 2026_03_20_000012_create_surveys_table.php CREATE TABLE surveys ( @@ -205,7 +206,7 @@ CREATE TABLE surveys ( created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, FOREIGN KEY (tenant_id) REFERENCES tenants(id) -); +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Migration: 2026_03_20_000013_create_survey_questions_table.php CREATE TABLE survey_questions ( @@ -213,12 +214,12 @@ CREATE TABLE survey_questions ( survey_id CHAR(36) NOT NULL, question TEXT NOT NULL, question_type VARCHAR(50) NOT NULL, - is_required BIT NOT NULL DEFAULT 0, + is_required TINYINT(1) NOT NULL DEFAULT 0, sort_order INT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, FOREIGN KEY (survey_id) REFERENCES surveys(id) -); +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Migration: 2026_03_20_000014_create_survey_answers_table.php CREATE TABLE survey_answers ( @@ -232,7 +233,7 @@ CREATE TABLE survey_answers ( FOREIGN KEY (survey_id) REFERENCES surveys(id), FOREIGN KEY (question_id) REFERENCES survey_questions(id), FOREIGN KEY (tenant_user_id) REFERENCES tenant_users(id) -); +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Migration: 2026_03_20_000015_create_import_jobs_table.php CREATE TABLE import_jobs ( @@ -246,7 +247,7 @@ CREATE TABLE import_jobs ( created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, FOREIGN KEY (tenant_id) REFERENCES tenants(id) -); +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Migration: 2026_03_20_000016_create_export_jobs_table.php CREATE TABLE export_jobs ( @@ -258,7 +259,7 @@ CREATE TABLE export_jobs ( created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, FOREIGN KEY (tenant_id) REFERENCES tenants(id) -); +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Migration: 2026_03_20_000017_create_notification_logs_table.php CREATE TABLE notification_logs ( @@ -272,6 +273,6 @@ CREATE TABLE notification_logs ( created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, FOREIGN KEY (tenant_id) REFERENCES tenants(id) -); +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -COMMIT TRANSACTION; +SET FOREIGN_KEY_CHECKS=1; diff --git a/saas-app/public/index.php b/saas-app/public/index.php index 7565637..67dbb23 100644 --- a/saas-app/public/index.php +++ b/saas-app/public/index.php @@ -50,8 +50,8 @@ $migrationSteps = [ 'Die Dateien unter `saas-app/database/migrations/*.php` sind keine Laravel-Migrationsklassen, sondern SQL-Skizzen.', 'Das Bundle wird ueber `php scripts/build-migration-bundle.php` erzeugt.', 'Empfohlener Weg: `php scripts/install-saas.php`.', - 'Direkte SQL-Server-Ausfuehrung: `php scripts/run-sql-migrations.php --server= --database= --username= --password=`.', - 'Ohne `pdo_sqlsrv` die erzeugte SQL-Datei manuell in SQL Server Management Studio oder einem kompatiblen Tool ausfuehren.', + 'Direkte MySQL/MariaDB-Ausfuehrung: `php scripts/run-sql-migrations.php --connection=mysql --server= --database= --username= --password=`.', + 'Ohne `pdo_mysql` die erzeugte SQL-Datei manuell im Datenbank-Tool des Hosters importieren.', ]; function renderList(array $items): void @@ -380,7 +380,7 @@ function renderList(array $items): void
  • Composer ist fuer den finalen Bootstrap weiterhin erforderlich.
  • Die Migrationen sind aktuell SQL-basiert, nicht artisan-basiert.
  • -
  • Fuer automatische Ausfuehrung wird die PHP-Erweiterung pdo_sqlsrv benoetigt.
  • +
  • Fuer automatische Ausfuehrung auf MySQL/MariaDB wird die PHP-Erweiterung pdo_mysql benoetigt.
  • Der bevorzugte Hosting-Pfad ist jetzt /install/ als gefuehrter Einmal-Installer.
diff --git a/saas-app/public/install-support.php b/saas-app/public/install-support.php index ae137f9..7d76afa 100644 --- a/saas-app/public/install-support.php +++ b/saas-app/public/install-support.php @@ -36,6 +36,56 @@ function scripts_installer_lock_path(): string return scripts_saas_app_path() . DIRECTORY_SEPARATOR . '.installer.lock'; } +function scripts_supported_connections(): array +{ + return [ + 'mysql' => 'MySQL', + 'mariadb' => 'MariaDB', + 'sqlsrv' => 'SQL Server', + ]; +} + +function scripts_normalize_db_connection(?string $connection): string +{ + $normalized = strtolower(trim((string) $connection)); + + if ($normalized === 'maria' || $normalized === 'maria-db') { + $normalized = 'mariadb'; + } + + if ($normalized === 'sqlserver' || $normalized === 'mssql') { + $normalized = 'sqlsrv'; + } + + return array_key_exists($normalized, scripts_supported_connections()) ? $normalized : 'mysql'; +} + +function scripts_connection_label(string $connection): string +{ + $connection = scripts_normalize_db_connection($connection); + $supported = scripts_supported_connections(); + + return $supported[$connection]; +} + +function scripts_default_db_port(string $connection): string +{ + $connection = scripts_normalize_db_connection($connection); + + if ($connection === 'sqlsrv') { + return '1433'; + } + + return '3306'; +} + +function scripts_required_pdo_extension(string $connection): string +{ + $connection = scripts_normalize_db_connection($connection); + + return $connection === 'sqlsrv' ? 'pdo_sqlsrv' : 'pdo_mysql'; +} + function scripts_parent_directory(string $path): string { return dirname($path); @@ -215,8 +265,85 @@ function scripts_format_env_value(string $value): string return $value; } -function scripts_build_migration_bundle(?string $outputPath = null): string +function scripts_transform_migration_sql(string $sql, string $connection): string { + $connection = scripts_normalize_db_connection($connection); + $sql = trim(str_replace(["\r\n", "\r"], "\n", $sql)); + + if ($connection === 'sqlsrv') { + return $sql; + } + + $sql = preg_replace('/\bBIT\s+NOT NULL\s+DEFAULT\s+([01])\b/i', 'TINYINT(1) NOT NULL DEFAULT $1', $sql) ?? $sql; + $sql = preg_replace('/\bBIT\b/i', 'TINYINT(1)', $sql) ?? $sql; + + if (preg_match('/^CREATE TABLE\s+/i', $sql) === 1) { + $sql = preg_replace( + '/\)\s*;\s*$/', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;', + $sql + ) ?? $sql; + } + + return $sql; +} + +function scripts_load_migration_sql(string $file, string $connection): string +{ + $sql = require $file; + + if (!is_string($sql) || trim($sql) === '') { + throw new RuntimeException('Ungueltige Migration: ' . $file); + } + + return scripts_transform_migration_sql($sql, $connection); +} + +function scripts_bundle_prologue(string $connection): array +{ + $connection = scripts_normalize_db_connection($connection); + + if ($connection === 'sqlsrv') { + return [ + '-- Generated migration bundle for Kaffeeliste SaaS', + '-- Generated at ' . date('c'), + '-- Target: ' . scripts_connection_label($connection), + 'SET XACT_ABORT ON;', + 'BEGIN TRANSACTION;', + ]; + } + + return [ + '-- Generated migration bundle for Kaffeeliste SaaS', + '-- Generated at ' . date('c'), + '-- Target: ' . scripts_connection_label($connection), + 'SET NAMES utf8mb4;', + 'SET FOREIGN_KEY_CHECKS=0;', + ]; +} + +function scripts_bundle_epilogue(string $connection): array +{ + $connection = scripts_normalize_db_connection($connection); + + if ($connection === 'sqlsrv') { + return [ + '', + 'COMMIT TRANSACTION;', + '', + ]; + } + + return [ + '', + 'SET FOREIGN_KEY_CHECKS=1;', + '', + ]; +} + +function scripts_build_migration_bundle(?string $outputPath = null, string $connection = 'mysql'): string +{ + $connection = scripts_normalize_db_connection($connection); $migrationDir = scripts_saas_app_path() . DIRECTORY_SEPARATOR . 'database' . DIRECTORY_SEPARATOR . 'migrations'; @@ -241,27 +368,19 @@ function scripts_build_migration_bundle(?string $outputPath = null): string 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;'; + $bundle = scripts_bundle_prologue($connection); foreach ($files as $file) { - $sql = require $file; - - if (!is_string($sql) || trim($sql) === '') { - throw new RuntimeException('Ungueltige Migration: ' . $file); - } + $sql = scripts_load_migration_sql($file, $connection); $bundle[] = ''; $bundle[] = '-- Migration: ' . basename($file); - $bundle[] = trim($sql); + $bundle[] = $sql; } - $bundle[] = ''; - $bundle[] = 'COMMIT TRANSACTION;'; - $bundle[] = ''; + foreach (scripts_bundle_epilogue($connection) as $line) { + $bundle[] = $line; + } if (file_put_contents($outputPath, implode(PHP_EOL, $bundle)) === false) { throw new RuntimeException('Das SQL-Bundle konnte nicht geschrieben werden: ' . $outputPath); @@ -272,9 +391,10 @@ function scripts_build_migration_bundle(?string $outputPath = null): string function scripts_run_sql_migrations(array $config): array { + $connection = scripts_normalize_db_connection(isset($config['connection']) ? (string) $config['connection'] : 'mysql'); $server = isset($config['server']) ? $config['server'] : null; $database = isset($config['database']) ? $config['database'] : null; - $port = isset($config['port']) ? (string) $config['port'] : '1433'; + $port = isset($config['port']) ? (string) $config['port'] : scripts_default_db_port($connection); $username = isset($config['username']) ? $config['username'] : null; $password = isset($config['password']) ? $config['password'] : null; @@ -282,8 +402,9 @@ function scripts_run_sql_migrations(array $config): array throw new RuntimeException('Bitte Server und Datenbank angeben.'); } - if (!extension_loaded('pdo_sqlsrv')) { - throw new RuntimeException('Die PHP-Erweiterung pdo_sqlsrv ist nicht geladen.'); + $requiredExtension = scripts_required_pdo_extension($connection); + if (!extension_loaded($requiredExtension)) { + throw new RuntimeException('Die PHP-Erweiterung ' . $requiredExtension . ' ist nicht geladen.'); } $migrationDir = scripts_saas_app_path() @@ -297,33 +418,53 @@ function scripts_run_sql_migrations(array $config): array 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(); + if ($connection === 'sqlsrv') { + $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, + ]); + } else { + $dsn = sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $server, $port, $database); + $pdo = new PDO($dsn, $username, $password, [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ]); + } $executed = []; try { - foreach ($files as $file) { - $sql = require $file; + if ($connection === 'sqlsrv') { + $pdo->beginTransaction(); + } else { + $pdo->exec('SET NAMES utf8mb4'); + $pdo->exec('SET FOREIGN_KEY_CHECKS=0'); + } - if (!is_string($sql) || trim($sql) === '') { - throw new RuntimeException('Ungueltige Migration: ' . basename($file)); - } + foreach ($files as $file) { + $sql = scripts_load_migration_sql($file, $connection); $pdo->exec($sql); $executed[] = basename($file); } - $pdo->commit(); + if ($connection === 'sqlsrv') { + $pdo->commit(); + } else { + $pdo->exec('SET FOREIGN_KEY_CHECKS=1'); + } } catch (Throwable $exception) { if ($pdo->inTransaction()) { $pdo->rollBack(); } + if ($connection !== 'sqlsrv') { + try { + $pdo->exec('SET FOREIGN_KEY_CHECKS=1'); + } catch (Throwable $ignored) { + } + } + throw $exception; } diff --git a/saas-app/public/install.php b/saas-app/public/install.php index 2f3dcea..fd36534 100644 --- a/saas-app/public/install.php +++ b/saas-app/public/install.php @@ -33,10 +33,12 @@ if (!isset($_SESSION['installer_csrf'])) { $defaults = scripts_read_env_file(scripts_env_example_path()); $currentEnv = scripts_read_env_file(scripts_env_path()); +$defaultConnection = scripts_normalize_db_connection($currentEnv['DB_CONNECTION'] ?? ($defaults['DB_CONNECTION'] ?? 'mysql')); $values = [ 'APP_URL' => $currentEnv['APP_URL'] ?? ($defaults['APP_URL'] ?? ''), + 'DB_CONNECTION' => $defaultConnection, 'DB_HOST' => $currentEnv['DB_HOST'] ?? ($defaults['DB_HOST'] ?? '127.0.0.1'), - 'DB_PORT' => $currentEnv['DB_PORT'] ?? ($defaults['DB_PORT'] ?? '1433'), + 'DB_PORT' => $currentEnv['DB_PORT'] ?? ($defaults['DB_PORT'] ?? scripts_default_db_port($defaultConnection)), 'DB_DATABASE' => $currentEnv['DB_DATABASE'] ?? ($defaults['DB_DATABASE'] ?? ''), 'DB_USERNAME' => $currentEnv['DB_USERNAME'] ?? ($defaults['DB_USERNAME'] ?? ''), 'DB_PASSWORD' => '', @@ -64,6 +66,15 @@ $openBaseDir = (string) ini_get('open_basedir'); $envPathState = scripts_path_writable_state(scripts_env_path()); $lockPathState = scripts_path_writable_state(scripts_installer_lock_path()); $bundlePathState = scripts_path_writable_state(scripts_bundle_output_path()); +$activeConnection = scripts_normalize_db_connection($values['DB_CONNECTION']); +$activeConnectionLabel = scripts_connection_label($activeConnection); +$requiredExtension = scripts_required_pdo_extension($activeConnection); +$requiredExtensionLoaded = extension_loaded($requiredExtension); + +if (!$saasAppVisible) { + $errors[] = 'PHP darf den SaaS-App-Ordner aktuell nicht lesen. Die `open_basedir`-Konfiguration erlaubt nur `public/`.'; + $errors[] = 'Stelle den Document-Root weiter auf `saas-app/public`, erweitere aber `open_basedir` mindestens auf `saas-app/`.'; +} if ($requestMethod === 'POST' && !$locked) { $csrf = (string) ($_POST['csrf'] ?? ''); @@ -77,6 +88,17 @@ if ($requestMethod === 'POST' && !$locked) { } } + $values['DB_CONNECTION'] = scripts_normalize_db_connection($values['DB_CONNECTION']); + + if ($values['DB_PORT'] === '') { + $values['DB_PORT'] = scripts_default_db_port($values['DB_CONNECTION']); + } + + $activeConnection = scripts_normalize_db_connection($values['DB_CONNECTION']); + $activeConnectionLabel = scripts_connection_label($activeConnection); + $requiredExtension = scripts_required_pdo_extension($activeConnection); + $requiredExtensionLoaded = extension_loaded($requiredExtension); + $values['OIDC_ENABLED'] = isset($_POST['OIDC_ENABLED']) ? 'true' : 'false'; if ($values['DB_PASSWORD'] === '' && isset($currentEnv['DB_PASSWORD'])) { @@ -98,18 +120,19 @@ if ($requestMethod === 'POST' && !$locked) { scripts_write_env_file($values); $messages[] = '.env wurde geschrieben.'; - $bundlePath = scripts_build_migration_bundle(); - $messages[] = 'SQL-Bundle wurde erzeugt.'; + $bundlePath = scripts_build_migration_bundle(null, $values['DB_CONNECTION']); + $messages[] = 'SQL-Bundle fuer ' . scripts_connection_label($values['DB_CONNECTION']) . ' wurde erzeugt.'; if (isset($_POST['run_migrations'])) { $executedMigrations = scripts_run_sql_migrations([ + 'connection' => $values['DB_CONNECTION'], 'server' => $values['DB_HOST'], 'database' => $values['DB_DATABASE'], 'port' => $values['DB_PORT'], 'username' => $values['DB_USERNAME'], 'password' => $values['DB_PASSWORD'], ]); - $messages[] = 'Migrationen wurden direkt ueber PHP ausgefuehrt.'; + $messages[] = 'Migrationen wurden direkt ueber PHP fuer ' . $activeConnectionLabel . ' ausgefuehrt.'; } if (isset($_POST['lock_installer'])) { @@ -123,8 +146,8 @@ if ($requestMethod === 'POST' && !$locked) { } catch (Throwable $exception) { $message = $exception->getMessage(); - if (scripts_string_contains($message, 'pdo_sqlsrv')) { - $errors[] = 'Migrationen konnten nicht direkt ueber PHP ausgefuehrt werden. Bitte `pdo_sqlsrv` pruefen oder das SQL-Bundle manuell importieren.'; + if (scripts_string_contains($message, 'pdo_')) { + $errors[] = 'Migrationen konnten nicht direkt ueber PHP ausgefuehrt werden. Bitte `' . $requiredExtension . '` pruefen oder das SQL-Bundle manuell importieren.'; } elseif (scripts_string_contains($message, '.env')) { $errors[] = 'Die Konfiguration konnte nicht gespeichert werden. Zielpfad: ' . scripts_env_path(); $errors[] = 'Status fuer .env: ' . scripts_path_state_label($envPathState) . '. Elternordner: ' . $envPathState['parent']; @@ -352,6 +375,14 @@ function h(string $value): string +
+ + +
@@ -426,8 +457,8 @@ function h(string $value): string