coniadev /
quma
| 1 | <?php |
||
| 2 | |||
| 3 | declare(strict_types=1); |
||
| 4 | |||
| 5 | namespace Conia\Quma; |
||
| 6 | |||
| 7 | use Conia\Quma\Util; |
||
| 8 | use PDO; |
||
| 9 | use RuntimeException; |
||
| 10 | use ValueError; |
||
| 11 | |||
| 12 | /** |
||
| 13 | * @psalm-api |
||
| 14 | * |
||
| 15 | * @psalm-type MigrationDirs = list<non-empty-string> |
||
| 16 | * @psalm-type SqlDirs = list<non-empty-string> |
||
| 17 | * @psalm-type SqlAssoc = array<non-empty-string, non-empty-string> |
||
| 18 | * @psalm-type SqlMixed = list<non-empty-string|SqlAssoc> |
||
| 19 | * @psalm-type SqlConfig = non-empty-string|SqlAssoc|SqlMixed |
||
| 20 | */ |
||
| 21 | class Connection |
||
| 22 | { |
||
| 23 | use GetsSetsPrint; |
||
| 24 | |||
| 25 | /** @psalm-var non-empty-string */ |
||
| 26 | public readonly string $driver; |
||
| 27 | |||
| 28 | /** @psalm-var SqlDirs */ |
||
| 29 | protected array $sql; |
||
| 30 | |||
| 31 | /** @psalm-var MigrationDirs */ |
||
| 32 | protected array $migrations; |
||
| 33 | |||
| 34 | protected string $migrationsTable = 'migrations'; |
||
| 35 | protected string $migrationsColumnMigration = 'migration'; |
||
| 36 | protected string $migrationsColumnApplied = 'applied'; |
||
| 37 | |||
| 38 | /** |
||
| 39 | * @psalm-param SqlConfig $sql |
||
| 40 | * @psalm-param MigrationDirs $migrations |
||
| 41 | * */ |
||
| 42 | 84 | public function __construct( |
|
| 43 | public readonly string $dsn, |
||
| 44 | string|array $sql, |
||
| 45 | string|array $migrations = null, |
||
| 46 | public readonly ?string $username = null, |
||
| 47 | public readonly ?string $password = null, |
||
| 48 | public readonly array $options = [], |
||
| 49 | public readonly int $fetchMode = PDO::FETCH_BOTH, |
||
| 50 | bool $print = false |
||
| 51 | ) { |
||
| 52 | 84 | $this->driver = $this->readDriver($this->dsn); |
|
| 53 | 83 | $this->sql = $this->readDirs($sql); |
|
| 54 | 82 | $this->migrations = $this->readDirs($migrations ?? []); |
|
| 55 | 81 | $this->print = $print; |
|
| 56 | } |
||
| 57 | |||
| 58 | 73 | public function setMigrationsTable(string $table): void |
|
| 59 | { |
||
| 60 | 73 | $this->migrationsTable = $table; |
|
| 61 | } |
||
| 62 | |||
| 63 | 2 | public function setMigrationsColumnMigration(string $column): void |
|
| 64 | { |
||
| 65 | 2 | $this->migrationsColumnMigration = $column; |
|
| 66 | } |
||
| 67 | |||
| 68 | 2 | public function setMigrationsColumnApplied(string $column): void |
|
| 69 | { |
||
| 70 | 2 | $this->migrationsColumnApplied = $column; |
|
| 71 | } |
||
| 72 | |||
| 73 | 40 | public function migrationsTable(): string |
|
| 74 | { |
||
| 75 | 40 | if ($this->driver === 'pgsql') { |
|
| 76 | // PostgreSQL table names can contain a schema |
||
| 77 | 9 | if (preg_match('/^([a-zA-Z0-9_]+\.)?[a-zA-Z0-9_]+$/', $this->migrationsTable)) { |
|
| 78 | 9 | return $this->migrationsTable; |
|
| 79 | } |
||
| 80 | } else { |
||
| 81 | 31 | if (preg_match('/^[a-zA-Z0-9_]+$/', $this->migrationsTable)) { |
|
| 82 | 30 | return $this->migrationsTable; |
|
| 83 | } |
||
| 84 | } |
||
| 85 | |||
| 86 | 1 | throw new ValueError('Invalid migrations table name: ' . $this->migrationsTable); |
|
| 87 | } |
||
| 88 | |||
| 89 | 40 | public function migrationsColumnMigration(): string |
|
| 90 | { |
||
| 91 | 40 | return $this->getColumnName($this->migrationsColumnMigration); |
|
| 92 | } |
||
| 93 | |||
| 94 | 40 | public function migrationsColumnApplied(): string |
|
| 95 | { |
||
| 96 | 40 | return $this->getColumnName($this->migrationsColumnApplied); |
|
| 97 | } |
||
| 98 | |||
| 99 | /** @psalm-param non-empty-string $migrations */ |
||
| 100 | 1 | public function addMigrationDir(string $migrations): void |
|
| 101 | { |
||
| 102 | 1 | $migrations = $this->readDirs($migrations); |
|
| 103 | 1 | $this->migrations = array_merge($migrations, $this->migrations); |
|
| 104 | } |
||
| 105 | |||
| 106 | /** @psalm-return MigrationDirs */ |
||
| 107 | 28 | public function migrations(): array |
|
| 108 | { |
||
| 109 | 28 | return $this->migrations; |
|
| 110 | } |
||
| 111 | |||
| 112 | /** @psalm-param SqlConfig $sql */ |
||
| 113 | 1 | public function addSqlDirs(array|string $sql): void |
|
| 114 | { |
||
| 115 | 1 | $sql = $this->readDirs($sql); |
|
| 116 | 1 | $this->sql = array_merge($sql, $this->sql); |
|
| 117 | } |
||
| 118 | |||
| 119 | 31 | public function sql(): array |
|
| 120 | { |
||
| 121 | 31 | return $this->sql; |
|
| 122 | } |
||
| 123 | |||
| 124 | /** @psalm-return non-empty-string */ |
||
| 125 | 83 | protected function preparePath(string $path): string |
|
| 126 | { |
||
| 127 | 83 | $result = realpath($path); |
|
| 128 | |||
| 129 | 83 | if ($result) { |
|
| 130 | 83 | return $result; |
|
| 131 | } |
||
| 132 | |||
| 133 | 1 | throw new ValueError("Path does not exist: {$path}"); |
|
| 134 | } |
||
| 135 | |||
| 136 | /** @psalm-return non-empty-string */ |
||
| 137 | 84 | protected function readDriver(string $dsn): string |
|
| 138 | { |
||
| 139 | 84 | $driver = explode(':', $dsn)[0]; |
|
| 140 | |||
| 141 | 84 | if (in_array($driver, PDO::getAvailableDrivers())) { |
|
| 142 | assert(!empty($driver)); |
||
| 143 | |||
| 144 | 83 | return $driver; |
|
| 145 | } |
||
| 146 | |||
| 147 | 1 | throw new RuntimeException('PDO driver not supported: ' . $driver); |
|
| 148 | } |
||
| 149 | |||
| 150 | /** |
||
| 151 | * @psalm-param SqlAssoc $entry |
||
| 152 | * |
||
| 153 | * @psalm-return MigrationDirs |
||
| 154 | */ |
||
| 155 | 7 | protected function prepareDirs(array $entry): array |
|
| 156 | { |
||
| 157 | /** @psalm-var MigrationDirs */ |
||
| 158 | 7 | $dirs = []; |
|
| 159 | |||
| 160 | // Add sql scripts for the current pdo driver. |
||
| 161 | // Should be the first in the list as they |
||
| 162 | // may have platform specific queries. |
||
| 163 | 7 | if (array_key_exists($this->driver, $entry)) { |
|
| 164 | 7 | $dirs[] = $this->preparePath($entry[$this->driver]); |
|
| 165 | } |
||
| 166 | |||
| 167 | // Add sql scripts for all platforms |
||
| 168 | 7 | if (array_key_exists('all', $entry)) { |
|
| 169 | 6 | $dirs[] = $this->preparePath($entry['all']); |
|
| 170 | } |
||
| 171 | |||
| 172 | 7 | return $dirs; |
|
| 173 | } |
||
| 174 | |||
| 175 | /** |
||
| 176 | * Adds the sql script paths from configuration. |
||
| 177 | * |
||
| 178 | * Script paths are ordered last in first out (LIFO). |
||
| 179 | * Which means the last path added is the first one searched |
||
| 180 | * for a SQL script. |
||
| 181 | * |
||
| 182 | * @psalm-param SqlConfig $sql |
||
| 183 | * |
||
| 184 | * @psalm-return MigrationDirs |
||
| 185 | */ |
||
| 186 | 83 | protected function readDirs(string|array $sql): array |
|
| 187 | { |
||
| 188 | 83 | if (is_string($sql)) { |
|
|
0 ignored issues
–
show
introduced
by
Loading history...
|
|||
| 189 | /** @psalm-var MigrationDirs */ |
||
| 190 | 80 | return [$this->preparePath($sql)]; |
|
| 191 | } |
||
| 192 | |||
| 193 | 16 | if (Util::isAssoc($sql)) { |
|
| 194 | /** @psalm-var SqlAssoc $sql */ |
||
| 195 | 2 | return $this->prepareDirs($sql); |
|
| 196 | } |
||
| 197 | |||
| 198 | /** @psalm-var MigrationDirs */ |
||
| 199 | 16 | $dirs = []; |
|
| 200 | |||
| 201 | 16 | foreach ($sql as $entry) { |
|
| 202 | 7 | if (is_string($entry)) { |
|
| 203 | 7 | array_unshift($dirs, $this->preparePath($entry)); |
|
| 204 | |||
| 205 | 7 | continue; |
|
| 206 | } |
||
| 207 | |||
| 208 | 6 | if (Util::isAssoc($entry)) { |
|
| 209 | 5 | $dirs = array_merge($this->prepareDirs($entry), $dirs); |
|
| 210 | |||
| 211 | 5 | continue; |
|
| 212 | } |
||
| 213 | |||
| 214 | 1 | throw new ValueError( |
|
| 215 | 1 | "A single 'sql' item must be either a string or an associative array" |
|
| 216 | 1 | ); |
|
| 217 | } |
||
| 218 | |||
| 219 | 15 | return $dirs; |
|
| 220 | } |
||
| 221 | |||
| 222 | 41 | protected function getColumnName(string $column): string |
|
| 223 | { |
||
| 224 | 41 | if (preg_match('/^[a-zA-Z0-9_]+$/', $column)) { |
|
| 225 | 39 | return $column; |
|
| 226 | } |
||
| 227 | |||
| 228 | 2 | throw new ValueError('Invalid migrations table column name: ' . $column); |
|
| 229 | } |
||
| 230 | } |
||
| 231 |