Produktiv- und Testtabellen per Prefix trennen
This commit is contained in:
+3
-1
@@ -3,6 +3,7 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/bootstrap.php';
|
||||
require_once __DIR__ . '/prefixed-pdo.php';
|
||||
|
||||
function app_db_pdo(): PDO
|
||||
{
|
||||
@@ -23,11 +24,12 @@ function app_db_pdo(): PDO
|
||||
}
|
||||
|
||||
$dsn = sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $host, $port, $database);
|
||||
$pdo = new PDO($dsn, $user, $password ?? '', [
|
||||
$pdo = new AppPrefixedPDO($dsn, $user, $password ?? '', [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => true,
|
||||
]);
|
||||
$pdo->setTablePrefix(app_db_table_prefix());
|
||||
|
||||
// DB-Session auf denselben Offset wie die (in bootstrap.php gepinnte)
|
||||
// PHP-Zeitzone setzen, damit MySQL NOW() und PHP-Zeitstempel
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/bootstrap.php';
|
||||
|
||||
/**
|
||||
* Vollständiger Katalog aller Tabellen, die zur Kaffeeliste gehören. Die
|
||||
* zentrale SQL-Umschreibung beschränkt sich bewusst auf diese feste Liste;
|
||||
* ein vom Request gelieferter Bezeichner kann dadurch nie in SQL gelangen.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
function app_database_table_names(): array
|
||||
{
|
||||
return [
|
||||
'schema_migrations',
|
||||
'CoffeeSurveyVotedEmails',
|
||||
'CoffeeSurveyResponses',
|
||||
'dev_baseline_metadata',
|
||||
'kl_Kaffeeverbrauch',
|
||||
'kl_Einzahlungen',
|
||||
'kl_Mitarbeiter',
|
||||
'kl_hinweise',
|
||||
'kl_config',
|
||||
'payment_import_batches',
|
||||
'payment_import_rows',
|
||||
'stripe_webhook_events',
|
||||
'rate_limit_attempts',
|
||||
'tenant_memberships',
|
||||
'legal_acceptances',
|
||||
'outbound_emails',
|
||||
'user_auth_tokens',
|
||||
'tenant_settings',
|
||||
'platform_admins',
|
||||
'tenant_features',
|
||||
'paypal_payments',
|
||||
'ledger_entries',
|
||||
'legal_requests',
|
||||
'tenant_domains',
|
||||
'tenant_billing',
|
||||
'faq_entries',
|
||||
'participants',
|
||||
'audit_log',
|
||||
'notices',
|
||||
'tenants',
|
||||
'users',
|
||||
];
|
||||
}
|
||||
|
||||
function app_validate_db_table_prefix(string $prefix): string
|
||||
{
|
||||
$prefix = trim($prefix);
|
||||
if ($prefix === '') {
|
||||
return '';
|
||||
}
|
||||
if (strlen($prefix) > 16 || preg_match('/^[a-z][a-z0-9_]*_$/', $prefix) !== 1) {
|
||||
throw new RuntimeException(
|
||||
'DB_TABLE_PREFIX muss leer sein oder aus höchstens 16 Kleinbuchstaben, Ziffern und Unterstrichen bestehen und mit Unterstrich enden.'
|
||||
);
|
||||
}
|
||||
|
||||
return $prefix;
|
||||
}
|
||||
|
||||
function app_db_table_prefix(): string
|
||||
{
|
||||
$prefix = app_validate_db_table_prefix((string)app_env('DB_TABLE_PREFIX', ''));
|
||||
if ($prefix === '' && !app_is_dev()) {
|
||||
throw new RuntimeException('DB_TABLE_PREFIX darf außerhalb der Entwicklungsumgebung nicht leer sein.');
|
||||
}
|
||||
|
||||
return $prefix;
|
||||
}
|
||||
|
||||
function app_db_table_name(string $baseName, ?string $prefix = null): string
|
||||
{
|
||||
if (!in_array($baseName, app_database_table_names(), true)) {
|
||||
throw new InvalidArgumentException('Unbekannte Anwendungstabelle: ' . $baseName);
|
||||
}
|
||||
|
||||
return app_validate_db_table_prefix($prefix ?? app_db_table_prefix()) . $baseName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schreibt ausschließlich bekannte Tabellen- und FK-Namen um. Das gilt auch
|
||||
* innerhalb der dynamischen DDL-Strings in den Migrationen 0003/0008 und für
|
||||
* deren INFORMATION_SCHEMA-Vergleiche.
|
||||
*/
|
||||
function app_prefix_database_sql(string $sql, ?string $prefix = null): string
|
||||
{
|
||||
$prefix = app_validate_db_table_prefix($prefix ?? app_db_table_prefix());
|
||||
if ($prefix === '') {
|
||||
return $sql;
|
||||
}
|
||||
|
||||
static $tablePattern = null;
|
||||
if ($tablePattern === null) {
|
||||
$tables = app_database_table_names();
|
||||
usort($tables, static fn(string $a, string $b): int => strlen($b) <=> strlen($a));
|
||||
$tablePattern = '/(?<![A-Za-z0-9_])('
|
||||
. implode('|', array_map(static fn(string $table): string => preg_quote($table, '/'), $tables))
|
||||
. ')(?![A-Za-z0-9_])/i';
|
||||
}
|
||||
$sql = preg_replace_callback(
|
||||
$tablePattern,
|
||||
static fn(array $match): string => $prefix . $match[0],
|
||||
$sql
|
||||
) ?? $sql;
|
||||
|
||||
// MySQL verlangt innerhalb einer Datenbank eindeutige Namen für
|
||||
// Fremdschlüssel-Constraints. Die Migrationen verwenden durchgängig
|
||||
// sprechende fk_*-Namen; sie brauchen daher ebenfalls den Umgebungs-Prefix.
|
||||
$sql = preg_replace_callback(
|
||||
'/(?<![A-Za-z0-9_])(fk_[A-Za-z0-9_]+)(?![A-Za-z0-9_])/i',
|
||||
static fn(array $match): string => $prefix . $match[1],
|
||||
$sql
|
||||
) ?? $sql;
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
final class AppPrefixedPDO extends PDO
|
||||
{
|
||||
private string $tablePrefix = '';
|
||||
|
||||
public function setTablePrefix(string $prefix): void
|
||||
{
|
||||
$this->tablePrefix = app_validate_db_table_prefix($prefix);
|
||||
}
|
||||
|
||||
public function tablePrefix(): string
|
||||
{
|
||||
return $this->tablePrefix;
|
||||
}
|
||||
|
||||
public function prepare(string $query, array $options = []): PDOStatement|false
|
||||
{
|
||||
return parent::prepare(app_prefix_database_sql($query, $this->tablePrefix), $options);
|
||||
}
|
||||
|
||||
public function query(string $query, ?int $fetchMode = null, mixed ...$fetchModeArgs): PDOStatement|false
|
||||
{
|
||||
$query = app_prefix_database_sql($query, $this->tablePrefix);
|
||||
if ($fetchMode === null) {
|
||||
return parent::query($query);
|
||||
}
|
||||
|
||||
return parent::query($query, $fetchMode, ...$fetchModeArgs);
|
||||
}
|
||||
|
||||
public function exec(string $statement): int|false
|
||||
{
|
||||
return parent::exec(app_prefix_database_sql($statement, $this->tablePrefix));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user