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
![]() |
|||
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 |