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}

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.

Database->__construct
23        protected readonly Connection $conn,
24    ) {
25        $this->debug = Debug::enabled();
26    }
Database->__get
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) {
34        foreach ($this->conn->config->sql as $path) {
35            $exists = is_dir($path . DIRECTORY_SEPARATOR . $key);
36
37            if ($exists) {
38                break;
34        foreach ($this->conn->config->sql as $path) {
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);
46        return new Folder($this, $key);
47    }
Database->begin
198        return $this->requirePdo()->beginTransaction();
199    }
Database->commit
203        return $this->requirePdo()->commit();
204    }
Database->compilePlaceholders
107    private function compilePlaceholders(string $source, string $path): string
108    {
109        return $this->conn->config->placeholders?->compileSql($source, $path) ?? $source;
110    }
Database->connect
114        if ($this->pdo !== null) {
115            return $this;
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    }
Database->connected
56        return $this->pdo !== null;
57    }
Database->disconnect
135        if ($this->pdo !== null) {
136            try {
137                if ($this->pdo->inTransaction()) {
138                    $this->pdo->rollBack();
138                    $this->pdo->rollBack();
140            } catch (Throwable) {
145        $this->pdo = null;
146        $this->connectedAt = null;
147        $this->lastUsedAt = null;
148    }
Database->execute
243    public function execute(string $query, mixed ...$args): Query
244    {
245        return new Query($this, $query, new Args($args), null);
246    }
Database->getConn
213        return $this->requirePdo();
214    }
Database->getFetchMode
51        return $this->conn->config->pdo->fetchMode;
52    }
Database->getPdoDriver
61        return $this->conn->config->driver;
62    }
Database->getSqlDirs
66        return $this->conn->config->sql;
67    }
Database->loadScript
69    public function loadScript(string $path, bool $isTemplate): LoadedScript
70    {
71        $key = ($isTemplate ? 'tpql:' : 'sql:') . $path;
71        $key = ($isTemplate ? 'tpql:' : 'sql:') . $path;
71        $key = ($isTemplate ? 'tpql:' : 'sql:') . $path;
71        $key = ($isTemplate ? 'tpql:' : 'sql:') . $path;
72
73        if (array_key_exists($key, $this->compiledScripts)) {
74            return $this->compiledScripts[$key];
77        if ($isTemplate) {
78            if (!is_readable($path)) {
79                throw new RuntimeException('Could not read SQL script: ' . $path);
82            $script = new LoadedScript($path, $path, compile: $this->placeholderCompiler());
83            $this->compiledScripts[$key] = $script;
84
85            return $script;
88        $source = file_get_contents($path);
89
90        if ($source === false) {
91            throw new RuntimeException('Could not read SQL script: ' . $path);
94        $compiled = $this->compilePlaceholders($source, $path);
95        $script = new LoadedScript($compiled, $path);
96        $this->compiledScripts[$key] = $script;
97
98        return $script;
99    }
Database->markConnected
231        $now = time();
232        $this->connectedAt = $now;
233        $this->lastUsedAt = $now;
234    }
Database->ping
159        if ($this->pdo === null) {
160            return false;
163        try {
164            $stmt = $this->pdo->query('SELECT 1');
165
166            if ($stmt === false) {
167                return false;
170            $this->touchConnection();
171
172            return $stmt->fetchColumn() !== false;
173        } catch (Throwable) {
174            return false;
175        }
176    }
Database->placeholderCompiler
104        return $this->compilePlaceholders(...);
105    }
Database->quote
191    public function quote(string $value): string
192    {
193        return $this->requirePdo()->quote($value);
194    }
Database->reconnect
152        $this->disconnect();
153
154        return $this->connect();
155    }
Database->requirePdo
218        $this->connect();
219
220        if ($this->pdo !== null) {
221            $this->touchConnection();
222
223            return $this->pdo;
226        throw new RuntimeException('Database connection not initialized');
227    }
Database->reset
180        if ($this->pdo === null) {
181            return;
184        if ($this->pdo->inTransaction()) {
185            $this->pdo->rollBack();
186        }
187
188        $this->touchConnection();
188        $this->touchConnection();
189    }
Database->rollback
208        return $this->requirePdo()->rollback();
209    }
Database->touchConnection
238        if ($this->pdo !== null) {
239            $this->lastUsedAt = time();
240        }
241    }
241    }
compilePlaceholders
107    private function compilePlaceholders(string $source, string $path): string
108    {
109        return $this->conn->config->placeholders?->compileSql($source, $path) ?? $source;
110    }