Lines
52.41%
76 / 145
Methods
59.25%
16 / 27
Classes
0.00%
0 / 1
| Name | Lines | Methods | CRAP | ||||
|---|---|---|---|---|---|---|---|
| __construct | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 1 | ||
| getDependencies | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| isReversible | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| down | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| table | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| isDbType | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 1 | ||
| isMySql | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| isPostgreSql | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| isSqlite | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| isSqlServer | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| addColumn | 62.50% | 5 / 8 | 0.00% | 0 / 1 | 4.84 | ||
| dropColumn | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 1 | ||
| dropColumns | 100.00% | 6 / 6 | 100.00% | 1 / 1 | 1 | ||
| createIndex | 33.33% | 5 / 15 | 0.00% | 0 / 1 | 8.74 | ||
| createUniqueIndex | 28.57% | 6 / 21 | 0.00% | 0 / 1 | 6.28 | ||
| indexExists | 40.62% | 13 / 32 | 0.00% | 0 / 1 | 10.23 | ||
| dropTable | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| dropTableIfExists | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| updateLanguageCode | 100.00% | 11 / 11 | 100.00% | 1 / 1 | 1 | ||
| integerType | 66.66% | 2 / 3 | 0.00% | 0 / 1 | 3.33 | ||
| textType | 66.66% | 2 / 3 | 0.00% | 0 / 1 | 3.33 | ||
| varcharType | 0.00% | 0 / 3 | 0.00% | 0 / 1 | 12 | ||
| timestampType | 40.00% | 4 / 10 | 0.00% | 0 / 1 | 21.82 | ||
| booleanType | 0.00% | 0 / 4 | 0.00% | 0 / 1 | 20 | ||
| autoIncrementColumn | 0.00% | 0 / 6 | 0.00% | 0 / 1 | 42 | ||
| getConfig | 0.00% | 0 / 1 | 0.00% | 0 / 1 | 2 | ||
| getChecksum | 100.00% | 6 / 6 | 100.00% | 1 / 1 | 1 | ||
| 27 | abstract readonly class AbstractMigration implements MigrationInterface | |
| 28 | { | |
| 29 | protected string $tablePrefix; | |
| 30 | protected string $dbType; | |
| 31 | ||
| 32 | public function __construct( | |
| 33 | protected Configuration $configuration, | |
| 34 | ) { | |
| 35 | $this->tablePrefix = Database::getTablePrefix(); | |
| 36 | $this->dbType = Database::getType(); | |
| 37 | } | |
| 38 | ||
| 39 | /** | |
| 40 | * Default implementation returns an empty array (no dependencies). | |
| 41 | * | |
| 42 | * @return string[] | |
| 43 | */ | |
| 44 | public function getDependencies(): array | |
| 45 | { | |
| 46 | return []; | |
| 47 | } | |
| 48 | ||
| 49 | /** | |
| 50 | * Default implementation - migrations are not reversible unless overridden. | |
| 51 | */ | |
| 52 | public function isReversible(): bool | |
| 53 | { | |
| 54 | return false; | |
| 55 | } | |
| 56 | ||
| 57 | /** | |
| 58 | * Default implementation - does nothing. Override in subclass if migration is reversible. | |
| 59 | */ | |
| 60 | public function down(OperationRecorder $recorder): void | |
| 61 | { | |
| 62 | // Override in subclass if migration is reversible | |
| 63 | } | |
| 64 | ||
| 65 | /** | |
| 66 | * Returns the table name with a prefix. | |
| 67 | */ | |
| 68 | protected function table(string $name): string | |
| 69 | { | |
| 70 | return $this->tablePrefix . $name; | |
| 71 | } | |
| 72 | ||
| 73 | /** | |
| 74 | * Checks if the current database type matches any of the given types. | |
| 75 | * | |
| 76 | * @param string|string[] $types Database type(s) to check | |
| 77 | */ | |
| 78 | protected function isDbType(string|array $types): bool | |
| 79 | { | |
| 80 | $types = (array) $types; | |
| 81 | return in_array($this->dbType, $types, strict: true); | |
| 82 | } | |
| 83 | ||
| 84 | /** | |
| 85 | * Returns true if running MySQL/MariaDB. | |
| 86 | */ | |
| 87 | protected function isMySql(): bool | |
| 88 | { | |
| 89 | return $this->isDbType(['mysqli', 'pdo_mysql']); | |
| 90 | } | |
| 91 | ||
| 92 | /** | |
| 93 | * Returns true if running PostgreSQL. | |
| 94 | */ | |
| 95 | protected function isPostgreSql(): bool | |
| 96 | { | |
| 97 | return $this->isDbType(['pgsql', 'pdo_pgsql']); | |
| 98 | } | |
| 99 | ||
| 100 | /** | |
| 101 | * Returns true if running SQLite. | |
| 102 | */ | |
| 103 | protected function isSqlite(): bool | |
| 104 | { | |
| 105 | return $this->isDbType(['sqlite3', 'pdo_sqlite']); | |
| 106 | } | |
| 107 | ||
| 108 | /** | |
| 109 | * Returns true if running SQL Server. | |
| 110 | */ | |
| 111 | protected function isSqlServer(): bool | |
| 112 | { | |
| 113 | return $this->isDbType(['sqlsrv', 'pdo_sqlsrv']); | |
| 114 | } | |
| 115 | ||
| 116 | /** | |
| 117 | * Helper to add a column to a table. | |
| 118 | * Returns the appropriate SQL for the current database type. | |
| 119 | */ | |
| 120 | protected function addColumn(string $table, string $column, string $type, ?string $default = null): string | |
| 121 | { | |
| 122 | $tableName = $this->table($table); | |
| 123 | $defaultClause = ''; | |
| 124 | if ($default !== null) { | |
| 125 | $defaultClause = " DEFAULT {$default}"; | |
| 126 | } | |
| 127 | ||
| 128 | if ($this->isSqlite()) { | |
| 129 | return sprintf('ALTER TABLE %s ADD COLUMN %s %s%s', $tableName, $column, $type, $defaultClause); | |
| 130 | } | |
| 131 | ||
| 132 | // MySQL, PostgreSQL, SQL Server | |
| 133 | $columnKeyword = $this->isSqlServer() ? '' : 'COLUMN '; | |
| 134 | return sprintf('ALTER TABLE %s ADD %s%s %s%s', $tableName, $columnKeyword, $column, $type, $defaultClause); | |
| 135 | } | |
| 136 | ||
| 137 | /** | |
| 138 | * Helper to drop a column from a table. | |
| 139 | */ | |
| 140 | protected function dropColumn(string $table, string $column): string | |
| 141 | { | |
| 142 | $tableName = $this->table($table); | |
| 143 | return sprintf('ALTER TABLE %s DROP COLUMN %s', $tableName, $column); | |
| 144 | } | |
| 145 | ||
| 146 | /** | |
| 147 | * Helper to drop multiple columns from a table. | |
| 148 | * Returns one ALTER TABLE ... DROP COLUMN statement per column for cross-database compatibility. | |
| 149 | * | |
| 150 | * @param string[] $columns | |
| 151 | * @return string[] | |
| 152 | */ | |
| 153 | protected function dropColumns(string $table, array $columns): array | |
| 154 | { | |
| 155 | $tableName = $this->table($table); | |
| 156 | return array_map(static fn(string $col): string => sprintf( | |
| 157 | 'ALTER TABLE %s DROP COLUMN %s', | |
| 158 | $tableName, | |
| 159 | $col, | |
| 160 | ), $columns); | |
| 161 | } | |
| 162 | ||
| 163 | /** | |
| 164 | * Helper to create an index. | |
| 165 | * | |
| 166 | * @param string|string[] $columns | |
| 167 | */ | |
| 168 | protected function createIndex(string $table, string $indexName, string|array $columns): string | |
| 169 | { | |
| 170 | $tableName = $this->table($table); | |
| 171 | $columnList = is_array($columns) ? implode(', ', $columns) : $columns; | |
| 172 | ||
| 173 | if ($this->isSqlServer()) { | |
| 174 | return sprintf( | |
| 175 | "IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = '%s' AND object_id = OBJECT_ID('%s')) " | |
| 176 | . 'CREATE INDEX %s ON %s (%s)', | |
| 177 | $indexName, | |
| 178 | $tableName, | |
| 179 | $indexName, | |
| 180 | $tableName, | |
| 181 | $columnList, | |
| 182 | ); | |
| 183 | } | |
| 184 | ||
| 185 | // MySQL/MariaDB don't support IF NOT EXISTS in older versions | |
| 186 | // Use conditional execution or manual check instead | |
| 187 | if ($this->isMySql()) { | |
| 188 | return sprintf('CREATE INDEX %s ON %s (%s)', $indexName, $tableName, $columnList); | |
| 189 | } | |
| 190 | ||
| 191 | // PostgreSQL and SQLite support IF NOT EXISTS | |
| 192 | return sprintf('CREATE INDEX IF NOT EXISTS %s ON %s (%s)', $indexName, $tableName, $columnList); | |
| 193 | } | |
| 194 | ||
| 195 | /** | |
| 196 | * Helper to create a UNIQUE index. | |
| 197 | * | |
| 198 | * On SQL Server, NULLs are treated as equal in unique indexes, so a filtered | |
| 199 | * index is emitted to allow multiple NULL values. MySQL/MariaDB, PostgreSQL | |
| 200 | * and SQLite already treat NULLs as distinct in unique indexes. | |
| 201 | * | |
| 202 | * @param string|string[] $columns | |
| 203 | */ | |
| 204 | protected function createUniqueIndex(string $table, string $indexName, string|array $columns): string | |
| 205 | { | |
| 206 | $tableName = $this->table($table); | |
| 207 | $columns = (array) $columns; | |
| 208 | $columnList = implode(', ', $columns); | |
| 209 | ||
| 210 | if ($this->isSqlServer()) { | |
| 211 | $whereClause = implode(' AND ', array_map(static fn(string $col): string => sprintf( | |
| 212 | '%s IS NOT NULL', | |
| 213 | $col, | |
| 214 | ), $columns)); | |
| 215 | return sprintf( | |
| 216 | "IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = '%s' AND object_id = OBJECT_ID('%s')) " | |
| 217 | . 'CREATE UNIQUE INDEX %s ON %s (%s) WHERE %s', | |
| 218 | $indexName, | |
| 219 | $tableName, | |
| 220 | $indexName, | |
| 221 | $tableName, | |
| 222 | $columnList, | |
| 223 | $whereClause, | |
| 224 | ); | |
| 225 | } | |
| 226 | ||
| 227 | if ($this->isMySql()) { | |
| 228 | return sprintf('CREATE UNIQUE INDEX %s ON %s (%s)', $indexName, $tableName, $columnList); | |
| 229 | } | |
| 230 | ||
| 231 | return sprintf('CREATE UNIQUE INDEX IF NOT EXISTS %s ON %s (%s)', $indexName, $tableName, $columnList); | |
| 232 | } | |
| 233 | ||
| 234 | /** | |
| 235 | * Helper to check if an index exists. | |
| 236 | * Returns a SQL query that checks for index existence. | |
| 237 | */ | |
| 238 | protected function indexExists(string $table, string $indexName): string | |
| 239 | { | |
| 240 | $db = $this->configuration->getDb(); | |
| 241 | $tableName = $this->table($table); | |
| 242 | $escapedTableName = $db->escape($tableName); | |
| 243 | $escapedIndexName = $db->escape($indexName); | |
| 244 | ||
| 245 | if ($this->isMySql()) { | |
| 246 | return sprintf( | |
| 247 | 'SELECT COUNT(*) as idx_count FROM information_schema.STATISTICS ' | |
| 248 | . "WHERE table_schema = DATABASE() AND table_name = '%s' AND index_name = '%s'", | |
| 249 | $escapedTableName, | |
| 250 | $escapedIndexName, | |
| 251 | ); | |
| 252 | } | |
| 253 | ||
| 254 | if ($this->isPostgreSql()) { | |
| 255 | return sprintf( | |
| 256 | 'SELECT COUNT(*) as idx_count FROM pg_indexes ' | |
| 257 | . "WHERE schemaname = 'public' AND tablename = '%s' AND indexname = '%s'", | |
| 258 | $escapedTableName, | |
| 259 | $escapedIndexName, | |
| 260 | ); | |
| 261 | } | |
| 262 | ||
| 263 | if ($this->isSqlite()) { | |
| 264 | return sprintf( | |
| 265 | 'SELECT COUNT(*) as idx_count FROM sqlite_master ' | |
| 266 | . "WHERE type = 'index' AND name = '%s' AND tbl_name = '%s'", | |
| 267 | $escapedIndexName, | |
| 268 | $escapedTableName, | |
| 269 | ); | |
| 270 | } | |
| 271 | ||
| 272 | if ($this->isSqlServer()) { | |
| 273 | return sprintf( | |
| 274 | 'SELECT COUNT(*) as idx_count FROM sys.indexes ' . "WHERE name = '%s' AND object_id = OBJECT_ID('%s')", | |
| 275 | $escapedIndexName, | |
| 276 | $escapedTableName, | |
| 277 | ); | |
| 278 | } | |
| 279 | ||
| 280 | throw new \RuntimeException("Unsupported database type for index existence check: {$this->dbType}"); | |
| 281 | } | |
| 282 | ||
| 283 | /** | |
| 284 | * Helper to drop a table. | |
| 285 | */ | |
| 286 | protected function dropTable(string $table): string | |
| 287 | { | |
| 288 | return sprintf('DROP TABLE %s', $this->table($table)); | |
| 289 | } | |
| 290 | ||
| 291 | /** | |
| 292 | * Helper to drop a table if it exists. | |
| 293 | */ | |
| 294 | protected function dropTableIfExists(string $table): string | |
| 295 | { | |
| 296 | return sprintf('DROP TABLE IF EXISTS %s', $this->table($table)); | |
| 297 | } | |
| 298 | ||
| 299 | /** | |
| 300 | * Helper for UPDATE queries with a language code fix pattern. | |
| 301 | */ | |
| 302 | protected function updateLanguageCode(string $table, string $column, string $oldCode, string $newCode): string | |
| 303 | { | |
| 304 | $db = $this->configuration->getDb(); | |
| 305 | $escapedOldCode = $db->escape($oldCode); | |
| 306 | $escapedNewCode = $db->escape($newCode); | |
| 307 | ||
| 308 | return sprintf( | |
| 309 | "UPDATE %s SET %s='%s' WHERE %s='%s'", | |
| 310 | $this->table($table), | |
| 311 | $column, | |
| 312 | $escapedNewCode, | |
| 313 | $column, | |
| 314 | $escapedOldCode, | |
| 315 | ); | |
| 316 | } | |
| 317 | ||
| 318 | /** | |
| 319 | * Returns the INTEGER type appropriate for the database. | |
| 320 | */ | |
| 321 | protected function integerType(): string | |
| 322 | { | |
| 323 | return match ($this->dbType) { | |
| 324 | 'mysqli', 'pdo_mysql' => 'INT', | |
| 325 | default => 'INTEGER', | |
| 326 | }; | |
| 327 | } | |
| 328 | ||
| 329 | /** | |
| 330 | * Returns the TEXT type appropriate for the database. | |
| 331 | */ | |
| 332 | protected function textType(): string | |
| 333 | { | |
| 334 | return match ($this->dbType) { | |
| 335 | 'sqlsrv', 'pdo_sqlsrv' => 'NVARCHAR(MAX)', | |
| 336 | default => 'TEXT', | |
| 337 | }; | |
| 338 | } | |
| 339 | ||
| 340 | /** | |
| 341 | * Returns the VARCHAR type appropriate for the database. | |
| 342 | */ | |
| 343 | protected function varcharType(int $length): string | |
| 344 | { | |
| 345 | return match ($this->dbType) { | |
| 346 | 'sqlsrv', 'pdo_sqlsrv' => "NVARCHAR({$length})", | |
| 347 | default => "VARCHAR({$length})", | |
| 348 | }; | |
| 349 | } | |
| 350 | ||
| 351 | /** | |
| 352 | * Returns the TIMESTAMP/DATETIME type with default appropriate for the database. | |
| 353 | */ | |
| 354 | protected function timestampType(bool $withDefault = true): string | |
| 355 | { | |
| 356 | $type = match ($this->dbType) { | |
| 357 | 'mysqli', 'pdo_mysql' => 'TIMESTAMP', | |
| 358 | 'sqlsrv', 'pdo_sqlsrv' => 'DATETIME', | |
| 359 | default => 'TIMESTAMP', | |
| 360 | }; | |
| 361 | ||
| 362 | if (!$withDefault) { | |
| 363 | return $type; | |
| 364 | } | |
| 365 | ||
| 366 | return match ($this->dbType) { | |
| 367 | 'sqlsrv', 'pdo_sqlsrv' => $type . ' NOT NULL DEFAULT GETDATE()', | |
| 368 | 'sqlite3', 'pdo_sqlite' => 'DATETIME DEFAULT CURRENT_TIMESTAMP', | |
| 369 | default => $type . ' NOT NULL DEFAULT CURRENT_TIMESTAMP', | |
| 370 | }; | |
| 371 | } | |
| 372 | ||
| 373 | /** | |
| 374 | * Returns the BOOLEAN/TINYINT type appropriate for the database. | |
| 375 | */ | |
| 376 | protected function booleanType(): string | |
| 377 | { | |
| 378 | return match ($this->dbType) { | |
| 379 | 'mysqli', 'pdo_mysql' => 'TINYINT(1)', | |
| 380 | 'sqlsrv', 'pdo_sqlsrv' => 'TINYINT', | |
| 381 | default => 'INTEGER', | |
| 382 | }; | |
| 383 | } | |
| 384 | ||
| 385 | /** | |
| 386 | * Returns the auto-increment column definition appropriate for the database. | |
| 387 | */ | |
| 388 | protected function autoIncrementColumn(string $columnName = 'id'): string | |
| 389 | { | |
| 390 | return match ($this->dbType) { | |
| 391 | 'mysqli', 'pdo_mysql' => "{$columnName} INT NOT NULL AUTO_INCREMENT", | |
| 392 | 'pgsql', 'pdo_pgsql' => "{$columnName} SERIAL NOT NULL", | |
| 393 | 'sqlite3', 'pdo_sqlite' => "{$columnName} INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT", | |
| 394 | 'sqlsrv', 'pdo_sqlsrv' => "{$columnName} INT IDENTITY(1,1) NOT NULL", | |
| 395 | default => "{$columnName} INTEGER NOT NULL", | |
| 396 | }; | |
| 397 | } | |
| 398 | ||
| 399 | /** | |
| 400 | * Gets a configuration value safely. | |
| 401 | */ | |
| 402 | protected function getConfig(string $key): mixed | |
| 403 | { | |
| 404 | return $this->configuration->get(item: $key); | |
| 405 | } | |
| 406 | ||
| 407 | /** | |
| 408 | * Calculates a checksum for migration integrity. | |
| 409 | */ | |
| 410 | public function getChecksum(): string | |
| 411 | { | |
| 412 | $data = [ | |
| 413 | 'version' => $this->getVersion(), | |
| 414 | 'description' => $this->getDescription(), | |
| 415 | 'class' => static::class, | |
| 416 | ]; | |
| 417 | return hash('sha256', (string) json_encode($data)); | |
| 418 | } | |
| 419 | } |