Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
99.24% |
130 / 131 |
|
92.86% |
13 / 14 |
CRAP | |
0.00% |
0 / 1 |
QueryBuilderBase | |
99.24% |
130 / 131 |
|
92.86% |
13 / 14 |
62 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
__destruct | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
__call | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
resetQuery | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
_select | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
_getCompile | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
_like | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
4 | |||
_having | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
4 | |||
_where | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
_whereString | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
6 | |||
_whereIn | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
_run | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
5 | |||
_appendQuery | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
4 | |||
_compileType | n/a |
0 / 0 |
n/a |
0 / 0 |
8 | |||||
_compile | |
95.24% |
20 / 21 |
|
0.00% |
0 / 1 |
6 | |||
_compileReturning | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
10 |
1 | <?php declare(strict_types=1); |
2 | /** |
3 | * Query |
4 | * |
5 | * SQL Query Builder / Database Abstraction Layer |
6 | * |
7 | * PHP version 8.1 |
8 | * |
9 | * @package Query |
10 | * @author Timothy J. Warren <tim@timshome.page> |
11 | * @copyright 2012 - 2023 Timothy J. Warren |
12 | * @license http://www.opensource.org/licenses/mit-license.html MIT License |
13 | * @link https://git.timshomepage.net/aviat/Query |
14 | * @version 4.0.0 |
15 | */ |
16 | |
17 | namespace Query; |
18 | |
19 | use BadMethodCallException; |
20 | |
21 | use PDO; |
22 | use PDOStatement; |
23 | use Query\Drivers\DriverInterface; |
24 | use function is_string; |
25 | use function regexInArray; |
26 | |
27 | /** |
28 | * @method affectedRows(): int |
29 | * @method beginTransaction(): bool |
30 | * @method commit(): bool |
31 | * @method errorCode(): string |
32 | * @method errorInfo(): array |
33 | * @method exec(string $statement): int |
34 | * @method getAttribute(int $attribute) |
35 | * @method getColumns(string $table): array | null |
36 | * @method getDbs(): array | null |
37 | * @method getFks(string $table): array | null |
38 | * @method getIndexes(string $table): array | null |
39 | * @method getLastQuery(): string |
40 | * @method getSchemas(): array | null |
41 | * @method getSequences(): array | null |
42 | * @method getSystemTables(): array | null |
43 | * @method getTables(): array |
44 | * @method getTypes(): array | null |
45 | * @method getUtil(): \Query\Drivers\AbstractUtil |
46 | * @method getVersion(): string |
47 | * @method getViews(): array | null |
48 | * @method inTransaction(): bool |
49 | * @method lastInsertId(string $name = NULL): string |
50 | * @method numRows(): int | null |
51 | * @method prepare(string $statement, array $driver_options = []): PDOStatement |
52 | * @method prepareExecute(string $sql, array $params): PDOStatement |
53 | * @method prepareQuery(string $sql, array $data): PDOStatement |
54 | * @method query(string $statement): PDOStatement |
55 | * @method quote(string $string, int $parameter_type = PDO::PARAM_STR): string |
56 | * @method rollback(): bool |
57 | * @method setAttribute(int $attribute, $value): bool |
58 | * @method setTablePrefix(string $prefix): void |
59 | * @method truncate(string $table): PDOStatement |
60 | */ |
61 | class QueryBuilderBase |
62 | { |
63 | /** |
64 | * Convenience property for connection management |
65 | */ |
66 | public string $connName = ''; |
67 | |
68 | /** |
69 | * List of queries executed |
70 | */ |
71 | public array $queries = [ |
72 | 'total_time' => 0, |
73 | ]; |
74 | |
75 | /** |
76 | * Whether to do only an explain on the query |
77 | */ |
78 | protected bool $explain = FALSE; |
79 | |
80 | /** |
81 | * Whether to return data from a modification query |
82 | */ |
83 | protected bool $returning = FALSE; |
84 | |
85 | /** |
86 | * Query Builder state |
87 | */ |
88 | protected State $state; |
89 | |
90 | // -------------------------------------------------------------------------- |
91 | // ! Methods |
92 | // -------------------------------------------------------------------------- |
93 | /** |
94 | * Constructor |
95 | */ |
96 | public function __construct(protected ?DriverInterface $driver, protected QueryParser $parser) |
97 | { |
98 | // Create new State object |
99 | $this->state = new State(); |
100 | } |
101 | |
102 | /** |
103 | * Destructor |
104 | * @codeCoverageIgnore |
105 | */ |
106 | public function __destruct() |
107 | { |
108 | $this->driver = NULL; |
109 | } |
110 | |
111 | /** |
112 | * Calls a function further down the inheritance chain. |
113 | * 'Implements' methods on the driver object |
114 | * |
115 | * @throws BadMethodCallException |
116 | * @return mixed |
117 | */ |
118 | public function __call(string $name, array $params) |
119 | { |
120 | if (method_exists($this->driver, $name)) |
121 | { |
122 | return $this->driver->$name(...$params); |
123 | } |
124 | |
125 | throw new BadMethodCallException('Method does not exist'); |
126 | } |
127 | |
128 | /** |
129 | * Clear out the class variables, so the next query can be run |
130 | */ |
131 | public function resetQuery(): void |
132 | { |
133 | $this->state = new State(); |
134 | $this->explain = FALSE; |
135 | $this->returning = FALSE; |
136 | } |
137 | |
138 | /** |
139 | * Method to simplify select_ methods |
140 | */ |
141 | protected function _select(string $field, bool|string $as = FALSE): string |
142 | { |
143 | // Escape the identifiers |
144 | $field = $this->driver->quoteIdent($field); |
145 | |
146 | if ( ! is_string($as)) |
147 | { |
148 | // @codeCoverageIgnoreStart |
149 | return $field; |
150 | // @codeCoverageIgnoreEnd |
151 | } |
152 | |
153 | $as = $this->driver->quoteIdent($as); |
154 | |
155 | return "({$field}) AS {$as} "; |
156 | } |
157 | |
158 | /** |
159 | * Helper function for returning sql strings |
160 | */ |
161 | protected function _getCompile(QueryType $type, string $table, bool $reset): string |
162 | { |
163 | $sql = $this->_compile($type, $table); |
164 | |
165 | // Reset the query builder for the next query |
166 | if ($reset) |
167 | { |
168 | $this->resetQuery(); |
169 | } |
170 | |
171 | return $sql; |
172 | } |
173 | |
174 | /** |
175 | * Simplify 'like' methods |
176 | */ |
177 | protected function _like(string $field, mixed $val, LikeType $pos, string $like = 'LIKE', string $conj = 'AND'): self |
178 | { |
179 | $field = $this->driver->quoteIdent($field); |
180 | |
181 | // Add the like string into the order map |
182 | $like = $field . " {$like} ?"; |
183 | |
184 | if ($pos === LikeType::BEFORE) |
185 | { |
186 | $val = "%{$val}"; |
187 | } |
188 | elseif ($pos === LikeType::AFTER) |
189 | { |
190 | $val = "{$val}%"; |
191 | } |
192 | else |
193 | { |
194 | $val = "%{$val}%"; |
195 | } |
196 | |
197 | $conj = empty($this->state->getQueryMap()) ? ' WHERE ' : " {$conj} "; |
198 | $this->state->appendMap($conj, $like, MapType::LIKE); |
199 | |
200 | // Add to the values array |
201 | $this->state->appendWhereValues($val); |
202 | |
203 | return $this; |
204 | } |
205 | |
206 | /** |
207 | * Simplify building having clauses |
208 | */ |
209 | protected function _having(mixed $key, mixed $values = [], string $conj = 'AND'): self |
210 | { |
211 | $where = $this->_where($key, $values); |
212 | |
213 | // Create key/value placeholders |
214 | foreach ($where as $f => $val) |
215 | { |
216 | // Split each key by spaces, in case there |
217 | // is an operator such as >, <, !=, etc. |
218 | $fArray = explode(' ', trim($f)); |
219 | |
220 | $item = $this->driver->quoteIdent($fArray[0]); |
221 | |
222 | // Simple key value, or an operator |
223 | $item .= (count($fArray) === 1) ? '=?' : " {$fArray[1]} ?"; |
224 | |
225 | // Put in the having map |
226 | $this->state->appendHavingMap([ |
227 | 'conjunction' => empty($this->state->getHavingMap()) |
228 | ? ' HAVING ' |
229 | : " {$conj} ", |
230 | 'string' => $item, |
231 | ]); |
232 | } |
233 | |
234 | return $this; |
235 | } |
236 | |
237 | /** |
238 | * Do all the redundant stuff for where/having type methods |
239 | */ |
240 | protected function _where(mixed $key, mixed $val = []): array |
241 | { |
242 | $where = []; |
243 | $pairs = []; |
244 | |
245 | if (is_scalar($key)) |
246 | { |
247 | $pairs[$key] = $val; |
248 | } |
249 | else |
250 | { |
251 | $pairs = $key; |
252 | } |
253 | |
254 | foreach ($pairs as $k => $v) |
255 | { |
256 | $where[$k] = $v; |
257 | $this->state->appendWhereValues($v); |
258 | } |
259 | |
260 | return $where; |
261 | } |
262 | |
263 | /** |
264 | * Simplify generating where string |
265 | */ |
266 | protected function _whereString(mixed $key, mixed $values = [], string $defaultConj = 'AND'): self |
267 | { |
268 | // Create key/value placeholders |
269 | foreach ($this->_where($key, $values) as $f => $val) |
270 | { |
271 | $queryMap = $this->state->getQueryMap(); |
272 | |
273 | // Split each key by spaces, in case there |
274 | // is an operator such as >, <, !=, etc. |
275 | $fArray = explode(' ', trim($f)); |
276 | |
277 | $item = $this->driver->quoteIdent($fArray[0]); |
278 | |
279 | // Simple key value, or an operator |
280 | $item .= (count($fArray) === 1) ? '=?' : " {$fArray[1]} ?"; |
281 | $lastItem = end($queryMap); |
282 | |
283 | // Determine the correct conjunction |
284 | $conjunctionList = array_column($queryMap, 'conjunction'); |
285 | if (empty($queryMap) || ( ! regexInArray($conjunctionList, "/^ ?\n?WHERE/i"))) |
286 | { |
287 | $conj = "\nWHERE "; |
288 | } |
289 | elseif ($lastItem['type'] === MapType::GROUP_START) |
290 | { |
291 | $conj = ''; |
292 | } |
293 | else |
294 | { |
295 | $conj = " {$defaultConj} "; |
296 | } |
297 | |
298 | $this->state->appendMap($conj, $item, MapType::WHERE); |
299 | } |
300 | |
301 | return $this; |
302 | } |
303 | |
304 | /** |
305 | * Simplify where_in methods |
306 | * |
307 | * @param string $in - The (not) in fragment |
308 | * @param string $conj - The where in conjunction |
309 | */ |
310 | protected function _whereIn(mixed $key, mixed $val = [], string $in = 'IN', string $conj = 'AND'): self |
311 | { |
312 | $key = $this->driver->quoteIdent($key); |
313 | $params = array_fill(0, is_countable($val) ? count($val) : 0, '?'); |
314 | $this->state->appendWhereValues($val); |
315 | |
316 | $conjunction = empty($this->state->getQueryMap()) ? ' WHERE ' : " {$conj} "; |
317 | $str = $key . " {$in} (" . implode(',', $params) . ') '; |
318 | |
319 | $this->state->appendMap($conjunction, $str, MapType::WHERE_IN); |
320 | |
321 | return $this; |
322 | } |
323 | |
324 | /** |
325 | * Executes the compiled query |
326 | */ |
327 | protected function _run(QueryType $type, string $table, ?string $sql = NULL, ?array $vals = NULL, bool $reset = TRUE): PDOStatement |
328 | { |
329 | if ($sql === NULL) |
330 | { |
331 | $sql = $this->_compile($type, $table); |
332 | } |
333 | |
334 | if ($vals === NULL) |
335 | { |
336 | $vals = array_merge($this->state->getValues(), $this->state->getWhereValues()); |
337 | } |
338 | |
339 | $startTime = microtime(TRUE); |
340 | |
341 | $res = empty($vals) |
342 | ? $this->driver->query($sql) |
343 | : $this->driver->prepareExecute($sql, $vals); |
344 | |
345 | $endTime = microtime(TRUE); |
346 | $totalTime = number_format($endTime - $startTime, 5); |
347 | |
348 | // Add this query to the list of executed queries |
349 | $this->_appendQuery($vals, $sql, (int) $totalTime); |
350 | |
351 | // Reset class state for next query |
352 | if ($reset) |
353 | { |
354 | $this->resetQuery(); |
355 | } |
356 | |
357 | return $res; |
358 | } |
359 | |
360 | /** |
361 | * Convert the prepared statement into readable sql |
362 | */ |
363 | protected function _appendQuery(array $values, string $sql, int $totalTime): void |
364 | { |
365 | $evals = is_array($values) ? $values : []; |
366 | $esql = str_replace('?', '%s', $sql); |
367 | |
368 | // Quote string values |
369 | foreach ($evals as &$v) |
370 | { |
371 | $v = (is_numeric($v)) |
372 | ? $v |
373 | : htmlentities($this->driver->quote($v), ENT_NOQUOTES, 'utf-8'); |
374 | } |
375 | unset($v); |
376 | |
377 | // Add the query onto the array of values to pass |
378 | // as arguments to sprintf |
379 | array_unshift($evals, $esql); |
380 | |
381 | // Add the interpreted query to the list of executed queries |
382 | $this->queries[] = [ |