Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
83 / 83
98.39% covered (success)
98.39%
61 / 62
72.00% covered (warning)
72.00%
36 / 50
95.65% covered (success)
95.65%
22 / 23
CRAP
0.00% covered (danger)
0.00%
0 / 1
Database
100.00% covered (success)
100.00%
83 / 83
98.39% covered (success)
98.39%
61 / 62
72.00% covered (warning)
72.00%
36 / 50
100.00% covered (success)
100.00%
23 / 23
80.72
100.00% covered (success)
100.00%
1 / 1
 __construct
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
 __get
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
8 / 8
25.00% covered (danger)
25.00%
2 / 8
100.00% covered (success)
100.00%
1 / 1
10.75
 getFetchMode
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
 connected
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
 getPdoDriver
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
 getSqlDirs
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
 loadScript
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
12 / 12
60.00% covered (warning)
60.00%
6 / 10
100.00% covered (success)
100.00%
1 / 1
8.30
 placeholderCompiler
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
 compilePlaceholders
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
 connect
100.00% covered (success)
100.00%
12 / 12
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
 disconnect
100.00% covered (success)
100.00%
7 / 7
83.33% covered (warning)
83.33%
5 / 6
50.00% covered (danger)
50.00%
2 / 4
100.00% covered (success)
100.00%
1 / 1
6.00
 reconnect
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
 ping
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 reset
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
5 / 5
66.67% covered (warning)
66.67%
2 / 3
100.00% covered (success)
100.00%
1 / 1
3.33
 quote
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
 begin
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
 commit
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
 rollback
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
 getConn
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
 requirePdo
100.00% covered (success)
100.00%
5 / 5
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
 markConnected
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
 touchConnection
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
3 / 3
50.00% covered (danger)
50.00%
1 / 2
100.00% covered (success)
100.00%
1 / 1
2.50
 execute
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
1<?php
2
3declare(strict_types=1);
4
5namespace Celemas\Quma;
6
7use Closure;
8use PDO;
9use RuntimeException;
10use Throwable;
11
12/** @api */
13class Database
14{
15    public readonly bool $debug;
16    protected ?PDO $pdo = null;
17    protected ?int $connectedAt = null;
18    protected ?int $lastUsedAt = null;
19    /** @var array<string, LoadedScript> */
20    protected array $compiledScripts = [];
21
22    public function __construct(
23        protected readonly Connection $conn,
24    ) {
25        $this->debug = Debug::enabled();
26    }
27
28    public function __get(string $key): Folder
29    {
30        Util::assertPathSegment($key, 'SQL folder name');
31
32        $exists = false;
33
34        foreach ($this->conn->config->sql as $path) {
35            $exists = is_dir($path . DIRECTORY_SEPARATOR . $key);
36
37            if ($exists) {
38                break;
39            }
40        }
41
42        if (!$exists) {
43            throw new RuntimeException('The SQL folder does not exist: ' . $key);
44        }
45
46        return new Folder($this, $key);
47    }
48
49    public function getFetchMode(): int
50    {
51        return $this->conn->config->pdo->fetchMode;
52    }
53
54    public function connected(): bool
55    {
56        return $this->pdo !== null;
57    }
58
59    public function getPdoDriver(): string
60    {
61        return $this->conn->config->driver;
62    }
63
64    public function getSqlDirs(): array
65    {
66        return $this->conn->config->sql;
67    }
68
69    public function loadScript(string $path, bool $isTemplate): LoadedScript
70    {
71        $key = ($isTemplate ? 'tpql:' : 'sql:') . $path;
72
73        if (array_key_exists($key, $this->compiledScripts)) {
74            return $this->compiledScripts[$key];
75        }
76
77        if ($isTemplate) {
78            if (!is_readable($path)) {
79                throw new RuntimeException('Could not read SQL script: ' . $path);
80            }
81
82            $script = new LoadedScript($path, $path, compile: $this->placeholderCompiler());
83            $this->compiledScripts[$key] = $script;
84
85            return $script;
86        }
87
88        $source = file_get_contents($path);
89
90        if ($source === false) {
91            throw new RuntimeException('Could not read SQL script: ' . $path);
92        }
93
94        $compiled = $this->compilePlaceholders($source, $path);
95        $script = new LoadedScript($compiled, $path);
96        $this->compiledScripts[$key] = $script;
97
98        return $script;
99    }
100
101    /** @return Closure(string, string): string */
102    private function placeholderCompiler(): Closure
103    {
104        return $this->compilePlaceholders(...);
105    }
106
107    private function compilePlaceholders(string $source, string $path): string
108    {
109        return $this->conn->config->placeholders?->compileSql($source, $path) ?? $source;
110    }
111
112    public function connect(): static
113    {
114        if ($this->pdo !== null) {
115            return $this;
116        }
117
118        $conn = $this->conn;
119
120        $pdo = new PDO(
121            $conn->config->dsn,
122            $conn->config->pdo->username,
123            $conn->config->pdo->password,
124            $conn->config->pdo->effectiveOptions(),
125        );
126
127        $this->pdo = $pdo;
128        $this->markConnected();
129
130        return $this;
131    }
132
133    public function disconnect(): void
134    {
135        if ($this->pdo !== null) {
136            try {
137                if ($this->pdo->inTransaction()) {
138                    $this->pdo->rollBack();
139                }
140            } catch (Throwable) {
141                // @mago-expect lint:no-empty-catch-clause Rollback failures are intentionally ignored during teardown.
142            }
143        }
144
145        $this->pdo = null;
146        $this->connectedAt = null;
147        $this->lastUsedAt = null;
148    }
149
150    public function reconnect(): static
151    {
152        $this->disconnect();
153
154        return $this->connect();
155    }
156
157    public function ping(): bool
158    {
159        if ($this->pdo === null) {
160            return false;
161        }
162
163        try {
164            $stmt = $this->pdo->query('SELECT 1');
165
166            if ($stmt === false) {
167                return false;
168            }
169
170            $this->touchConnection();
171
172            return $stmt->fetchColumn() !== false;
173        } catch (Throwable) {
174            return false;
175        }
176    }
177
178    public function reset(): void
179    {
180        if ($this->pdo === null) {
181            return;
182        }
183
184        if ($this->pdo->inTransaction()) {
185            $this->pdo->rollBack();
186        }
187
188        $this->touchConnection();
189    }
190
191    public function quote(string $value): string
192    {
193        return $this->requirePdo()->quote($value);
194    }
195
196    public function begin(): bool
197    {
198        return $this->requirePdo()->beginTransaction();
199    }
200
201    public function commit(): bool
202    {
203        return $this->requirePdo()->commit();
204    }
205
206    public function rollback(): bool
207    {
208        return $this->requirePdo()->rollback();
209    }
210
211    public function getConn(): PDO
212    {
213        return $this->requirePdo();
214    }
215
216    protected function requirePdo(): PDO
217    {
218        $this->connect();
219
220        if ($this->pdo !== null) {
221            $this->touchConnection();
222
223            return $this->pdo;
224        }
225
226        throw new RuntimeException('Database connection not initialized');
227    }
228
229    protected function markConnected(): void
230    {
231        $now = time();
232        $this->connectedAt = $now;
233        $this->lastUsedAt = $now;
234    }
235
236    protected function touchConnection(): void
237    {
238        if ($this->pdo !== null) {
239            $this->lastUsedAt = time();
240        }
241    }
242
243    public function execute(string $query, mixed ...$args): Query
244    {
245        return new Query($this, $query, new Args($args), null);
246    }
247}