Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
100.00% |
83 / 83 |
|
98.39% |
61 / 62 |
|
72.00% |
36 / 50 |
|
95.65% |
22 / 23 |
CRAP | |
0.00% |
0 / 1 |
| Database | |
100.00% |
83 / 83 |
|
98.39% |
61 / 62 |
|
72.00% |
36 / 50 |
|
100.00% |
23 / 23 |
80.72 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| __get | |
100.00% |
9 / 9 |
|
100.00% |
8 / 8 |
|
25.00% |
2 / 8 |
|
100.00% |
1 / 1 |
10.75 | |||
| getFetchMode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| connected | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getPdoDriver | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getSqlDirs | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| loadScript | |
100.00% |
16 / 16 |
|
100.00% |
12 / 12 |
|
60.00% |
6 / 10 |
|
100.00% |
1 / 1 |
8.30 | |||
| placeholderCompiler | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| compilePlaceholders | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| connect | |
100.00% |
12 / 12 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| disconnect | |
100.00% |
7 / 7 |
|
83.33% |
5 / 6 |
|
50.00% |
2 / 4 |
|
100.00% |
1 / 1 |
6.00 | |||
| reconnect | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| ping | |
100.00% |
9 / 9 |
|
100.00% |
7 / 7 |
|
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
4 | |||
| reset | |
100.00% |
5 / 5 |
|
100.00% |
5 / 5 |
|
66.67% |
2 / 3 |
|
100.00% |
1 / 1 |
3.33 | |||
| quote | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| begin | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| commit | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| rollback | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getConn | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| requirePdo | |
100.00% |
5 / 5 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| markConnected | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| touchConnection | |
100.00% |
2 / 2 |
|
100.00% |
3 / 3 |
|
50.00% |
1 / 2 |
|
100.00% |
1 / 1 |
2.50 | |||
| execute | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Celemas\Quma; |
| 6 | |
| 7 | use Closure; |
| 8 | use PDO; |
| 9 | use RuntimeException; |
| 10 | use Throwable; |
| 11 | |
| 12 | /** @api */ |
| 13 | class 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 | } |
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.
| 23 | protected readonly Connection $conn, |
| 24 | ) { |
| 25 | $this->debug = Debug::enabled(); |
| 26 | } |
| 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 | } |
| 198 | return $this->requirePdo()->beginTransaction(); |
| 199 | } |
| 203 | return $this->requirePdo()->commit(); |
| 204 | } |
| 107 | private function compilePlaceholders(string $source, string $path): string |
| 108 | { |
| 109 | return $this->conn->config->placeholders?->compileSql($source, $path) ?? $source; |
| 110 | } |
| 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 | } |
| 56 | return $this->pdo !== null; |
| 57 | } |
| 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 | } |
| 243 | public function execute(string $query, mixed ...$args): Query |
| 244 | { |
| 245 | return new Query($this, $query, new Args($args), null); |
| 246 | } |
| 213 | return $this->requirePdo(); |
| 214 | } |
| 51 | return $this->conn->config->pdo->fetchMode; |
| 52 | } |
| 61 | return $this->conn->config->driver; |
| 62 | } |
| 66 | return $this->conn->config->sql; |
| 67 | } |
| 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 | } |
| 231 | $now = time(); |
| 232 | $this->connectedAt = $now; |
| 233 | $this->lastUsedAt = $now; |
| 234 | } |
| 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 | } |
| 104 | return $this->compilePlaceholders(...); |
| 105 | } |
| 191 | public function quote(string $value): string |
| 192 | { |
| 193 | return $this->requirePdo()->quote($value); |
| 194 | } |
| 152 | $this->disconnect(); |
| 153 | |
| 154 | return $this->connect(); |
| 155 | } |
| 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 | } |
| 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 | } |
| 208 | return $this->requirePdo()->rollback(); |
| 209 | } |
| 238 | if ($this->pdo !== null) { |
| 239 | $this->lastUsedAt = time(); |
| 240 | } |
| 241 | } |
| 241 | } |
| 107 | private function compilePlaceholders(string $source, string $path): string |
| 108 | { |
| 109 | return $this->conn->config->placeholders?->compileSql($source, $path) ?? $source; |
| 110 | } |