Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
80.27% |
179 / 223 |
|
71.21% |
47 / 66 |
CRAP | |
0.00% |
0 / 1 |
Builder | |
80.27% |
179 / 223 |
|
71.21% |
47 / 66 |
236.33 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setConnection | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
select | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
selectAs | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
random | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
bind | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
toSql | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
3.04 | |||
resolveJoinDependencies | |
75.00% |
15 / 20 |
|
0.00% |
0 / 1 |
11.56 | |||
raw | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
isValidReadOnly | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
8 | |||
distinct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
from | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
fromAs | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
where | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
5 | |||
andWhere | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
orWhere | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
whereIn | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
whereNull | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
whereNotNull | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
groupBy | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
newest | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
oldest | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
orderBy | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
offset | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
limit | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
union | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
lock | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
lockUpdate | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
__toString | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
find | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
count | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
min | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
max | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
sum | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
avg | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
insert | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
into | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
values | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getValues | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
value | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
sets | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
set | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
update | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
delete | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
increment | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
decrement | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
join | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
leftJoin | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
leftOuterJoin | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
leftInnerJoin | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
rightJoin | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
rightOuterJoin | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
rightInnerJoin | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
outerJoin | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
innerJoin | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
crossJoin | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
fullJoin | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
fullOuterJoin | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
rollback | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
on | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
5 | |||
orOn | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
andOn | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
merge | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
getBindParamType | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 | |||
getPublicColumnName | |
66.67% |
6 / 9 |
|
0.00% |
0 / 1 |
5.93 |
1 | <?php |
2 | /** |
3 | * Jingga |
4 | * |
5 | * PHP Version 8.1 |
6 | * |
7 | * @package phpOMS\DataStorage\Database\Query |
8 | * @copyright Dennis Eichhorn |
9 | * @license OMS License 2.0 |
10 | * @version 1.0.0 |
11 | * @link https://jingga.app |
12 | */ |
13 | declare(strict_types=1); |
14 | |
15 | namespace phpOMS\DataStorage\Database\Query; |
16 | |
17 | use phpOMS\Algorithm\Graph\DependencyResolver; |
18 | use phpOMS\Contract\SerializableInterface; |
19 | use phpOMS\DataStorage\Database\BuilderAbstract; |
20 | use phpOMS\DataStorage\Database\Connection\ConnectionAbstract; |
21 | |
22 | /** |
23 | * Database query builder. |
24 | * |
25 | * @package phpOMS\DataStorage\Database\Query |
26 | * @license OMS License 2.0 |
27 | * @link https://jingga.app |
28 | * @since 1.0.0 |
29 | */ |
30 | class Builder extends BuilderAbstract |
31 | { |
32 | /** |
33 | * Log queries. |
34 | * |
35 | * @var bool |
36 | * @since 1.0.0 |
37 | */ |
38 | public static bool $log = false; |
39 | |
40 | /** |
41 | * Columns. |
42 | * |
43 | * @var array |
44 | * @since 1.0.0 |
45 | */ |
46 | public array $selects = []; |
47 | |
48 | /** |
49 | * Columns. |
50 | * |
51 | * @var array |
52 | * @since 1.0.0 |
53 | */ |
54 | public array $random; |
55 | |
56 | /** |
57 | * Columns. |
58 | * |
59 | * @var array |
60 | * @since 1.0.0 |
61 | */ |
62 | public array $updates = []; |
63 | |
64 | /** |
65 | * Stupid work around because value needs to be not null for it to work in Grammar. |
66 | * |
67 | * @var array |
68 | * @since 1.0.0 |
69 | */ |
70 | public array $deletes = [1]; |
71 | |
72 | /** |
73 | * Into. |
74 | * |
75 | * @var string |
76 | * @since 1.0.0 |
77 | */ |
78 | public string $into = ''; |
79 | |
80 | /** |
81 | * Into columns. |
82 | * |
83 | * @var array |
84 | * @since 1.0.0 |
85 | */ |
86 | public array $inserts = []; |
87 | |
88 | /** |
89 | * Into columns. |
90 | * |
91 | * @var array<int, mixed> |
92 | * @since 1.0.0 |
93 | */ |
94 | public array $values = []; |
95 | |
96 | /** |
97 | * Into columns. |
98 | * |
99 | * @var array |
100 | * @since 1.0.0 |
101 | */ |
102 | public array $sets = []; |
103 | |
104 | /** |
105 | * Distinct. |
106 | * |
107 | * @var bool |
108 | * @since 1.0.0 |
109 | */ |
110 | public bool $distinct = false; |
111 | |
112 | /** |
113 | * From. |
114 | * |
115 | * @var array |
116 | * @since 1.0.0 |
117 | */ |
118 | public array $from = []; |
119 | |
120 | /** |
121 | * Joins. |
122 | * |
123 | * @var array |
124 | * @since 1.0.0 |
125 | */ |
126 | public array $joins = []; |
127 | |
128 | /** |
129 | * Ons of joins. |
130 | * |
131 | * @var array |
132 | * @since 1.0.0 |
133 | */ |
134 | public array $ons = []; |
135 | |
136 | /** |
137 | * Where. |
138 | * |
139 | * @var array |
140 | * @since 1.0.0 |
141 | */ |
142 | public array $wheres = []; |
143 | |
144 | /** |
145 | * Group. |
146 | * |
147 | * @var string[]|self[] |
148 | * @since 1.0.0 |
149 | */ |
150 | public array $groups = []; |
151 | |
152 | /** |
153 | * Order. |
154 | * |
155 | * @var array |
156 | * @since 1.0.0 |
157 | */ |
158 | public array $orders = []; |
159 | |
160 | /** |
161 | * Limit. |
162 | * |
163 | * @var null|int |
164 | * @since 1.0.0 |
165 | */ |
166 | public ?int $limit = null; |
167 | |
168 | /** |
169 | * Offset. |
170 | * |
171 | * @var null|int |
172 | * @since 1.0.0 |
173 | */ |
174 | public ?int $offset = null; |
175 | |
176 | /** |
177 | * Binds. |
178 | * |
179 | * @var array |
180 | * @since 1.0.0 |
181 | */ |
182 | private array $binds = []; |
183 | |
184 | /** |
185 | * Union. |
186 | * |
187 | * @var array |
188 | * @since 1.0.0 |
189 | */ |
190 | public array $unions = []; |
191 | |
192 | /** |
193 | * Lock. |
194 | * |
195 | * @var bool |
196 | * @since 1.0.0 |
197 | */ |
198 | public bool $lock = false; |
199 | |
200 | /** |
201 | * Comparison OPERATORS. |
202 | * |
203 | * @var string[] |
204 | * @since 1.0.0 |
205 | */ |
206 | public const OPERATORS = [ |
207 | '=', |
208 | '<', |
209 | '>', |
210 | '<=', |
211 | '>=', |
212 | '<>', |
213 | '!=', |
214 | 'like', |
215 | 'like binary', |
216 | 'not like', |
217 | 'between', |
218 | 'ilike', |
219 | '&', |
220 | '|', |
221 | '^', |
222 | '<<', |
223 | '>>', |
224 | 'rlike', |
225 | 'regexp', |
226 | 'not regexp', |
227 | '~', |
228 | '~*', |
229 | '!~', |
230 | '!~*', |
231 | 'similar to', |
232 | 'not similar to', |
233 | 'in', |
234 | ]; |
235 | |
236 | /** |
237 | * Constructor. |
238 | * |
239 | * @param ConnectionAbstract $connection Database connection |
240 | * @param bool $readOnly Query is read only |
241 | * |
242 | * @since 1.0.0 |
243 | */ |
244 | public function __construct(ConnectionAbstract $connection, bool $readOnly = false) |
245 | { |
246 | $this->isReadOnly = $readOnly; |
247 | $this->setConnection($connection); |
248 | } |
249 | |
250 | /** |
251 | * Set connection for grammar. |
252 | * |
253 | * @param ConnectionAbstract $connection Database connection |
254 | * |
255 | * @return void |
256 | * |
257 | * @since 1.0.0 |
258 | */ |
259 | public function setConnection(ConnectionAbstract $connection) : void |
260 | { |
261 | $this->connection = $connection; |
262 | $this->grammar = $connection->getGrammar(); |
263 | } |
264 | |
265 | /** |
266 | * Select. |
267 | * |
268 | * @param mixed ...$columns Columns |
269 | * |
270 | * @return Builder |
271 | * |
272 | * @throws \InvalidArgumentException |
273 | * |
274 | * @since 1.0.0 |
275 | */ |
276 | public function select(mixed ...$columns) : self |
277 | { |
278 | $this->type = QueryType::SELECT; |
279 | |
280 | /** @var mixed[] $columns */ |
281 | /** @var mixed $column */ |
282 | foreach ($columns as $column) { |
283 | if (\is_string($column) || $column instanceof self) { |
284 | $this->selects[] = $column; |
285 | } else { |
286 | throw new \InvalidArgumentException(); |
287 | } |
288 | } |
289 | |
290 | return $this; |
291 | } |
292 | |
293 | /** |
294 | * Select with alias. |
295 | * |
296 | * @param mixed $column Column query |
297 | * @param string $alias Alias |
298 | * |
299 | * @return Builder |
300 | * |
301 | * @since 1.0.0 |
302 | */ |
303 | public function selectAs(mixed $column, string $alias) : self |
304 | { |
305 | $this->type = QueryType::SELECT; |
306 | $this->selects[$alias] = $column; |
307 | |
308 | return $this; |
309 | } |
310 | |
311 | /** |
312 | * Select. |
313 | * |
314 | * @param array ...$columns Columns |
315 | * |
316 | * @return Builder |
317 | * |
318 | * @since 1.0.0 |
319 | */ |
320 | public function random(mixed ...$columns) : self |
321 | { |
322 | $this->select(...$columns); |
323 | |
324 | $this->type = QueryType::RANDOM; |
325 | $this->random = &$this->selects; |
326 | |
327 | return $this; |
328 | } |
329 | |
330 | /** |
331 | * Bind parameter. |
332 | * |
333 | * @param string|array $binds Binds |
334 | * |
335 | * @return Builder |
336 | * |
337 | * @since 1.0.0 |
338 | */ |
339 | public function bind(string | array $binds) : self |
340 | { |
341 | if (\is_array($binds)) { |
342 | $this->binds += $binds; |
343 | } else { |
344 | $this->binds[] = $binds; |
345 | } |
346 | |
347 | return $this; |
348 | } |
349 | |
350 | /** |
351 | * {@inheritdoc} |
352 | */ |
353 | public function toSql() : string |
354 | { |
355 | if (!empty($this->joins)) { |
356 | $this->resolveJoinDependencies(); |
357 | } |
358 | |
359 | $query = $this->grammar->compileQuery($this); |
360 | |
361 | if (self::$log) { |
362 | \phpOMS\Log\FileLogger::getInstance()->debug($query); |
363 | } |
364 | |
365 | return $query; |
366 | } |
367 | |
368 | /** |
369 | * Resolves join dependencies |
370 | * |
371 | * @return void |
372 | * |
373 | * @since 1.0.0 |
374 | */ |
375 | private function resolveJoinDependencies() : void |
376 | { |
377 | // create dependencies |
378 | $dependencies = []; |
379 | foreach ($this->joins as $table => $_) { |
380 | $dependencies[$table] = []; |
381 | |
382 | foreach ($this->ons[$table] as $on) { |
383 | if (!\is_string($on)) { |
384 | continue; |
385 | } |
386 | |
387 | if (\stripos($on['column'], '.')) { |
388 | $dependencies[$table][] = \explode('.', $on['column'])[0]; |
389 | } |
390 | |
391 | if (\stripos($on['value'], '.')) { |
392 | $dependencies[$table][] = \explode('.', $on['value'])[0]; |
393 | } |
394 | } |
395 | } |
396 | |
397 | // add from to existing dependencies |
398 | foreach ($this->from as $table => $from) { |
399 | $dependencies[$table] = []; |
400 | } |
401 | |
402 | $resolved = DependencyResolver::resolve($dependencies); |
403 | |
404 | // cyclomatic dependencies |
405 | if ($resolved === null) { |
406 | return; |
407 | } |
408 | |
409 | $temp = $this->joins; |
410 | $this->joins = []; |
411 | foreach ($resolved as $table) { |
412 | if (isset($temp[$table])) { |
413 | $this->joins[$table] = $temp[$table]; |
414 | } |
415 | } |
416 | } |
417 | |
418 | /** |
419 | * Set raw query. |
420 | * |
421 | * @param string $raw Raw query |
422 | * |
423 | * @return Builder |
424 | * |
425 | * @throws \Exception |
426 | * |
427 | * @since 1.0.0 |
428 | */ |
429 | public function raw(string $raw) : self |
430 | { |
431 | if (!$this->isValidReadOnly($raw)) { |
432 | throw new \Exception(); |
433 | } |
434 | |
435 | $this->type = QueryType::RAW; |
436 | $this->raw = \rtrim($raw, ';'); |
437 | |
438 | return $this; |
439 | } |
440 | |
441 | /** |
442 | * Tests if a string contains a non read only component in case the builder is read only. |
443 | * If the builder is not read only it will always return true |
444 | * |
445 | * @param string $raw Raw query |
446 | * |
447 | * @return bool |
448 | * |
449 | * @since 1.0.0 |
450 | */ |
451 | private function isValidReadOnly(string $raw) : bool |
452 | { |
453 | if (!$this->isReadOnly) { |
454 | return true; |
455 | } |
456 | |
457 | $raw = \strtolower($raw); |
458 | |
459 | if (\stripos($raw, 'insert') !== false |
460 | || \stripos($raw, 'update') !== false |
461 | || \stripos($raw, 'drop') !== false |
462 | || \stripos($raw, 'delete') !== false |
463 | || \stripos($raw, 'create') !== false |
464 | || \stripos($raw, 'alter') !== false |
465 | ) { |
466 | return false; |
467 | } |
468 | |
469 | return true; |
470 | } |
471 | |
472 | /** |
473 | * Is distinct. |
474 | * |
475 | * @return Builder |
476 | * |
477 | * @since 1.0.0 |
478 | */ |
479 | public function distinct() : self |
480 | { |
481 | $this->distinct = true; |
482 | |
483 | return $this; |
484 | } |
485 | |
486 | /** |
487 | * From. |
488 | * |
489 | * @param mixed ...$tables Tables |
490 | * |
491 | * @return Builder |
492 | * |
493 | * @throws \InvalidArgumentException |
494 | * |
495 | * @since 1.0.0 |
496 | */ |
497 | public function from(mixed ...$tables) : self |
498 | { |
499 | /** @var mixed[] $tables */ |
500 | /** @var mixed $table */ |
501 | foreach ($tables as $key => $table) { |
502 | if (\is_string($table) || $table instanceof self) { |
503 | $this->from[] = $table; |
504 | } else { |
505 | throw new \InvalidArgumentException(); |
506 | } |
507 | } |
508 | |
509 | return $this; |
510 | } |
511 | |
512 | /** |
513 | * From with alias. |
514 | * |
515 | * @param mixed $column Column query |
516 | * @param string $alias Alias |
517 | * |
518 | * @return Builder |
519 | * |
520 | * @since 1.0.0 |
521 | */ |
522 | public function fromAs(mixed $column, string $alias) : self |
523 | { |
524 | $this->from[$alias] = $column; |
525 | |
526 | return $this; |
527 | } |
528 | |
529 | /** |
530 | * Where. |
531 | * |
532 | * @param string|array|Where $columns Columns |
533 | * @param string|array $operator Operator |
534 | * @param mixed $values Values |
535 | * @param string|array $boolean Boolean condition |
536 | * |
537 | * @return Builder |
538 | * |
539 | * @throws \InvalidArgumentException |
540 | * |
541 | * @since 1.0.0 |
542 | */ |
543 | public function where(string | array | self $columns, string | array $operator = null, mixed $values = null, string | array $boolean = 'and') : self |
544 | { |
545 | if (!\is_array($columns)) { |
546 | $columns = [$columns]; |
547 | $operator = [$operator]; |
548 | $values = [$values]; |
549 | $boolean = [$boolean]; |
550 | } |
551 | |
552 | $i = 0; |
553 | foreach ($columns as $column) { |
554 | if (isset($operator[$i]) && !\in_array(\strtolower($operator[$i]), self::OPERATORS)) { |
555 | throw new \InvalidArgumentException('Unknown operator: "' . $operator[$i] . '"'); |
556 | } |
557 | |
558 | $this->wheres[self::getPublicColumnName($column)][] = [ |
559 | 'column' => $column, |
560 | 'operator' => $operator[$i], |
561 | 'value' => $values[$i], |
562 | 'boolean' => $boolean[$i], |
563 | ]; |
564 | |
565 | ++$i; |
566 | } |
567 | |
568 | return $this; |
569 | } |
570 | |
571 | /** |
572 | * Where and sub condition. |
573 | * |
574 | * @param string|array|Where $where Where sub condition |
575 | * @param string|array $operator Operator |
576 | * @param mixed $values Values |
577 | * |
578 | * @return Builder |
579 | * |
580 | * @since 1.0.0 |
581 | */ |
582 | public function andWhere(string | array | Where $where, string | array $operator = null, mixed $values = null) : self |
583 | { |
584 | return $this->where($where, $operator, $values, 'and'); |
585 | } |
586 | |
587 | /** |
588 | * Where or sub condition. |
589 | * |
590 | * @param string|array|Where $where Where sub condition |
591 | * @param string|array $operator Operator |
592 | * @param mixed $values Values |
593 | * |
594 | * @return Builder |
595 | * |
596 | * @since 1.0.0 |
597 | */ |
598 | public function orWhere(string | array | self $where, string | array $operator = null, mixed $values = null) : self |
599 | { |
600 | return $this->where($where, $operator, $values, 'or'); |
601 | } |
602 | |
603 | /** |
604 | * Where in. |
605 | * |
606 | * @param string|array|Where $column Column |
607 | * @param string|array $values Values |
608 | * @param string $boolean Boolean condition |
609 | * |
610 | * @return Builder |
611 | * |
612 | * @since 1.0.0 |
613 | */ |
614 | public function whereIn(string | array | Where $column, mixed $values = null, string $boolean = 'and') : self |
615 | { |
616 | $this->where($column, 'in', $values, $boolean); |
617 | |
618 | return $this; |
619 | } |
620 | |
621 | /** |
622 | * Where null. |
623 | * |
624 | * @param string|array|Where $column Column |
625 | * @param string $boolean Boolean condition |
626 | * |
627 | * @return Builder |
628 | * |
629 | * @since 1.0.0 |
630 | */ |
631 | public function whereNull(string | array | Where $column, string $boolean = 'and') : self |
632 | { |
633 | $this->where($column, '=', null, $boolean); |
634 | |
635 | return $this; |
636 | } |
637 | |
638 | /** |
639 | * Where not null. |
640 | * |
641 | * @param string|array|Where $column Column |
642 | * @param string $boolean Boolean condition |
643 | * |
644 | * @return Builder |
645 | * |
646 | * @since 1.0.0 |
647 | */ |
648 | public function whereNotNull(string | array | Where $column, string $boolean = 'and') : self |
649 | { |
650 | $this->where($column, '!=', null, $boolean); |
651 | |
652 | return $this; |
653 | } |
654 | |
655 | /** |
656 | * Group by. |
657 | * |
658 | * @param mixed ...$columns Grouping result |
659 | * |
660 | * @return Builder |
661 | * |
662 | * @throws \InvalidArgumentException |
663 | * |
664 | * @since 1.0.0 |
665 | */ |
666 | public function groupBy(mixed ...$columns) : self |
667 | { |
668 | /** @var mixed[] $columns */ |
669 | /** @var mixed $column */ |
670 | foreach ($columns as $column) { |
671 | if (\is_string($column) || $column instanceof self) { |
672 | $this->groups[] = $column; |
673 | } else { |
674 | throw new \InvalidArgumentException(); |
675 | } |
676 | } |
677 | |
678 | return $this; |
679 | } |
680 | |
681 | /** |
682 | * Order by newest. |
683 | * |
684 | * @param string $column Column |
685 | * |
686 | * @return Builder |
687 | * |
688 | * @since 1.0.0 |
689 | */ |
690 | public function newest(string $column) : self |
691 | { |
692 | $this->orderBy($column, OrderType::DESC); |
693 | |
694 | return $this; |
695 | } |
696 | |
697 | /** |
698 | * Order by oldest. |
699 | * |
700 | * @param string $column Column |
701 | * |
702 | * @return Builder |
703 | * |
704 | * @since 1.0.0 |
705 | */ |
706 | public function oldest(string $column) : self |
707 | { |
708 | $this->orderBy($column, OrderType::ASC); |
709 | |
710 | return $this; |
711 | } |
712 | |
713 | /** |
714 | * Order by. |
715 | * |
716 | * @param string|array $columns Columns |
717 | * @param string|string[] $order Orders |
718 | * |
719 | * @return Builder |
720 | * |
721 | * @since 1.0.0 |
722 | */ |
723 | public function orderBy(string | array $columns, string | array $order = OrderType::DESC) : self |
724 | { |
725 | if (\is_string($columns)) { |
726 | $columns = [$columns]; |
727 | } |
728 | |
729 | foreach ($columns as $key => $column) { |
730 | $tOrder = \is_string($order) ? $order : $order[$key]; |
731 | |
732 | $this->orders[$column] = $tOrder; |
733 | } |
734 | |
735 | return $this; |
736 | } |
737 | |
738 | /** |
739 | * Offset. |
740 | * |
741 | * @param int $offset Offset |
742 | * |
743 | * @return Builder |
744 | * |
745 | * @since 1.0.0 |
746 | */ |
747 | public function offset(int $offset) : self |
748 | { |
749 | $this->offset = $offset; |
750 | |
751 | return $this; |
752 | } |
753 | |
754 | /** |
755 | * Limit. |
756 | * |
757 | * @param int $limit Limit |
758 | * |
759 | * @return Builder |
760 | * |
761 | * @since 1.0.0 |
762 | */ |
763 | public function limit(int $limit) : self |
764 | { |
765 | $this->limit = $limit; |
766 | |
767 | return $this; |
768 | } |
769 | |
770 | /** |
771 | * Union. |
772 | * |
773 | * @param mixed $query Query |
774 | * |
775 | * @return Builder |
776 | * |
777 | * @since 1.0.0 |
778 | */ |
779 | public function union(mixed $query) : self |
780 | { |
781 | if (!\is_array($query)) { |
782 | $this->unions[] = $query; |
783 | } else { |
784 | $this->unions += $query; |
785 | } |
786 | |
787 | return $this; |
788 | } |
789 | |
790 | /** |
791 | * Lock query. |
792 | * |
793 | * @return void |
794 | * |
795 | * @since 1.0.0 |
796 | */ |
797 | public function lock() : void |
798 | { |
799 | } |
800 | |
801 | /** |
802 | * Lock for update query. |
803 | * |
804 | * @return void |
805 | * |
806 | * @since 1.0.0 |
807 | */ |
808 | public function lockUpdate() : void |
809 | { |
810 | } |
811 | |
812 | /** |
813 | * Create query string. |
814 | * |
815 | * @return string |
816 | * |
817 | * @since 1.0.0 |
818 | */ |
819 | public function __toString() |
820 | { |
821 | return $this->grammar->compileQuery($this); |
822 | } |
823 | |
824 | /** |
825 | * Find query. |
826 | * |
827 | * @return void |
828 | * |
829 | * @since 1.0.0 |
830 | */ |
831 | public function find() : void |
832 | { |
833 | } |
834 | |
835 | /** |
836 | * Count results. |
837 | * |
838 | * @param string $table Table to count the result set |
839 | * |
840 | * @return Builder |
841 | * |
842 | * @since 1.0.0 |
843 | */ |
844 | public function count(string $table = '*') : self |
845 | { |
846 | /** |
847 | * @todo |
848 | * Don't do this as a string, create a new object $this->select(new Count($table)). |
849 | * The parser should be able to handle this much better |
850 | */ |
851 | return $this->select('COUNT(' . $table . ')'); |
852 | } |
853 | |
854 | /** |
855 | * Select minimum. |
856 | * |
857 | * @return void |
858 | * |
859 | * @since 1.0.0 |
860 | */ |
861 | public function min() : void |
862 | { |
863 | } |
864 | |
865 | /** |
866 | * Select maximum. |
867 | * |
868 | * @return void |
869 | * |
870 | * @since 1.0.0 |
871 | */ |
872 | public function max() : void |
873 | { |
874 | } |
875 | |
876 | /** |
877 | * Select sum. |
878 | * |
879 | * @return void |
880 | * |
881 | * @since 1.0.0 |
882 | */ |
883 | public function sum() : void |
884 | { |
885 | } |
886 | |
887 | /** |
888 | * Select average. |
889 | * |
890 | * @return void |
891 | * |
892 | * @since 1.0.0 |
893 | */ |
894 | public function avg() : void |
895 | { |
896 | } |
897 | |
898 | /** |
899 | * Insert into columns. |
900 | * |
901 | * @param mixed ...$columns Columns |
902 | * |
903 | * @return Builder |
904 | * |
905 | * @throws \Exception |
906 | * |
907 | * @since 1.0.0 |
908 | */ |
909 | public function insert(mixed ...$columns) : self |
910 | { |
911 | if ($this->isReadOnly) { |
912 | throw new \Exception(); |
913 | } |
914 | |
915 | $this->type = QueryType::INSERT; |
916 | |
917 | foreach ($columns as $column) { |
918 | $this->inserts[] = $column; |
919 | } |
920 | |
921 | return $this; |
922 | } |
923 | |
924 | /** |
925 | * Table to insert into. |
926 | * |
927 | * @param string $table Table |
928 | * |
929 | * @return Builder |
930 | * |
931 | * @since 1.0.0 |
932 | */ |
933 | public function into(string $table) : self |
934 | { |
935 | $this->into = $table; |
936 | |
937 | return $this; |
938 | } |
939 | |
940 | /** |
941 | * Values to insert. |
942 | * |
943 | * @param mixed ...$values Values |
944 | * |
945 | * @return Builder |
946 | * |
947 | * @since 1.0.0 |
948 | */ |
949 | public function values(mixed ...$values) : self |
950 | { |
951 | $this->values[] = $values; |
952 | |
953 | return $this; |
954 | } |
955 | |
956 | /** |
957 | * Get insert values |
958 | * |
959 | * @return array |
960 | * |
961 | * @since 1.0.0 |
962 | */ |
963 | public function getValues() : array |
964 | { |
965 | return $this->values; |
966 | } |
967 | |
968 | /** |
969 | * Values to insert. |
970 | * |
971 | * @param mixed $value Values |
972 | * |
973 | * @return Builder |
974 | * |
975 | * @since 1.0.0 |
976 | */ |
977 | public function value(mixed $value) : self |
978 | { |
979 | \end($this->values); |
980 | |
981 | $key = \key($this->values); |
982 | $key ??= 0; |
983 | |
984 | if (\is_array($value)) { |
985 | $this->values[$key + 1] = $value; |
986 | } else { |
987 | $this->values[$key][] = $value; |
988 | } |
989 | |
990 | \reset($this->values); |
991 | |
992 | return $this; |
993 | } |
994 | |
995 | /** |
996 | * Values to insert. |
997 | * |
998 | * @param mixed ...$sets Values |
999 | * |
1000 | * @return Builder |
1001 | * |
1002 | * @since 1.0.0 |
1003 | */ |
1004 | public function sets(mixed ...$sets) : self |
1005 | { |
1006 | $this->sets[$sets[0]] = $sets[1] ?? null; |
1007 | |
1008 | return $this; |
1009 | } |
1010 | |
1011 | /** |
1012 | * Values to insert. |
1013 | * |
1014 | * @param mixed $set Values |
1015 | * |
1016 | * @return Builder |
1017 | * |
1018 | * @since 1.0.0 |
1019 | */ |
1020 | public function set(mixed $set) : self |
1021 | { |
1022 | $this->sets[\key($set)] = \current($set); |
1023 | |
1024 | return $this; |
1025 | } |
1026 | |
1027 | /** |
1028 | * Update columns. |
1029 | * |
1030 | * @param string $table Column names to update |
1031 | * |
1032 | * @return Builder |
1033 | * |
1034 | * @throws \Exception |
1035 | * @throws \InvalidArgumentException |
1036 | * |
1037 | * @since 1.0.0 |
1038 | */ |
1039 | public function update(string $table) : self |
1040 | { |
1041 | if ($this->isReadOnly) { |
1042 | throw new \Exception(); |
1043 | } |
1044 | |
1045 | $this->type = QueryType::UPDATE; |
1046 | $this->updates[] = $table; |
1047 | |
1048 | return $this; |
1049 | } |
1050 | |
1051 | /** |
1052 | * Delete query |
1053 | * |
1054 | * @return Builder |
1055 | * |
1056 | * @since 1.0.0 |
1057 | */ |
1058 | public function delete() : self |
1059 | { |
1060 | if ($this->isReadOnly) { |
1061 | throw new \Exception(); |
1062 | } |
1063 | |
1064 | $this->type = QueryType::DELETE; |
1065 | |
1066 | return $this; |
1067 | } |
1068 | |
1069 | /** |
1070 | * Increment value. |
1071 | * |
1072 | * @return void |
1073 | * |
1074 | * @since 1.0.0 |
1075 | */ |
1076 | public function increment() : void |
1077 | { |
1078 | } |
1079 | |
1080 | /** |
1081 | * Decrement value. |
1082 | * |
1083 | * @return void |
1084 | * |
1085 | * @since 1.0.0 |
1086 | */ |
1087 | public function decrement() : void |
1088 | { |
1089 | } |
1090 | |
1091 | /** |
1092 | * Join. |
1093 | * |
1094 | * @param string|self $table Join query |
1095 | * @param string $type Join type |
1096 | * @param null|string $alias Alias name (empty = none) |
1097 | * |
1098 | * @return Builder |
1099 | * |
1100 | * @since 1.0.0 |
1101 | */ |
1102 | public function join(string | self $table, string $type = JoinType::JOIN, string $alias = null) : self |
1103 | { |
1104 | $this->joins[$alias ?? $table] = ['type' => $type, 'table' => $table, 'alias' => $alias]; |
1105 | |
1106 | return $this; |
1107 | } |
1108 | |
1109 | /** |
1110 | * Join. |
1111 | * |
1112 | * @param string|self $table Join query |
1113 | * @param null|string $alias Alias name (empty = none) |
1114 | * |
1115 | * @return Builder |
1116 | * |
1117 | * @since 1.0.0 |
1118 | */ |
1119 | public function leftJoin(string | self $table, string $alias = null) : self |
1120 | { |
1121 | return $this->join($table, JoinType::LEFT_JOIN, $alias); |
1122 | } |
1123 | |
1124 | /** |
1125 | * Join. |
1126 | * |
1127 | * @param string|self $table Join query |
1128 | * @param null|string $alias Alias name (empty = none) |
1129 | * |
1130 | * @return Builder |
1131 | * |
1132 | * @since 1.0.0 |
1133 | */ |
1134 | public function leftOuterJoin(string | self $table, string $alias = null) : self |
1135 | { |
1136 | return $this->join($table, JoinType::LEFT_OUTER_JOIN, $alias); |
1137 | } |
1138 | |
1139 | /** |
1140 | * Join. |
1141 | * |
1142 | * @param string|self $table Join query |
1143 | * @param null|string $alias Alias name (empty = none) |
1144 | * |
1145 | * @return Builder |
1146 | * |
1147 | * @since 1.0.0 |
1148 | */ |
1149 | public function leftInnerJoin(string | self $table, string $alias = null) : self |
1150 | { |
1151 | return $this->join($table, JoinType::LEFT_INNER_JOIN, $alias); |
1152 | } |
1153 | |
1154 | /** |
1155 | * Join. |
1156 | * |
1157 | * @param string|self $table Join query |
1158 | * @param null|string $alias Alias name (empty = none) |
1159 | * |
1160 | * @return Builder |
1161 | * |
1162 | * @since 1.0.0 |
1163 | */ |
1164 | public function rightJoin(string | self $table, string $alias = null) : self |
1165 | { |
1166 | return $this->join($table, JoinType::RIGHT_JOIN, $alias); |
1167 | } |
1168 | |
1169 | /** |
1170 | * Join. |
1171 | * |
1172 | * @param string|self $table Join query |
1173 | * @param null|string $alias Alias name (empty = none) |
1174 | * |
1175 | * @return Builder |
1176 | * |
1177 | * @since 1.0.0 |
1178 | */ |
1179 | public function rightOuterJoin(string | self $table, string $alias = null) : self |
1180 | { |
1181 | return $this->join($table, JoinType::RIGHT_OUTER_JOIN, $alias); |
1182 | } |
1183 | |
1184 | /** |
1185 | * Join. |
1186 | * |
1187 | * @param string|self $table Join query |
1188 | * @param null|string $alias Alias name (empty = none) |
1189 | * |
1190 | * @return Builder |
1191 | * |
1192 | * @since 1.0.0 |
1193 | */ |
1194 | public function rightInnerJoin(string | self $table, string $alias = null) : self |
1195 | { |
1196 | return $this->join($table, JoinType::RIGHT_INNER_JOIN, $alias); |
1197 | } |
1198 | |
1199 | /** |
1200 | * Join. |
1201 | * |
1202 | * @param string|self $table Join query |
1203 | * @param null|string $alias Alias name (empty = none) |
1204 | * |
1205 | * @return Builder |
1206 | * |
1207 | * @since 1.0.0 |
1208 | */ |
1209 | public function outerJoin(string | self $table, string $alias = null) : self |
1210 | { |
1211 | return $this->join($table, JoinType::OUTER_JOIN, $alias); |
1212 | } |
1213 | |
1214 | /** |
1215 | * Join. |
1216 | * |
1217 | * @param string|self $table Join query |
1218 | * @param null|string $alias Alias name (empty = none) |
1219 | * |
1220 | * @return Builder |
1221 | * |
1222 | * @since 1.0.0 |
1223 | */ |
1224 | public function innerJoin(string | self $table, string $alias = null) : self |
1225 | { |
1226 | return $this->join($table, JoinType::INNER_JOIN, $alias); |
1227 | } |
1228 | |
1229 | /** |
1230 | * Join. |
1231 | * |
1232 | * @param string|self $table Join query |
1233 | * @param null|string $alias Alias name (empty = none) |
1234 | * |
1235 | * @return Builder |
1236 | * |
1237 | * @since 1.0.0 |
1238 | */ |
1239 | public function crossJoin(string | self $table, string $alias = null) : self |
1240 | { |
1241 | return $this->join($table, JoinType::CROSS_JOIN, $alias); |
1242 | } |
1243 | |
1244 | /** |
1245 | * Join. |
1246 | * |
1247 | * @param string|self $table Join query |
1248 | * @param null|string $alias Alias name (empty = none) |
1249 | * |
1250 | * @return Builder |
1251 | * |
1252 | * @since 1.0.0 |
1253 | */ |
1254 | public function fullJoin(string | self $table, string $alias = null) : self |
1255 | { |
1256 | return $this->join($table, JoinType::FULL_JOIN, $alias); |
1257 | } |
1258 | |
1259 | /** |
1260 | * Join. |
1261 | * |
1262 | * @param string|self $table Join query |
1263 | * @param null|string $alias Alias name (empty = none) |
1264 | * |
1265 | * @return Builder |
1266 | * |
1267 | * @since 1.0.0 |
1268 | */ |
1269 | public function fullOuterJoin(string | self $table, string $alias = null) : self |
1270 | { |
1271 | return $this->join($table, JoinType::FULL_OUTER_JOIN, $alias); |
1272 | } |
1273 | |
1274 | /** |
1275 | * Rollback. |
1276 | * |
1277 | * @return Builder |
1278 | * |
1279 | * @since 1.0.0 |
1280 | */ |
1281 | public function rollback() : self |
1282 | { |
1283 | return $this; |
1284 | } |
1285 | |
1286 | /** |
1287 | * On. |
1288 | * |
1289 | * @param string|array $columns Columns to join on |
1290 | * @param null|string|array $operator Comparison operator |
1291 | * @param null|string|array $values Values to compare with |
1292 | * @param string|array $boolean Concatonator |
1293 | * @param null|string $table Table this belongs to |
1294 | * |
1295 | * @return Builder |
1296 | * |
1297 | * @throws \InvalidArgumentException |
1298 | * |
1299 | * @since 1.0.0 |
1300 | */ |
1301 | public function on(string | array $columns, string | array $operator = null, mixed $values = null, string | array $boolean = 'and', string $table = null) : self |
1302 | { |
1303 | if (!\is_array($columns)) { |
1304 | $columns = [$columns]; |
1305 | $operator = [$operator]; |
1306 | $values = [$values]; |
1307 | $boolean = [$boolean]; |
1308 | } |
1309 | |
1310 | $joinCount = \count($this->joins) - 1; |
1311 | $i = 0; |
1312 | $table ??= \array_keys($this->joins)[$joinCount]; |
1313 | |
1314 | foreach ($columns as $column) { |
1315 | if (isset($operator[$i]) && !\in_array(\strtolower($operator[$i]), self::OPERATORS)) { |
1316 | throw new \InvalidArgumentException('Unknown operator.'); |
1317 | } |
1318 | |
1319 | $this->ons[$table][] = [ |
1320 | 'column' => $column, |
1321 | 'operator' => $operator[$i], |
1322 | 'value' => $values[$i], |
1323 | 'boolean' => $boolean[$i], |
1324 | ]; |
1325 | |
1326 | ++$i; |
1327 | } |
1328 | |
1329 | return $this; |
1330 | } |
1331 | |
1332 | /** |
1333 | * On. |
1334 | * |
1335 | * @param string|array $columns Columns to join on |
1336 | * @param null|string|array $operator Comparison operator |
1337 | * @param null|string|array $values Values to compare with |
1338 | * |
1339 | * @return Builder |
1340 | * |
1341 | * @since 1.0.0 |
1342 | */ |
1343 | public function orOn(string | array $columns, string | array $operator = null, string | array $values = null) : self |
1344 | { |
1345 | return $this->on($columns, $operator, $values, 'or'); |
1346 | } |
1347 | |
1348 | /** |
1349 | * On. |
1350 | * |
1351 | * @param string|array $columns Columns to join on |
1352 | * @param null|string|array $operator Comparison operator |
1353 | * @param null|string|array $values Values to compare with |
1354 | * |
1355 | * @return Builder |
1356 | * |
1357 | * @since 1.0.0 |
1358 | */ |
1359 | public function andOn(string | array $columns, string | array $operator = null, string | array $values = null) : self |
1360 | { |
1361 | return $this->on($columns, $operator, $values, 'and'); |
1362 | } |
1363 | |
1364 | /** |
1365 | * Merging query. |
1366 | * |
1367 | * Merging query in order to remove database query volume |
1368 | * |
1369 | * @return Builder |
1370 | * |
1371 | * @since 1.0.0 |
1372 | */ |
1373 | public function merge() : self |
1374 | { |
1375 | return clone($this); |
1376 | } |
1377 | |
1378 | /** |
1379 | * {@inheritdoc} |
1380 | */ |
1381 | public function execute() : ?\PDOStatement |
1382 | { |
1383 | $sth = null; |
1384 | |
1385 | try { |
1386 | $sth = $this->connection->con->prepare($this->toSql()); |
1387 | if ($sth === false) { |
1388 | return null; |
1389 | } |
1390 | |
1391 | foreach ($this->binds as $key => $bind) { |
1392 | $type = self::getBindParamType($bind); |
1393 | |
1394 | $sth->bindParam($key, $bind, $type); |
1395 | } |
1396 | |
1397 | $sth->execute(); |
1398 | } catch (\Throwable $t) { |
1399 | // @codeCoverageIgnoreStart |
1400 | \phpOMS\Log\FileLogger::getInstance()->error( |
1401 | \phpOMS\Log\FileLogger::MSG_FULL, [ |
1402 | 'message' => $t->getMessage() . ':' . $this->toSql(), |
1403 | 'line' => __LINE__, |
1404 | 'file' => self::class, |
1405 | ] |
1406 | ); |
1407 | |
1408 | $sth = null; |
1409 | // @codeCoverageIgnoreEnd |
1410 | } |
1411 | |
1412 | return $sth; |
1413 | } |
1414 | |
1415 | /** |
1416 | * Get bind parameter type. |
1417 | * |
1418 | * @param mixed $value Value to bind |
1419 | * |
1420 | * @return int |
1421 | * |
1422 | * @throws \Exception |
1423 | * |
1424 | * @since 1.0.0 |
1425 | */ |
1426 | public static function getBindParamType(mixed $value) : int |
1427 | { |
1428 | if (\is_int($value)) { |
1429 | return \PDO::PARAM_INT; |
1430 | } elseif (\is_string($value) || \is_float($value)) { |
1431 | return \PDO::PARAM_STR; |
1432 | } |
1433 | |
1434 | throw new \Exception(); |
1435 | } |
1436 | |
1437 | /** |
1438 | * Get column name |
1439 | * |
1440 | * @param mixed $column Column name |
1441 | * |
1442 | * @return string |
1443 | * |
1444 | * @throws \Exception |
1445 | * |
1446 | * @since 1.0.0 |
1447 | */ |
1448 | public static function getPublicColumnName(mixed $column) : string |
1449 | { |
1450 | if (\is_string($column)) { |
1451 | return $column; |
1452 | } elseif ($column instanceof Column) { |
1453 | return $column->getColumn(); |
1454 | } elseif ($column instanceof SerializableInterface) { |
1455 | return $column->serialize(); |
1456 | } elseif ($column instanceof self) { |
1457 | return \md5($column->toSql()); |
1458 | } |
1459 | |
1460 | throw new \Exception(); |
1461 | } |
1462 | } |