Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.30% covered (success)
91.30%
42 / 46
66.67% covered (warning)
66.67%
6 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Database
91.30% covered (success)
91.30%
42 / 46
66.67% covered (warning)
66.67%
6 / 9
24.38
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 table
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 validatedPlaceholders
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 withPostgresqlObjectPrefix
80.00% covered (warning)
80.00%
8 / 10
0.00% covered (danger)
0.00%
0 / 1
5.20
 prefix
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
 assertValidPrefix
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 validPrefix
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 driver
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 strings
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Config;
6
7use Cosray\Exception\RuntimeException;
8
9final class Database
10{
11    /** @var list<non-empty-string>|null */
12    private ?array $sqlCache = null;
13
14    /** @var list<non-empty-string>|null */
15    private ?array $migrationsCache = null;
16
17    /** @var array<string, mixed>|null */
18    private ?array $optionsCache = null;
19
20    /** @var array<non-empty-string, array<non-empty-string, string>>|null */
21    private ?array $placeholdersCache = null;
22
23    public function __construct(
24        private readonly \Cosray\Config $config,
25    ) {}
26
27    /** @var ?non-empty-string */
28    public ?string $dsn {
29        get => $this->config->get('db.dsn');
30    }
31
32    /** @var list<non-empty-string> */
33    public array $sql {
34        get => $this->sqlCache ??= self::strings($this->config->get('db.sql'));
35    }
36
37    /** @var list<non-empty-string> */
38    public array $migrations {
39        get => $this->migrationsCache ??= self::strings($this->config->get('db.migrations'));
40    }
41
42    /** @var array<non-empty-string, array<non-empty-string, string>> */
43    public array $placeholders {
44        get => $this->placeholdersCache ??= $this->validatedPlaceholders();
45    }
46
47    public bool $print {
48        get => $this->config->get('db.print');
49    }
50
51    /** @var array<string, mixed> */
52    public array $options {
53        get => $this->optionsCache ??= $this->config->get('db.options');
54    }
55
56    public function table(string $name, ?string $driver = null): string
57    {
58        if (preg_match('/^[a-z_][a-z0-9_]*$/', $name) !== 1) {
59            throw new RuntimeException('Invalid table name.');
60        }
61
62        return $this->prefix($driver) . $name;
63    }
64
65    /** @return array<non-empty-string, array<non-empty-string, string>> */
66    private function validatedPlaceholders(): array
67    {
68        /** @var array<non-empty-string, array<non-empty-string, string>> $placeholders */
69        $placeholders = $this->config->get('db.placeholders');
70        $driver = $this->driver();
71        $prefix = $placeholders[$driver]['cms.prefix'] ?? null;
72
73        if ($prefix !== null) {
74            $this->assertValidPrefix($prefix, $driver);
75
76            return $this->withPostgresqlObjectPrefix($placeholders);
77        }
78
79        $prefix = $placeholders['all']['cms.prefix'] ?? null;
80
81        if ($prefix !== null) {
82            $this->assertValidPrefix($prefix, $driver);
83
84            return $this->withPostgresqlObjectPrefix($placeholders);
85        }
86
87        throw new RuntimeException('Invalid table prefix.');
88    }
89
90    /**
91     * @param array<non-empty-string, array<non-empty-string, string>> $placeholders
92     *
93     * @return array<non-empty-string, array<non-empty-string, string>>
94     */
95    private function withPostgresqlObjectPrefix(array $placeholders): array
96    {
97        if (array_key_exists('cms.obj', $placeholders['pgsql'] ?? [])) {
98            return $placeholders;
99        }
100
101        $prefix = $placeholders['pgsql']['cms.prefix'] ?? $placeholders['all']['cms.prefix'] ?? null;
102
103        if ($prefix === null) {
104            return $placeholders;
105        }
106
107        if (!is_string($prefix)) {
108            throw new RuntimeException('Invalid table prefix.');
109        }
110
111        $this->assertValidPrefix($prefix, 'pgsql');
112
113        $placeholders['pgsql']['cms.obj'] = str_ends_with($prefix, '.') ? '' : $prefix;
114
115        return $placeholders;
116    }
117
118    private function prefix(?string $driver = null): string
119    {
120        $placeholders = $this->placeholders;
121        $driver ??= $this->driver();
122        $prefix = $placeholders[$driver]['cms.prefix'] ?? $placeholders['all']['cms.prefix'] ?? null;
123
124        if (!is_string($prefix)) {
125            throw new RuntimeException('Invalid table prefix.');
126        }
127
128        $this->assertValidPrefix($prefix, $driver);
129
130        return $prefix;
131    }
132
133    private function assertValidPrefix(mixed $prefix, string $driver): void
134    {
135        if (is_string($prefix) && $this->validPrefix($prefix, $driver)) {
136            return;
137        }
138
139        throw new RuntimeException('Invalid table prefix.');
140    }
141
142    private function validPrefix(string $prefix, string $driver): bool
143    {
144        if ($driver === 'pgsql') {
145            return preg_match('/^(?:[a-z_][a-z0-9_]*[.]?)?$/', $prefix) === 1;
146        }
147
148        return preg_match('/^(?:[a-z_][a-z0-9_]*)?$/', $prefix) === 1;
149    }
150
151    private function driver(): string
152    {
153        $driver = strstr($this->dsn ?? '', ':', before_needle: true);
154
155        return $driver === false ? '' : $driver;
156    }
157
158    /** @return list<non-empty-string> */
159    private static function strings(mixed $value): array
160    {
161        if ($value === null) {
162            return [];
163        }
164
165        if (is_string($value)) {
166            $value = trim($value);
167
168            return $value === '' ? [] : [$value];
169        }
170
171        return array_values($value);
172    }
173}