Connection::getColumnName()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 7
ccs 4
cts 4
cp 1
crap 2
rs 10
c 0
b 0
f 0
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
The condition is_string($sql) is always false.
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