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<int, 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    public ?string $dsn {
28        get => $this->config->get('db.dsn');
29    }
30
31    /** @var list<non-empty-string> */
32    public array $sql {
33        get => $this->sqlCache ??= self::strings($this->config->get('db.sql'));
34    }
35
36    /** @var list<non-empty-string> */
37    public array $migrations {
38        get => $this->migrationsCache ??= self::strings($this->config->get('db.migrations'));
39    }
40
41    /** @var array<non-empty-string, array<non-empty-string, string>> */
42    public array $placeholders {
43        get => $this->placeholdersCache ??= $this->validatedPlaceholders();
44    }
45
46    public bool $print {
47        get => $this->config->get('db.print');
48    }
49
50    public bool $reuse {
51        get => $this->config->get('db.reuse');
52    }
53
54    /** @var array<int, mixed> */
55    public array $options {
56        get => $this->optionsCache ??= $this->config->get('db.options');
57    }
58
59    public function table(string $name, ?string $driver = null): string
60    {
61        if (preg_match('/^[a-z_][a-z0-9_]*$/', $name) !== 1) {
62            throw new RuntimeException('Invalid table name.');
63        }
64
65        return $this->prefix($driver) . $name;
66    }
67
68    /** @return array<non-empty-string, array<non-empty-string, string>> */
69    private function validatedPlaceholders(): array
70    {
71        /** @var array<non-empty-string, array<non-empty-string, string>> $placeholders */
72        $placeholders = $this->config->get('db.placeholders');
73        $driver = $this->driver();
74        $prefix = $placeholders[$driver]['cms.prefix'] ?? null;
75
76        if ($prefix !== null) {
77            $this->assertValidPrefix($prefix, $driver);
78
79            return $this->withPostgresqlObjectPrefix($placeholders);
80        }
81
82        $prefix = $placeholders['all']['cms.prefix'] ?? null;
83
84        if ($prefix !== null) {
85            $this->assertValidPrefix($prefix, $driver);
86
87            return $this->withPostgresqlObjectPrefix($placeholders);
88        }
89
90        throw new RuntimeException('Invalid table prefix.');
91    }
92
93    /**
94     * @param array<non-empty-string, array<non-empty-string, string>> $placeholders
95     *
96     * @return array<non-empty-string, array<non-empty-string, string>>
97     */
98    private function withPostgresqlObjectPrefix(array $placeholders): array
99    {
100        if (array_key_exists('cms.obj', $placeholders['pgsql'] ?? [])) {
101            return $placeholders;
102        }
103
104        $prefix = $placeholders['pgsql']['cms.prefix'] ?? $placeholders['all']['cms.prefix'] ?? null;
105
106        if ($prefix === null) {
107            return $placeholders;
108        }
109
110        if (!is_string($prefix)) {
111            throw new RuntimeException('Invalid table prefix.');
112        }
113
114        $this->assertValidPrefix($prefix, 'pgsql');
115
116        $placeholders['pgsql']['cms.obj'] = str_ends_with($prefix, '.') ? '' : $prefix;
117
118        return $placeholders;
119    }
120
121    private function prefix(?string $driver = null): string
122    {
123        $placeholders = $this->placeholders;
124        $driver ??= $this->driver();
125        $prefix = $placeholders[$driver]['cms.prefix'] ?? $placeholders['all']['cms.prefix'] ?? null;
126
127        if (!is_string($prefix)) {
128            throw new RuntimeException('Invalid table prefix.');
129        }
130
131        $this->assertValidPrefix($prefix, $driver);
132
133        return $prefix;
134    }
135
136    private function assertValidPrefix(mixed $prefix, string $driver): void
137    {
138        if (is_string($prefix) && $this->validPrefix($prefix, $driver)) {
139            return;
140        }
141
142        throw new RuntimeException('Invalid table prefix.');
143    }
144
145    private function validPrefix(string $prefix, string $driver): bool
146    {
147        if ($driver === 'pgsql') {
148            return preg_match('/^(?:[a-z_][a-z0-9_]*[.]?)?$/', $prefix) === 1;
149        }
150
151        return preg_match('/^(?:[a-z_][a-z0-9_]*)?$/', $prefix) === 1;
152    }
153
154    private function driver(): string
155    {
156        $driver = strstr($this->dsn ?? '', ':', before_needle: true);
157
158        return $driver === false ? '' : $driver;
159    }
160
161    /** @return list<non-empty-string> */
162    private static function strings(mixed $value): array
163    {
164        if ($value === null) {
165            return [];
166        }
167
168        if (is_string($value)) {
169            $value = trim($value);
170
171            return $value === '' ? [] : [$value];
172        }
173
174        return array_values($value);
175    }
176}