Lines
n/a
0 / 0
Methods
n/a
0 / 0
Classes
n/a
0 / 0
| Name | Lines | Methods | CRAP | ||||
|---|---|---|---|---|---|---|---|
| startExecuteSqlFile | n/a | 0 / 0 | n/a | 0 / 0 | 7 | ||
| executeSql | n/a | 0 / 0 | n/a | 0 / 0 | 1 | ||
| startUsingDatabase | n/a | 0 / 0 | n/a | 0 / 0 | 3 | ||
| startTestDataImport | n/a | 0 / 0 | n/a | 0 / 0 | 1 | ||
| startConfigDataImport | n/a | 0 / 0 | n/a | 0 / 0 | 1 | ||
| startMigrations | n/a | 0 / 0 | n/a | 0 / 0 | 4 | ||
| executeTestData | n/a | 0 / 0 | n/a | 0 / 0 | 2 | ||
| openSqlFileHandle | n/a | 0 / 0 | n/a | 0 / 0 | 2 | ||
| readDelimiterDirective | n/a | 0 / 0 | n/a | 0 / 0 | 2 | ||
| lineEndsWithStatementDelimiter | n/a | 0 / 0 | n/a | 0 / 0 | 2 | ||
| filterMigrationFilesByPhase | n/a | 0 / 0 | n/a | 0 / 0 | 5 | ||
| extractSqlStatement | n/a | 0 / 0 | n/a | 0 / 0 | 2 | ||
| executeSqlStatement | n/a | 0 / 0 | n/a | 0 / 0 | 3 | ||
| applyDatabaseNameToConnection | n/a | 0 / 0 | n/a | 0 / 0 | 2 | ||
| resolveMigrationFileList | n/a | 0 / 0 | n/a | 0 / 0 | 2 | ||
| 9 | class Db | |
| 10 | { | |
| 11 | public static $baseDSN = ''; | |
| 12 | ||
| 13 | public static function startExecuteSqlFile(string $file, $databaseName = null, bool $verbose = true): void | |
| 14 | { | |
| 15 | $databaseConnection = self::startUsingDatabase($databaseName, $verbose); | |
| 16 | $startedAt = microtime(true); | |
| 17 | $sqlFileHandle = self::openSqlFileHandle($file); | |
| 18 | ||
| 19 | if ($verbose) { | |
| 20 | \App::$log->info('Importing SQL file', ['file' => basename($file)]); | |
| 21 | } | |
| 22 | ||
| 23 | $statementDelimiter = ';'; | |
| 24 | $statementBuffer = ''; | |
| 25 | ||
| 26 | while ($line = $sqlFileHandle['readLine']($sqlFileHandle['handle'])) { | |
| 27 | $delimiterFromLine = self::readDelimiterDirective($line); | |
| 28 | if ($delimiterFromLine !== null) { | |
| 29 | $statementDelimiter = $delimiterFromLine; | |
| 30 | continue; | |
| 31 | } | |
| 32 | ||
| 33 | $statementBuffer .= $line; | |
| 34 | if (!self::lineEndsWithStatementDelimiter($line, $statementDelimiter)) { | |
| 35 | continue; | |
| 36 | } | |
| 37 | ||
| 38 | $sqlStatement = self::extractSqlStatement($statementBuffer, $statementDelimiter); | |
| 39 | $statementBuffer = ''; | |
| 40 | ||
| 41 | if ($sqlStatement === '') { | |
| 42 | continue; | |
| 43 | } | |
| 44 | ||
| 45 | self::executeSqlStatement($databaseConnection, $sqlStatement, $file, $verbose); | |
| 46 | } | |
| 47 | ||
| 48 | $sqlFileHandle['close']($sqlFileHandle['handle']); | |
| 49 | ||
| 50 | if ($verbose) { | |
| 51 | \App::$log->info('SQL import finished', [ | |
| 52 | 'file' => basename($file), | |
| 53 | 'seconds' => round(microtime(true) - $startedAt, 3), | |
| 54 | ]); | |
| 55 | } | |
| 56 | } | |
| 57 | ||
| 58 | /** | |
| 59 | * @psalm-api | |
| 60 | */ | |
| 61 | public static function executeSql($query, $databaseName = null): void | |
| 62 | { | |
| 63 | $databaseConnection = self::startUsingDatabase($databaseName, false); | |
| 64 | $databaseConnection->exec($query); | |
| 65 | } | |
| 66 | ||
| 67 | /** | |
| 68 | * @param null|string $databaseName | |
| 69 | * | |
| 70 | */ | |
| 71 | public static function startUsingDatabase(string|null $databaseName = null, bool $verbose = true): \BO\Zmsbackend\Connection\Pdo | |
| 72 | { | |
| 73 | if (!self::$baseDSN) { | |
| 74 | self::$baseDSN = \BO\Zmsbackend\Connection\Select::$writeSourceName; | |
| 75 | } | |
| 76 | ||
| 77 | \BO\Zmsbackend\Connection\Select::closeWriteConnection(); | |
| 78 | self::applyDatabaseNameToConnection($databaseName); | |
| 79 | ||
| 80 | if ($verbose) { | |
| 81 | \App::$log->info('Using database connection', [ | |
| 82 | 'dsn' => \BO\Zmsbackend\Connection\Select::$writeSourceName, | |
| 83 | ]); | |
| 84 | } | |
| 85 | ||
| 86 | return \BO\Zmsbackend\Connection\Select::getWriteConnection(); | |
| 87 | } | |
| 88 | ||
| 89 | public static function startTestDataImport($fixturesDirectory, $filename = 'mysql_zmsbo.sql'): void | |
| 90 | { | |
| 91 | $defaultDatabaseName = \BO\Zmsbackend\Connection\Select::$dbname_zms; | |
| 92 | ||
| 93 | $databaseConnection = self::startUsingDatabase('information_schema'); | |
| 94 | $databaseConnection->exec("DROP DATABASE IF EXISTS `$defaultDatabaseName`;"); | |
| 95 | $databaseConnection->exec("CREATE DATABASE IF NOT EXISTS `$defaultDatabaseName`;"); | |
| 96 | ||
| 97 | self::startExecuteSqlFile($fixturesDirectory . '/' . $filename); | |
| 98 | } | |
| 99 | ||
| 100 | public static function startConfigDataImport(): void | |
| 101 | { | |
| 102 | $defaults = new \BO\Zmsentities\Config(); | |
| 103 | (new \BO\Zmsbackend\Config\Service\Config())->updateEntity($defaults); | |
| 104 | } | |
| 105 | ||
| 106 | public static function startMigrations($migrationList, bool $commit = true, ?string $phase = null): int | |
| 107 | { | |
| 108 | $migrationFiles = self::resolveMigrationFileList($migrationList); | |
| 109 | $migrationFiles = self::filterMigrationFilesByPhase($migrationFiles, $phase); | |
| 110 | $databaseConnection = self::startUsingDatabase(); | |
| 111 | $completedMigrations = $databaseConnection->fetchPairs( | |
| 112 | 'SELECT filename, changeTimestamp FROM migrations' | |
| 113 | ); | |
| 114 | $addedMigrationCount = 0; | |
| 115 | ||
| 116 | foreach ($migrationFiles as $migrationFile) { | |
| 117 | $migrationFilename = basename($migrationFile); | |
| 118 | if (array_key_exists($migrationFilename, $completedMigrations)) { | |
| 119 | continue; | |
| 120 | } | |
| 121 | ||
| 122 | $addedMigrationCount++; | |
| 123 | if (!$commit) { | |
| 124 | \App::$log->info('Pending migration', [ | |
| 125 | 'index' => $addedMigrationCount, | |
| 126 | 'migration' => $migrationFilename, | |
| 127 | ]); | |
| 128 | continue; | |
| 129 | } | |
| 130 | ||
| 131 | self::startExecuteSqlFile($migrationFile); | |
| 132 | $databaseConnection->prepare('INSERT INTO `migrations` SET `filename` = :filename') | |
| 133 | ->execute(['filename' => $migrationFilename]); | |
| 134 | } | |
| 135 | ||
| 136 | \App::$log->info('Migration check finished', [ | |
| 137 | 'completed' => count($completedMigrations), | |
| 138 | 'added' => $addedMigrationCount, | |
| 139 | ]); | |
| 140 | ||
| 141 | return $addedMigrationCount; | |
| 142 | } | |
| 143 | ||
| 144 | /** | |
| 145 | * @return void | |
| 146 | */ | |
| 147 | public static function executeTestData(string $testName, string $step) | |
| 148 | { | |
| 149 | $fixturesDirectory = realpath(__DIR__ . '/../../../tests/Zmsbackend/Service/fixtures/'); | |
| 150 | $sqlFile = $fixturesDirectory . '/' . $testName . '/' . $step . '.sql'; | |
| 151 | ||
| 152 | if (!file_exists($sqlFile)) { | |
| 153 | return; | |
| 154 | } | |
| 155 | ||
| 156 | self::startExecuteSqlFile($sqlFile, null, false); | |
| 157 | } | |
| 158 | ||
| 159 | private static function openSqlFileHandle(string $file): array | |
| 160 | { | |
| 161 | $isGzipCompressed = substr($file, -3) === '.gz'; | |
| 162 | ||
| 163 | if ($isGzipCompressed) { | |
| 164 | return [ | |
| 165 | 'handle' => gzopen($file, 'r'), | |
| 166 | 'readLine' => 'gzgets', | |
| 167 | 'close' => 'gzclose', | |
| 168 | ]; | |
| 169 | } | |
| 170 | ||
| 171 | return [ | |
| 172 | 'handle' => fopen($file, 'r'), | |
| 173 | 'readLine' => 'fgets', | |
| 174 | 'close' => 'fclose', | |
| 175 | ]; | |
| 176 | } | |
| 177 | ||
| 178 | private static function readDelimiterDirective(string $line): ?string | |
| 179 | { | |
| 180 | if (!preg_match('/^\s*DELIMITER\s+(\S+)\s*$/i', rtrim($line), $matches)) { | |
| 181 | return null; | |
| 182 | } | |
| 183 | ||
| 184 | return $matches[1]; | |
| 185 | } | |
| 186 | ||
| 187 | private static function lineEndsWithStatementDelimiter(string $line, string $statementDelimiter): bool | |
| 188 | { | |
| 189 | if ($statementDelimiter === ';') { | |
| 190 | return (bool) preg_match('/;\s*$/', $line); | |
| 191 | } | |
| 192 | ||
| 193 | return (bool) preg_match( | |
| 194 | '/' . preg_quote($statementDelimiter, '/') . '\s*$/', | |
| 195 | rtrim($line) | |
| 196 | ); | |
| 197 | } | |
| 198 | ||
| 199 | /** | |
| 200 | * Split migrations into expand/contract phases by filename for zero-downtime | |
| 201 | * (Expand–Contract) deployments. | |
| 202 | * | |
| 203 | * - contract phase: files whose name contains "-contract-" or "-contract." (destructive cleanup) | |
| 204 | * - expand phase: every other file, i.e. "*-expand-*" plus unprefixed additive migrations | |
| 205 | * - no phase (null/empty): all files, preserving the legacy behaviour | |
| 206 | */ | |
| 207 | private static function filterMigrationFilesByPhase(array $migrationFiles, ?string $phase): array | |
| 208 | { | |
| 209 | if ($phase === null || $phase === '') { | |
| 210 | return $migrationFiles; | |
| 211 | } | |
| 212 | ||
| 213 | $isContract = static function (string $migrationFile): bool { | |
| 214 | return (bool) preg_match('/-contract[-.]/', basename($migrationFile)); | |
| 215 | }; | |
| 216 | ||
| 217 | if ($phase === 'contract') { | |
| 218 | return array_values(array_filter($migrationFiles, $isContract)); | |
| 219 | } | |
| 220 | ||
| 221 | if ($phase === 'expand') { | |
| 222 | return array_values(array_filter( | |
| 223 | $migrationFiles, | |
| 224 | static fn(string $migrationFile): bool => !$isContract($migrationFile) | |
| 225 | )); | |
| 226 | } | |
| 227 | ||
| 228 | throw new \InvalidArgumentException( | |
| 229 | "Unknown migration phase '$phase'; expected 'expand' or 'contract'" | |
| 230 | ); | |
| 231 | } | |
| 232 | ||
| 233 | private static function extractSqlStatement(string $statementBuffer, string $statementDelimiter): string | |
| 234 | { | |
| 235 | $sqlStatement = $statementBuffer; | |
| 236 | ||
| 237 | if ($statementDelimiter !== ';') { | |
| 238 | $sqlStatement = preg_replace( | |
| 239 | '/' . preg_quote($statementDelimiter, '/') . '\s*$/', | |
| 240 | '', | |
| 241 | $sqlStatement | |
| 242 | ); | |
| 243 | } | |
| 244 | ||
| 245 | return trim($sqlStatement); | |
| 246 | } | |
| 247 | ||
| 248 | private static function executeSqlStatement( | |
| 249 | \BO\Zmsbackend\Connection\Pdo $databaseConnection, | |
| 250 | string $sqlStatement, | |
| 251 | string $sourceFile, | |
| 252 | bool $verbose | |
| 253 | ): void { | |
| 254 | try { | |
| 255 | $databaseConnection->exec($sqlStatement); | |
| 256 | } catch (\Exception $exception) { | |
| 257 | if ($verbose) { | |
| 258 | \App::$log->error('SQL import failed', [ | |
| 259 | 'file' => basename($sourceFile), | |
| 260 | 'method' => __METHOD__, | |
| 261 | 'exception' => get_class($exception), | |
| 262 | 'message' => $exception->getMessage(), | |
| 263 | 'code' => $exception->getCode(), | |
| 264 | ]); | |
| 265 | } | |
| 266 | ||
| 267 | throw $exception; | |
| 268 | } | |
| 269 | } | |
| 270 | ||
| 271 | private static function applyDatabaseNameToConnection(?string $databaseName): void | |
| 272 | { | |
| 273 | if ($databaseName === null) { | |
| 274 | \BO\Zmsbackend\Connection\Select::$writeSourceName = self::$baseDSN; | |
| 275 | return; | |
| 276 | } | |
| 277 | ||
| 278 | $defaultDatabaseName = \BO\Zmsbackend\Connection\Select::$dbname_zms; | |
| 279 | \BO\Zmsbackend\Connection\Select::$writeSourceName = preg_replace( | |
| 280 | "#dbname=$defaultDatabaseName.*?;#", | |
| 281 | "dbname=$databaseName;", | |
| 282 | self::$baseDSN | |
| 283 | ); | |
| 284 | } | |
| 285 | ||
| 286 | private static function resolveMigrationFileList($migrationList): array | |
| 287 | { | |
| 288 | if (!is_array($migrationList)) { | |
| 289 | $migrationList = glob($migrationList . '/*.sql'); | |
| 290 | } | |
| 291 | ||
| 292 | sort($migrationList); | |
| 293 | ||
| 294 | return $migrationList; | |
| 295 | } | |
| 296 | } |