Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
121 / 121
96.91% covered (success)
96.91%
94 / 97
68.57% covered (warning)
68.57%
48 / 70
92.31% covered (success)
92.31%
24 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
Config
100.00% covered (success)
100.00%
121 / 121
96.91% covered (success)
96.91%
94 / 97
68.57% covered (warning)
68.57%
48 / 70
100.00% covered (success)
100.00%
26 / 26
186.21
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setCredentials
100.00% covered (success)
100.00%
1 / 1
n/a
0 / 0
n/a
0 / 0
100.00% covered (success)
100.00%
1 / 1
1
 setPdoOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setPdoOption
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setFetchMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setPingAfterIdle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setMaxConnectionAge
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 seconds
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 setPlaceholders
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addSqlDirs
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setMigrations
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addMigrationDir
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 setMigrationNamespace
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
13 / 13
36.36% covered (danger)
36.36%
4 / 11
100.00% covered (success)
100.00%
1 / 1
15.28
 setMigrationsTable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setMigrationsColumnMigration
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setMigrationsColumnApplied
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 preparePath
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
5 / 5
50.00% covered (danger)
50.00%
2 / 4
100.00% covered (success)
100.00%
1 / 1
4.12
 readDriver
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 readFlatDirs
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
17 / 17
50.00% covered (danger)
50.00%
5 / 10
100.00% covered (success)
100.00%
1 / 1
16.00
 readAssocDirs
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
6 / 6
75.00% covered (warning)
75.00%
3 / 4
100.00% covered (success)
100.00%
1 / 1
3.14
 readDirsEntry
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
7
 readMigrationDirs
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
7 / 7
80.00% covered (warning)
80.00%
4 / 5
100.00% covered (success)
100.00%
1 / 1
4.13
 isDriverConfig
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 readNamespacedDirs
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
5
 getMigrationsTableName
100.00% covered (success)
100.00%
6 / 6
84.62% covered (warning)
84.62%
11 / 13
50.00% covered (danger)
50.00%
4 / 8
100.00% covered (success)
100.00%
1 / 1
6.00
 getColumnName
100.00% covered (success)
100.00%
3 / 3
83.33% covered (warning)
83.33%
5 / 6
50.00% covered (danger)
50.00%
2 / 4
100.00% covered (success)
100.00%
1 / 1
2.50
1<?php
2
3declare(strict_types=1);
4
5namespace Celema\Quma;
6
7use PDO;
8use RuntimeException;
9use ValueError;
10
11/**
12 * @api
13 *
14 * @psalm-type SqlDirs = list<non-empty-string>
15 * @psalm-type SqlAssoc = array<non-empty-string, non-empty-string|list<non-empty-string>>
16 * @psalm-type SqlMixed = list<non-empty-string|SqlAssoc>
17 * @psalm-type SqlConfig = non-empty-string|SqlAssoc|SqlMixed
18 * @psalm-type MigrationDirsFlat = list<non-empty-string>
19 * @psalm-type MigrationDirsNamespaced = array<non-empty-string, non-empty-string|list<non-empty-string>>
20 * @psalm-type MigrationDirs = MigrationDirsFlat|MigrationDirsNamespaced
21 * @psalm-type PlaceholderMap = array<non-empty-string, string>
22 * @psalm-type PlaceholderConfig = array<non-empty-string, PlaceholderMap>
23 */
24final class Config
25{
26    /** @var non-empty-string */
27    public readonly string $driver;
28
29    /** @psalm-var SqlDirs */
30    public private(set) array $sql;
31
32    /** @psalm-var MigrationDirs */
33    public private(set) array $migrations = [];
34
35    public private(set) PdoConfig $pdo;
36    public private(set) ?Placeholders $placeholders = null;
37
38    public private(set) string $migrationsTable = 'migrations';
39    public private(set) string $migrationsColumnMigration = 'migration';
40    public private(set) string $migrationsColumnApplied = 'applied';
41
42    /** Seconds a connection may be idle before it is pinged on reuse; 0 never pings. */
43    public private(set) int $pingAfterIdle = 60;
44
45    /** Seconds after which a connection is replaced on reuse; 0 keeps it. */
46    public private(set) int $maxConnectionAge = 0;
47
48    /** @psalm-param SqlConfig $sql */
49    public function __construct(
50        public readonly string $dsn,
51        string|array $sql,
52    ) {
53        $this->driver = $this->readDriver($this->dsn);
54        $this->sql = $this->readFlatDirs($sql);
55        $this->pdo = new PdoConfig();
56    }
57
58    public function setCredentials(
59        string $username,
60        #[\SensitiveParameter]
61        ?string $password = null,
62    ): void {
63        $this->pdo = $this->pdo->credentials($username, $password);
64    }
65
66    /** @param array<array-key, mixed> $options */
67    public function setPdoOptions(array $options): void
68    {
69        $this->pdo = $this->pdo->options($options);
70    }
71
72    public function setPdoOption(int $attribute, mixed $value): void
73    {
74        $this->pdo = $this->pdo->option($attribute, $value);
75    }
76
77    public function setFetchMode(int $fetchMode): void
78    {
79        $this->pdo = $this->pdo->fetch($fetchMode);
80    }
81
82    public function setPingAfterIdle(int $seconds): void
83    {
84        $this->pingAfterIdle = self::seconds($seconds, 'pingAfterIdle');
85    }
86
87    public function setMaxConnectionAge(int $seconds): void
88    {
89        $this->maxConnectionAge = self::seconds($seconds, 'maxConnectionAge');
90    }
91
92    private static function seconds(int $seconds, string $setting): int
93    {
94        if ($seconds < 0) {
95            throw new ValueError("{$setting} must be a number of seconds, 0 to disable it: {$seconds}");
96        }
97
98        return $seconds;
99    }
100
101    /** @psalm-param PlaceholderConfig $placeholders */
102    public function setPlaceholders(Delimiters $delimiters, array $placeholders): void
103    {
104        $this->placeholders = new Placeholders($this->driver, $placeholders, $delimiters);
105    }
106
107    /** @psalm-param SqlConfig $sql */
108    public function addSqlDirs(array|string $sql): void
109    {
110        $dirs = $this->readFlatDirs($sql);
111        $this->sql = array_merge($dirs, $this->sql);
112    }
113
114    /** @psalm-param SqlConfig $migrations */
115    public function setMigrations(array|string $migrations): void
116    {
117        $this->migrations = $this->readMigrationDirs($migrations);
118    }
119
120    /** @param non-empty-string $migrations */
121    public function addMigrationDir(string $migrations): void
122    {
123        if (!array_is_list($this->migrations)) {
124            throw new ValueError(
125                'Cannot add a flat migration directory when migrations are namespaced. Use migrationNamespace().',
126            );
127        }
128
129        $dirs = $this->readFlatDirs($migrations);
130        $this->migrations = array_merge($dirs, $this->migrations);
131    }
132
133    /** @psalm-param SqlConfig $dirs */
134    public function setMigrationNamespace(string $namespace, string|array $dirs): void
135    {
136        if ($namespace === '') {
137            throw new ValueError('Migration namespace must not be empty.');
138        }
139
140        if (array_is_list($this->migrations) && count($this->migrations) > 0) {
141            throw new ValueError(
142                'Cannot add a namespaced migration directory when migrations are configured as a flat list.',
143            );
144        }
145
146        /** @psalm-var MigrationDirsNamespaced $migrations */
147        $migrations = array_is_list($this->migrations) ? [] : $this->migrations;
148        $migrations[$namespace] = is_string($dirs)
149            ? $this->preparePath($dirs)
150            : $this->readDirsEntry($dirs);
151        $this->migrations = $migrations;
152    }
153
154    public function setMigrationsTable(string $table): void
155    {
156        $this->migrationsTable = $this->getMigrationsTableName($table);
157    }
158
159    public function setMigrationsColumnMigration(string $column): void
160    {
161        $this->migrationsColumnMigration = $this->getColumnName($column);
162    }
163
164    public function setMigrationsColumnApplied(string $column): void
165    {
166        $this->migrationsColumnApplied = $this->getColumnName($column);
167    }
168
169    /** @return non-empty-string */
170    private function preparePath(string $path): string
171    {
172        $result = realpath($path);
173
174        if ($result !== false && $result !== '') {
175            return $result;
176        }
177
178        throw new ValueError("Path does not exist: {$path}");
179    }
180
181    /** @return non-empty-string */
182    private function readDriver(string $dsn): string
183    {
184        $driver = explode(':', $dsn)[0];
185
186        if (in_array($driver, PDO::getAvailableDrivers(), strict: true)) {
187            assert($driver !== '', 'PDO driver name must not be empty.');
188
189            return $driver;
190        }
191
192        throw new RuntimeException('PDO driver not supported: ' . $driver);
193    }
194
195    /**
196     * Reads directories from configuration into a flat list.
197     *
198     * @psalm-param SqlConfig $config
199     *
200     * @return list<non-empty-string>
201     */
202    private function readFlatDirs(string|array $config): array
203    {
204        if (is_string($config)) {
205            return [$this->preparePath($config)];
206        }
207
208        if (Util::isAssoc($config)) {
209            return $this->readAssocDirs($config);
210        }
211
212        $dirs = [];
213
214        foreach ($config as $entry) {
215            if (is_string($entry)) {
216                array_unshift($dirs, $this->preparePath($entry));
217
218                continue;
219            }
220
221            if (array_is_list($entry)) {
222                foreach ($entry as $path) {
223                    if (!is_string($path)) {
224                        continue;
225                    }
226
227                    array_unshift($dirs, $this->preparePath($path));
228                }
229
230                continue;
231            }
232
233            $dirs = array_merge($this->readAssocDirs($entry), $dirs);
234        }
235
236        return $dirs;
237    }
238
239    /**
240     * Reads directories from an associative array config.
241     *
242     * @param array<array-key, mixed> $entry
243     *
244     * @return list<non-empty-string>
245     */
246    private function readAssocDirs(array $entry): array
247    {
248        $dirs = array_key_exists($this->driver, $entry) ? $this->readDirsEntry($entry[$this->driver]) : [];
249
250        if (array_key_exists('all', $entry)) {
251            $dirs = array_merge($dirs, $this->readDirsEntry($entry['all']));
252        }
253
254        return $dirs;
255    }
256
257    /** @return list<non-empty-string> */
258    private function readDirsEntry(mixed $entry): array
259    {
260        if (is_string($entry)) {
261            return [$this->preparePath($entry)];
262        }
263
264        if (!is_array($entry)) {
265            return [];
266        }
267
268        $dirs = [];
269
270        array_walk(
271            $entry,
272            function (mixed $value) use (&$dirs): void {
273                if (is_string($value)) {
274                    $dirs[] = $this->preparePath($value);
275
276                    return;
277                }
278
279                if (!is_array($value)) {
280                    return;
281                }
282
283                if (Util::isAssoc($value)) {
284                    $dirs = array_merge($dirs, $this->readAssocDirs($value));
285
286                    return;
287                }
288
289                array_walk(
290                    $value,
291                    function (mixed $path) use (&$dirs): void {
292                        if (is_string($path)) {
293                            $dirs[] = $this->preparePath($path);
294                        }
295                    },
296                );
297            },
298        );
299
300        return $dirs;
301    }
302
303    /**
304     * Reads migration directories from configuration.
305     *
306     * Migrations can be configured as:
307     * - A flat list of directories
308     * - A namespaced structure with string keys mapping to directories
309     *
310     * @psalm-param SqlConfig $config
311     *
312     * @psalm-return MigrationDirs
313     */
314    private function readMigrationDirs(string|array $config): array
315    {
316        if (is_string($config)) {
317            return [$this->preparePath($config)];
318        }
319
320        if (Util::isAssoc($config) && !$this->isDriverConfig($config)) {
321            return $this->readNamespacedDirs($config);
322        }
323
324        return $this->readFlatDirs($config);
325    }
326
327    /** @param array<array-key, mixed> $config */
328    private function isDriverConfig(array $config): bool
329    {
330        return array_key_exists($this->driver, $config) || array_key_exists('all', $config);
331    }
332
333    /**
334     * Reads namespaced migration directories.
335     *
336     * @param array<array-key, mixed> $config
337     *
338     * @psalm-return MigrationDirsNamespaced
339     */
340    private function readNamespacedDirs(array $config): array
341    {
342        $result = [];
343
344        array_walk(
345            $config,
346            function (mixed $dirs, int|string $namespace) use (&$result): void {
347                if (!is_string($namespace) || $namespace === '') {
348                    return;
349                }
350
351                if (is_string($dirs)) {
352                    $result[$namespace] = $this->preparePath($dirs);
353
354                    return;
355                }
356
357                if (!is_array($dirs)) {
358                    return;
359                }
360
361                $result[$namespace] = $this->readDirsEntry($dirs);
362            },
363        );
364
365        return $result;
366    }
367
368    private function getMigrationsTableName(string $table): string
369    {
370        if ($this->driver === 'pgsql') {
371            if (preg_match('/^([a-zA-Z0-9_]+\.)?[a-zA-Z0-9_]+$/', $table)) {
372                return $table;
373            }
374        } elseif (preg_match('/^[a-zA-Z0-9_]+$/', $table)) {
375            return $table;
376        }
377
378        throw new ValueError('Invalid migrations table name: ' . $table);
379    }
380
381    private function getColumnName(string $column): string
382    {
383        if (preg_match('/^[a-zA-Z0-9_]+$/', $column)) {
384            return $column;
385        }
386
387        throw new ValueError('Invalid migrations table column name: ' . $column);
388    }
389}

Branches

Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once. Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

Config->__construct
50        public readonly string $dsn,
51        string|array $sql,
52    ) {
53        $this->driver = $this->readDriver($this->dsn);
54        $this->sql = $this->readFlatDirs($sql);
55        $this->pdo = new PdoConfig();
56    }
Config->addMigrationDir
121    public function addMigrationDir(string $migrations): void
122    {
123        if (!array_is_list($this->migrations)) {
124            throw new ValueError(
125                'Cannot add a flat migration directory when migrations are namespaced. Use migrationNamespace().',
129        $dirs = $this->readFlatDirs($migrations);
130        $this->migrations = array_merge($dirs, $this->migrations);
131    }
Config->addSqlDirs
108    public function addSqlDirs(array|string $sql): void
109    {
110        $dirs = $this->readFlatDirs($sql);
111        $this->sql = array_merge($dirs, $this->sql);
112    }
Config->getColumnName
381    private function getColumnName(string $column): string
382    {
383        if (preg_match('/^[a-zA-Z0-9_]+$/', $column)) {
383        if (preg_match('/^[a-zA-Z0-9_]+$/', $column)) {
383        if (preg_match('/^[a-zA-Z0-9_]+$/', $column)) {
383        if (preg_match('/^[a-zA-Z0-9_]+$/', $column)) {
384            return $column;
387        throw new ValueError('Invalid migrations table column name: ' . $column);
388    }
Config->getMigrationsTableName
368    private function getMigrationsTableName(string $table): string
369    {
370        if ($this->driver === 'pgsql') {
371            if (preg_match('/^([a-zA-Z0-9_]+\.)?[a-zA-Z0-9_]+$/', $table)) {
371            if (preg_match('/^([a-zA-Z0-9_]+\.)?[a-zA-Z0-9_]+$/', $table)) {
371            if (preg_match('/^([a-zA-Z0-9_]+\.)?[a-zA-Z0-9_]+$/', $table)) {
371            if (preg_match('/^([a-zA-Z0-9_]+\.)?[a-zA-Z0-9_]+$/', $table)) {
372                return $table;
370        if ($this->driver === 'pgsql') {
374        } elseif (preg_match('/^[a-zA-Z0-9_]+$/', $table)) {
374        } elseif (preg_match('/^[a-zA-Z0-9_]+$/', $table)) {
374        } elseif (preg_match('/^[a-zA-Z0-9_]+$/', $table)) {
374        } elseif (preg_match('/^[a-zA-Z0-9_]+$/', $table)) {
375            return $table;
378        throw new ValueError('Invalid migrations table name: ' . $table);
379    }
Config->isDriverConfig
328    private function isDriverConfig(array $config): bool
329    {
330        return array_key_exists($this->driver, $config) || array_key_exists('all', $config);
330        return array_key_exists($this->driver, $config) || array_key_exists('all', $config);
330        return array_key_exists($this->driver, $config) || array_key_exists('all', $config);
331    }
Config->preparePath
170    private function preparePath(string $path): string
171    {
172        $result = realpath($path);
173
174        if ($result !== false && $result !== '') {
174        if ($result !== false && $result !== '') {
174        if ($result !== false && $result !== '') {
175            return $result;
178        throw new ValueError("Path does not exist: {$path}");
179    }
Config->readAssocDirs
246    private function readAssocDirs(array $entry): array
247    {
248        $dirs = array_key_exists($this->driver, $entry) ? $this->readDirsEntry($entry[$this->driver]) : [];
248        $dirs = array_key_exists($this->driver, $entry) ? $this->readDirsEntry($entry[$this->driver]) : [];
248        $dirs = array_key_exists($this->driver, $entry) ? $this->readDirsEntry($entry[$this->driver]) : [];
248        $dirs = array_key_exists($this->driver, $entry) ? $this->readDirsEntry($entry[$this->driver]) : [];
249
250        if (array_key_exists('all', $entry)) {
251            $dirs = array_merge($dirs, $this->readDirsEntry($entry['all']));
252        }
253
254        return $dirs;
254        return $dirs;
255    }
Config->readDirsEntry
258    private function readDirsEntry(mixed $entry): array
259    {
260        if (is_string($entry)) {
261            return [$this->preparePath($entry)];
264        if (!is_array($entry)) {
265            return [];
268        $dirs = [];
269
270        array_walk(
271            $entry,
272            function (mixed $value) use (&$dirs): void {
273                if (is_string($value)) {
274                    $dirs[] = $this->preparePath($value);
275
276                    return;
277                }
278
279                if (!is_array($value)) {
280                    return;
281                }
282
283                if (Util::isAssoc($value)) {
284                    $dirs = array_merge($dirs, $this->readAssocDirs($value));
285
286                    return;
287                }
288
289                array_walk(
290                    $value,
291                    function (mixed $path) use (&$dirs): void {
292                        if (is_string($path)) {
293                            $dirs[] = $this->preparePath($path);
294                        }
295                    },
296                );
297            },
298        );
299
300        return $dirs;
301    }
Config->readDriver
182    private function readDriver(string $dsn): string
183    {
184        $driver = explode(':', $dsn)[0];
185
186        if (in_array($driver, PDO::getAvailableDrivers(), strict: true)) {
187            assert($driver !== '', 'PDO driver name must not be empty.');
188
189            return $driver;
192        throw new RuntimeException('PDO driver not supported: ' . $driver);
193    }
Config->readFlatDirs
202    private function readFlatDirs(string|array $config): array
203    {
204        if (is_string($config)) {
205            return [$this->preparePath($config)];
208        if (Util::isAssoc($config)) {
209            return $this->readAssocDirs($config);
212        $dirs = [];
213
214        foreach ($config as $entry) {
214        foreach ($config as $entry) {
215            if (is_string($entry)) {
216                array_unshift($dirs, $this->preparePath($entry));
217
218                continue;
221            if (array_is_list($entry)) {
222                foreach ($entry as $path) {
222                foreach ($entry as $path) {
223                    if (!is_string($path)) {
224                        continue;
222                foreach ($entry as $path) {
223                    if (!is_string($path)) {
224                        continue;
225                    }
226
227                    array_unshift($dirs, $this->preparePath($path));
222                foreach ($entry as $path) {
223                    if (!is_string($path)) {
224                        continue;
225                    }
226
227                    array_unshift($dirs, $this->preparePath($path));
228                }
229
230                continue;
214        foreach ($config as $entry) {
215            if (is_string($entry)) {
216                array_unshift($dirs, $this->preparePath($entry));
217
218                continue;
219            }
220
221            if (array_is_list($entry)) {
222                foreach ($entry as $path) {
223                    if (!is_string($path)) {
224                        continue;
225                    }
226
227                    array_unshift($dirs, $this->preparePath($path));
228                }
229
230                continue;
231            }
232
233            $dirs = array_merge($this->readAssocDirs($entry), $dirs);
214        foreach ($config as $entry) {
215            if (is_string($entry)) {
216                array_unshift($dirs, $this->preparePath($entry));
217
218                continue;
219            }
220
221            if (array_is_list($entry)) {
222                foreach ($entry as $path) {
223                    if (!is_string($path)) {
224                        continue;
225                    }
226
227                    array_unshift($dirs, $this->preparePath($path));
228                }
229
230                continue;
231            }
232
233            $dirs = array_merge($this->readAssocDirs($entry), $dirs);
234        }
235
236        return $dirs;
237    }
Config->readMigrationDirs
314    private function readMigrationDirs(string|array $config): array
315    {
316        if (is_string($config)) {
317            return [$this->preparePath($config)];
320        if (Util::isAssoc($config) && !$this->isDriverConfig($config)) {
320        if (Util::isAssoc($config) && !$this->isDriverConfig($config)) {
320        if (Util::isAssoc($config) && !$this->isDriverConfig($config)) {
321            return $this->readNamespacedDirs($config);
324        return $this->readFlatDirs($config);
325    }
Config->readNamespacedDirs
340    private function readNamespacedDirs(array $config): array
341    {
342        $result = [];
343
344        array_walk(
345            $config,
346            function (mixed $dirs, int|string $namespace) use (&$result): void {
347                if (!is_string($namespace) || $namespace === '') {
348                    return;
349                }
350
351                if (is_string($dirs)) {
352                    $result[$namespace] = $this->preparePath($dirs);
353
354                    return;
355                }
356
357                if (!is_array($dirs)) {
358                    return;
359                }
360
361                $result[$namespace] = $this->readDirsEntry($dirs);
362            },
363        );
364
365        return $result;
366    }
Config->seconds
92    private static function seconds(int $seconds, string $setting): int
93    {
94        if ($seconds < 0) {
95            throw new ValueError("{$setting} must be a number of seconds, 0 to disable it: {$seconds}");
98        return $seconds;
99    }
Config->setFetchMode
77    public function setFetchMode(int $fetchMode): void
78    {
79        $this->pdo = $this->pdo->fetch($fetchMode);
80    }
Config->setMaxConnectionAge
87    public function setMaxConnectionAge(int $seconds): void
88    {
89        $this->maxConnectionAge = self::seconds($seconds, 'maxConnectionAge');
90    }
Config->setMigrationNamespace
134    public function setMigrationNamespace(string $namespace, string|array $dirs): void
135    {
136        if ($namespace === '') {
137            throw new ValueError('Migration namespace must not be empty.');
140        if (array_is_list($this->migrations) && count($this->migrations) > 0) {
140        if (array_is_list($this->migrations) && count($this->migrations) > 0) {
140        if (array_is_list($this->migrations) && count($this->migrations) > 0) {
141            throw new ValueError(
142                'Cannot add a namespaced migration directory when migrations are configured as a flat list.',
147        $migrations = array_is_list($this->migrations) ? [] : $this->migrations;
147        $migrations = array_is_list($this->migrations) ? [] : $this->migrations;
147        $migrations = array_is_list($this->migrations) ? [] : $this->migrations;
147        $migrations = array_is_list($this->migrations) ? [] : $this->migrations;
148        $migrations[$namespace] = is_string($dirs)
149            ? $this->preparePath($dirs)
148        $migrations[$namespace] = is_string($dirs)
149            ? $this->preparePath($dirs)
150            : $this->readDirsEntry($dirs);
148        $migrations[$namespace] = is_string($dirs)
149            ? $this->preparePath($dirs)
150            : $this->readDirsEntry($dirs);
151        $this->migrations = $migrations;
152    }
Config->setMigrations
115    public function setMigrations(array|string $migrations): void
116    {
117        $this->migrations = $this->readMigrationDirs($migrations);
118    }
Config->setMigrationsColumnApplied
164    public function setMigrationsColumnApplied(string $column): void
165    {
166        $this->migrationsColumnApplied = $this->getColumnName($column);
167    }
Config->setMigrationsColumnMigration
159    public function setMigrationsColumnMigration(string $column): void
160    {
161        $this->migrationsColumnMigration = $this->getColumnName($column);
162    }
Config->setMigrationsTable
154    public function setMigrationsTable(string $table): void
155    {
156        $this->migrationsTable = $this->getMigrationsTableName($table);
157    }
Config->setPdoOption
72    public function setPdoOption(int $attribute, mixed $value): void
73    {
74        $this->pdo = $this->pdo->option($attribute, $value);
75    }
Config->setPdoOptions
67    public function setPdoOptions(array $options): void
68    {
69        $this->pdo = $this->pdo->options($options);
70    }
Config->setPingAfterIdle
82    public function setPingAfterIdle(int $seconds): void
83    {
84        $this->pingAfterIdle = self::seconds($seconds, 'pingAfterIdle');
85    }
Config->setPlaceholders
102    public function setPlaceholders(Delimiters $delimiters, array $placeholders): void
103    {
104        $this->placeholders = new Placeholders($this->driver, $placeholders, $delimiters);
105    }
{closure:/workspace/celema/quma/src/Config.php:272-297}
272            function (mixed $value) use (&$dirs): void {
273                if (is_string($value)) {
274                    $dirs[] = $this->preparePath($value);
275
276                    return;
279                if (!is_array($value)) {
280                    return;
283                if (Util::isAssoc($value)) {
284                    $dirs = array_merge($dirs, $this->readAssocDirs($value));
285
286                    return;
289                array_walk(
290                    $value,
291                    function (mixed $path) use (&$dirs): void {
292                        if (is_string($path)) {
293                            $dirs[] = $this->preparePath($path);
294                        }
295                    },
296                );
297            },
{closure:/workspace/celema/quma/src/Config.php:291-295}
291                    function (mixed $path) use (&$dirs): void {
292                        if (is_string($path)) {
293                            $dirs[] = $this->preparePath($path);
294                        }
295                    },
295                    },
{closure:/workspace/celema/quma/src/Config.php:346-362}
346            function (mixed $dirs, int|string $namespace) use (&$result): void {
347                if (!is_string($namespace) || $namespace === '') {
347                if (!is_string($namespace) || $namespace === '') {
347                if (!is_string($namespace) || $namespace === '') {
348                    return;
351                if (is_string($dirs)) {
352                    $result[$namespace] = $this->preparePath($dirs);
353
354                    return;
357                if (!is_array($dirs)) {
358                    return;
361                $result[$namespace] = $this->readDirsEntry($dirs);
362            },