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@timshomepage.net> |
11 | * @copyright 2012 - 2022 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 | namespace Query; |
17 | |
18 | use function regexInArray; |
19 | |
20 | use BadMethodCallException; |
21 | use PDO; |
22 | use PDOStatement; |
23 | use Query\Drivers\DriverInterface; |
24 | |
25 | /** |
26 | * @method affectedRows(): int |
27 | * @method beginTransaction(): bool |
28 | * @method commit(): bool |
29 | * @method errorCode(): string |
30 | * @method errorInfo(): array |
31 | * @method exec(string $statement): int |
32 | * @method getAttribute(int $attribute) |
33 | * @method getColumns(string $table): array | null |
34 | * @method getDbs(): array | null |
35 | * @method getFks(string $table): array | null |
36 | * @method getIndexes(string $table): array | null |
37 | * @method getLastQuery(): string |
38 | * @method getSchemas(): array | null |
39 | * @method getSequences(): array | null |
40 | * @method getSystemTables(): array | null |
41 | * @method getTables(): array |
42 | * @method getTypes(): array | null |
43 | * @method getUtil(): \Query\Drivers\AbstractUtil |
44 | * @method getVersion(): string |
45 | * @method getViews(): array | null |
46 | * @method inTransaction(): bool |
47 | * @method lastInsertId(string $name = NULL): string |
48 | * @method numRows(): int | null |
49 | * @method prepare(string $statement, array $driver_options = []): PDOStatement |
50 | * @method prepareExecute(string $sql, array $params): PDOStatement |
51 | * @method prepareQuery(string $sql, array $data): PDOStatement |
52 | * @method query(string $statement): PDOStatement |
53 | * @method quote(string $string, int $parameter_type = PDO::PARAM_STR): string |
54 | * @method rollback(): bool |
55 | * @method setAttribute(int $attribute, $value): bool |
56 | * @method setTablePrefix(string $prefix): void |
57 | * @method truncate(string $table): PDOStatement |
58 | */ |
59 | class QueryBuilderBase { |
60 | |
61 | /** |
62 | * Convenience property for connection management |
63 | */ |
64 | public string $connName = ''; |
65 | |
66 | /** |
67 | * List of queries executed |
68 | */ |
69 | public array $queries = [ |
70 | 'total_time' => 0 |
71 | ]; |
72 | |
73 | /** |
74 | * Whether to do only an explain on the query |
75 | */ |
76 | protected bool $explain = FALSE; |
77 | |
78 | /** |
79 | * Whether to return data from a modification query |
80 | */ |
81 | protected bool $returning = FALSE; |
82 | |
83 | /** |
84 | * Query Builder state |
85 | */ |
86 | protected State $state; |
87 | |
88 | // -------------------------------------------------------------------------- |
89 | // ! Methods |
90 | // -------------------------------------------------------------------------- |
91 | /** |
92 | * Constructor |
93 | */ |
94 | public function __construct(protected ?DriverInterface $driver, protected QueryParser $parser) |
95 | { |
96 | // Create new State object |
97 | $this->state = new State(); |
98 | } |
99 | |
100 | /** |
101 | * Destructor |
102 | * @codeCoverageIgnore |
103 | */ |
104 | public function __destruct() |
105 | { |
106 | $this->driver = NULL; |
107 | } |
108 | |
109 | /** |
110 | * Calls a function further down the inheritance chain. |
111 | * 'Implements' methods on the driver object |
112 | * |
113 | * @return mixed |
114 | * @throws BadMethodCallException |
115 | */ |
116 | public function __call(string $name, array $params) |
117 | { |
118 | if (method_exists($this->driver, $name)) |
119 | { |
120 | return $this->driver->$name(...$params); |
121 | } |
122 | |
123 | throw new BadMethodCallException('Method does not exist'); |
124 | } |
125 | |
126 | /** |
127 | * Clear out the class variables, so the next query can be run |
128 | */ |
129 | public function resetQuery(): void |
130 | { |
131 | $this->state = new State(); |
132 | $this->explain = FALSE; |
133 | $this->returning = FALSE; |
134 | } |
135 | |
136 | /** |
137 | * Method to simplify select_ methods |
138 | */ |
139 | protected function _select(string $field, bool|string $as = FALSE): string |
140 | { |
141 | // Escape the identifiers |
142 | $field = $this->driver->quoteIdent($field); |
143 | |
144 | if ( ! \is_string($as)) |
145 | { |
146 | // @codeCoverageIgnoreStart |
147 | return $field; |
148 | // @codeCoverageIgnoreEnd |
149 | } |
150 | |
151 | $as = $this->driver->quoteIdent($as); |
152 | return "({$field}) AS {$as} "; |
153 | } |
154 | |
155 | /** |
156 | * Helper function for returning sql strings |
157 | */ |
158 | protected function _getCompile(QueryType $type, string $table, bool $reset): string |
159 | { |
160 | $sql = $this->_compile($type, $table); |
161 | |
162 | // Reset the query builder for the next query |
163 | if ($reset) |
164 | { |
165 | $this->resetQuery(); |
166 | } |
167 | |
168 | return $sql; |
169 | } |
170 | |
171 | /** |
172 | * Simplify 'like' methods |
173 | */ |
174 | protected function _like(string $field, mixed $val, LikeType $pos, string $like = 'LIKE', string $conj = 'AND'): self |
175 | { |
176 | $field = $this->driver->quoteIdent($field); |
177 | |
178 | // Add the like string into the order map |
179 | $like = $field . " {$like} ?"; |
180 | |
181 | if ($pos === LikeType::BEFORE) |
182 | { |
183 | $val = "%{$val}"; |
184 | } |
185 | elseif ($pos === LikeType::AFTER) |
186 | { |
187 | $val = "{$val}%"; |
188 | } |
189 | else |
190 | { |
191 | $val = "%{$val}%"; |
192 | } |
193 | |
194 | $conj = empty($this->state->getQueryMap()) ? ' WHERE ' : " {$conj} "; |
195 | $this->state->appendMap($conj, $like, MapType::LIKE); |
196 | |
197 | // Add to the values array |
198 | $this->state->appendWhereValues($val); |
199 | |
200 | return $this; |
201 | } |
202 | |
203 | /** |
204 | * Simplify building having clauses |
205 | */ |
206 | protected function _having(mixed $key, mixed $values = [], string $conj = 'AND'): self |
207 | { |
208 | $where = $this->_where($key, $values); |
209 | |
210 | // Create key/value placeholders |
211 | foreach ($where as $f => $val) |
212 | { |
213 | // Split each key by spaces, in case there |
214 | // is an operator such as >, <, !=, etc. |
215 | $fArray = explode(' ', trim($f)); |
216 | |
217 | $item = $this->driver->quoteIdent($fArray[0]); |
218 | |
219 | // Simple key value, or an operator |
220 | $item .= (count($fArray) === 1) ? '=?' : " {$fArray[1]} ?"; |
221 | |
222 | // Put in the having map |
223 | $this->state->appendHavingMap([ |
224 | 'conjunction' => empty($this->state->getHavingMap()) |
225 | ? ' HAVING ' |
226 | : " {$conj} ", |
227 | 'string' => $item |
228 | ]); |
229 | } |
230 | |
231 | return $this; |
232 | } |
233 | |
234 | /** |
235 | * Do all the redundant stuff for where/having type methods |
236 | */ |
237 | protected function _where(mixed $key, mixed $val = []): array |
238 | { |
239 | $where = []; |
240 | $pairs = []; |
241 | |
242 | if (is_scalar($key)) |
243 | { |
244 | $pairs[$key] = $val; |
245 | } else |
246 | { |
247 | $pairs = $key; |
248 | } |
249 | |
250 | foreach ($pairs as $k => $v) |
251 | { |
252 | $where[$k] = $v; |
253 | $this->state->appendWhereValues($v); |
254 | } |
255 | |
256 | return $where; |
257 | } |
258 | |
259 | /** |
260 | * Simplify generating where string |
261 | */ |
262 | protected function _whereString(mixed $key, mixed $values = [], string $defaultConj = 'AND'): self |
263 | { |
264 | // Create key/value placeholders |
265 | foreach ($this->_where($key, $values) as $f => $val) |
266 | { |
267 | $queryMap = $this->state->getQueryMap(); |
268 | |
269 | // Split each key by spaces, in case there |
270 | // is an operator such as >, <, !=, etc. |
271 | $fArray = explode(' ', trim($f)); |
272 | |
273 | $item = $this->driver->quoteIdent($fArray[0]); |
274 | |
275 | // Simple key value, or an operator |
276 | $item .= (count($fArray) === 1) ? '=?' : " {$fArray[1]} ?"; |
277 | $lastItem = end($queryMap); |
278 | |
279 | // Determine the correct conjunction |
280 | $conjunctionList = array_column($queryMap, 'conjunction'); |
281 | if (empty($queryMap) || ( ! regexInArray($conjunctionList, "/^ ?\n?WHERE/i"))) |
282 | { |
283 | $conj = "\nWHERE "; |
284 | } |
285 | elseif ($lastItem['type'] === MapType::GROUP_START) |
286 | { |
287 | $conj = ''; |
288 | } |
289 | else |
290 | { |
291 | $conj = " {$defaultConj} "; |
292 | } |
293 | |
294 | $this->state->appendMap($conj, $item, MapType::WHERE); |
295 | } |
296 | |
297 | return $this; |
298 | } |
299 | |
300 | /** |
301 | * Simplify where_in methods |
302 | * |
303 | * @param mixed $key |
304 | * @param mixed $val |
305 | * @param string $in - The (not) in fragment |
306 | * @param string $conj - The where in conjunction |
307 | */ |
308 | protected function _whereIn(mixed $key, mixed $val = [], string $in = 'IN', string $conj = 'AND'): self |
309 | { |
310 | $key = $this->driver->quoteIdent($key); |
311 | $params = array_fill(0, is_countable($val) ? count($val) : 0, '?'); |
312 | $this->state->appendWhereValues($val); |
313 | |
314 | $conjunction = empty($this->state->getQueryMap()) ? ' WHERE ' : " {$conj} "; |
315 | $str = $key . " {$in} (" . implode(',', $params) . ') '; |
316 | |
317 | $this->state->appendMap($conjunction, $str, MapType::WHERE_IN); |
318 | |
319 | return $this; |
320 | } |
321 | |
322 | /** |
323 | * Executes the compiled query |
324 | * |
325 | * @param array|null $vals |
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[] = [ |