Passed
Branch test (9a25f2)
by William
01:42
created

Query::mergeBindFromAnotherQuery()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Willry\QueryBuilder;
4
5
use Exception;
6
use PDOStatement;
7
use stdClass;
8
9
abstract class Query
10
{
11
12
    /**
13
     * @var bool
14
     */
15
    protected $silentErrors;
16
17
    /**
18
     * @var \PDOException|null
19
     */
20
    protected $fail;
21
22
    /**
23
     * @var string $entity database table
24
     */
25
    protected $entity;
26
27
    /**
28
     * @var array
29
     */
30
    protected $columns = [];
31
32
    /**
33
     * @var string
34
     */
35
    protected $where;
36
37
    /**
38
     * @var string
39
     */
40
    protected $order;
41
42
    /**
43
     * @var string
44
     */
45
    protected $limit;
46
47
    /**
48
     * @var string
49
     */
50
    protected $offset;
51
52
    /**
53
     * @var string
54
     */
55
    protected $joins;
56
57
    /**
58
     * @var string
59
     */
60
    protected $query;
61
62
    /**
63
     * @var string
64
     */
65
    protected $groupBy;
66
67
    /**
68
     * @var string
69
     */
70
    protected $having;
71
72
    /**
73
     * @var \PDO
74
     */
75
    protected $db;
76
77
    public $bindings = [
78
        'select' => [],
79
        'from' => [],
80
        'join' => [],
81
        'where' => [],
82
        'groupBy' => [],
83
        'having' => [],
84
        'order' => [],
85
        'union' => [],
86
        'unionOrder' => [],
87
    ];
88
89
    /**
90
     * @var array|null
91
     */
92
    protected $connectionConfig;
93
94
95
    public function __construct(string $connectionName = 'default', bool $regenerateConnection = false)
96
    {
97
        $this->db = Connect::getInstance($connectionName, $regenerateConnection);
98
        $this->connectionConfig = Connect::getConfig($connectionName);
99
    }
100
101
102
    public function from(string $entity)
103
    {
104
        $this->entity = $entity;
105
        return $this;
106
    }
107
108
    public function setSilentErrors(bool $silentErrors = false)
109
    {
110
        $this->silentErrors = $silentErrors;
111
    }
112
113
    public function fromSubQuery(Query $query, string $alias = 'sub')
114
    {
115
        $this->entity = "(" . $query->toSQL() . ") as $alias";
116
        return $this;
117
    }
118
119
120
    /**
121
     * @param string $columns
122
     * @return $this
123
     */
124
    public function selectRaw(string $columns = "*", array $params = []): Query
125
    {
126
        $this->setBindings($params, 'select');
127
        $this->columns = array_merge($this->columns, explode(',', $columns));
128
        return $this;
129
    }
130
131
    public function select(array $columns = []): Query
132
    {
133
        $columns = empty($columns) ? ['*'] : $columns;
134
135
        $this->columns = array_merge($this->columns, $columns);
136
        return $this;
137
    }
138
139
    /**
140
     * @param string $where
141
     * @return $this
142
     */
143
    public function where(string $where, array $params = []): Query
144
    {
145
        $this->setBindings($params, 'where');
146
147
        if (!empty($this->where)) {
148
            $this->where .= " AND {$where}";
149
            return $this;
150
        }
151
152
        $this->where = "WHERE $where";
153
        return $this;
154
    }
155
156
    /**
157
     * @param string $where
158
     * @return $this
159
     */
160
    public function orWhere(string $where, array $params = []): Query
161
    {
162
        $this->setBindings($params, 'where');
163
        if (!empty($this->where)) {
164
            $this->where .= " OR {$where}";
165
            return $this;
166
        }
167
168
        $this->where = "WHERE $where";
169
        return $this;
170
    }
171
172
    public function whereIn(string $column, array $data): Query
173
    {
174
        $inString = implode(',', array_fill(0, count($data), '?'));
175
        $this->setBindings($data, 'where');
176
177
        if (!empty($this->where)) {
178
            $this->where .= " AND {$column} IN ({$inString})";
179
            return $this;
180
        }
181
182
        $this->where = "WHERE {$column} IN ({$inString})";
183
        return $this;
184
    }
185
186
    public function orWhereIn(string $column, array $data): Query
187
    {
188
189
        $inString = implode(',', array_fill(0, count($data), '?'));
190
        $this->setBindings($data, 'where');
191
192
        if (!empty($this->where)) {
193
            $this->where .= " OR {$column} IN ($inString)";
194
            return $this;
195
        }
196
197
        $this->where = "WHERE {$column} IN ($inString)";
198
        return $this;
199
    }
200
201
202
    /**
203
     * @param string $columnOrder
204
     */
205
    public function order(string $columnOrder): Query
206
    {
207
        $this->order = "ORDER BY ?";
208
        $this->setBindings([$columnOrder], 'order');
209
        return $this;
210
    }
211
212
    /**
213
     * @param int $limit
214
     */
215
    public function limit(int $limit): Query
216
    {
217
        $this->limit = "LIMIT $limit";
218
        return $this;
219
    }
220
221
    /**
222
     * @param int $offset
223
     * @return DB
224
     */
225
    public function offset(int $offset): Query
226
    {
227
        $this->offset = "OFFSET $offset";
228
        return $this;
229
    }
230
231
    /**
232
     * @param array $group
233
     * @return DB
234
     */
235
    public function groupBy(array $group): Query
236
    {
237
        $this->setBindings($group, 'groupBy');
238
        $this->groupBy = "GROUP BY ?";
239
        return $this;
240
    }
241
242
    /**
243
     * @param string $having
244
     * @return DB
245
     */
246
    public function having(string $having): Query
247
    {
248
        $this->setBindings([$having], 'having');
249
        $this->having = "HAVING ?";
250
        return $this;
251
    }
252
253
    public function getBindings()
254
    {
255
        return $this->bindings;
256
    }
257
258
    /**
259
     * @param array $params
260
     * @return $this
261
     */
262
    public function setBindings(array $params = [], string $type = 'where'): Query
263
    {
264
        $this->bindings[$type] = array_merge($params, $this->bindings[$type]);
265
266
        return $this;
267
    }
268
269
    /**
270
     * @param string $join
271
     * @return $this
272
     */
273
    public function leftJoin(string $join, array $params = []): Query
274
    {
275
        $this->setBindings($params, 'join');
276
277
        $this->joins .= PHP_EOL;
278
279
        $this->joins .= "LEFT JOIN $join";
280
281
        return $this;
282
    }
283
284
    /**
285
     * @param string $join
286
     * @return $this
287
     */
288
    public function rightJoin(string $join, array $params = []): Query
289
    {
290
        $this->setBindings($params, 'join');
291
292
        $this->joins .= PHP_EOL;
293
294
        $this->joins .= "RIGHT JOIN $join";
295
296
        return $this;
297
    }
298
299
    /**
300
     * @param string $join
301
     * @return $this
302
     */
303
    public function join(string $join, array $params = []): Query
304
    {
305
        $this->setBindings($params, 'join');
306
307
        $this->joins .= PHP_EOL;
308
309
        $this->joins .= "INNER JOIN $join";
310
        return $this;
311
    }
312
313
    public function joinSub(Query $subquery, string $alias, $condition, array $params = [])
314
    {
315
        $this->setBindings($params, 'join');
316
317
        $this->joins .= PHP_EOL;
318
319
        $this->joins .= "INNER JOIN ({$subquery->toSQL()}) AS $alias ON $condition";
320
321
        $this->mergeBindFromAnotherQuery($subquery);
322
323
        return $this;
324
    }
325
326
    public function leftJoinSub(Query $subquery, string $alias, $condition, array $params = [])
327
    {
328
        $this->setBindings($params, 'join');
329
330
        $this->joins .= PHP_EOL;
331
332
        $this->joins .= "LEFT JOIN ({$subquery->toSQL()}) AS $alias ON $condition";
333
334
        $this->mergeBindFromAnotherQuery($subquery);
335
336
        return $this;
337
    }
338
339
    public function rightJoinSub(Query $subquery, string $alias, $condition, array $params = [])
340
    {
341
        $this->setBindings($params, 'join');
342
343
        $this->joins .= PHP_EOL;
344
345
        $this->joins .= "RIGHT JOIN ({$subquery->toSQL()}) AS $alias ON $condition";
346
347
        $this->mergeBindFromAnotherQuery($subquery);
348
349
        return $this;
350
    }
351
352
    public function get(): ?array
353
    {
354
        $this->mountQuery();
355
356
        try {
357
            $stmt = $this->db->prepare($this->query);
358
            QueryHelpers::bind($stmt, $this->flatBindings());
359
            $stmt->execute();
360
361
            if (!$stmt->rowCount()) {
362
                return [];
363
            }
364
365
            return $stmt->fetchAll(\PDO::FETCH_CLASS);
366
        } catch (\PDOException $exception) {
367
            $this->handleError($exception);
368
        }
369
    }
370
371
    public function first(): ?stdClass
372
    {
373
        $this->mountQuery();
374
375
        try {
376
            $stmt = $this->db->prepare($this->query);
377
            QueryHelpers::bind($stmt, $this->flatBindings());
378
            $stmt->execute();
379
380
            if (!$stmt->rowCount()) {
381
                return null;
382
            }
383
384
            return $stmt->fetchObject();
385
        } catch (\PDOException $exception) {
386
            $this->handleError($exception);
387
        }
388
    }
389
390
    public function count(): ?int
391
    {
392
        $this->mountQuery();
393
394
        try {
395
            $stmt = $this->db->prepare($this->query);
396
            QueryHelpers::bind($stmt, $this->flatBindings());
397
            $stmt->execute();
398
399
            return $stmt->rowCount();
400
        } catch (\PDOException $exception) {
401
            $this->handleError($exception);
402
        }
403
    }
404
405
406
    /**
407
     * @param array $data
408
     * @return string|null
409
     */
410
    public function create(array $data)
411
    {
412
        try {
413
            $columns = implode(", ", array_keys($data));
414
            $values = ":" . implode(", :", array_keys($data));
415
416
            $stmt = $this->db->prepare("INSERT INTO {$this->entity} ({$columns}) VALUES ({$values})");
417
            $this->setBindings($data);
418
419
            QueryHelpers::bind($stmt, $this->flatBindings());
420
421
            $stmt->execute();
422
423
            return $this->db->lastInsertId();
424
        } catch (\PDOException $exception) {
425
            $this->handleError($exception);
426
        }
427
    }
428
429
    /**
430
     * @param array $data
431
     */
432
    public function update(array $data): ?int
433
    {
434
        try {
435
            $dateSet = [];
436
            foreach ($data as $bind => $value) {
437
                $dateSet[] = "{$bind} = :{$bind}";
438
            }
439
            $dateSet = implode(", ", $dateSet);
440
441
            $stmt = $this->db->prepare("UPDATE {$this->entity} SET {$dateSet} {$this->where}");
442
443
            $this->setBindings($data);
444
445
            QueryHelpers::bind($stmt, $this->flatBindings());
446
447
            $stmt->execute();
448
449
            return $stmt->rowCount() ?? 1;
450
        } catch (\PDOException $exception) {
451
            $this->handleError($exception);
452
        }
453
    }
454
455
    public function delete(): ?int
456
    {
457
        try {
458
            $stmt = $this->db->prepare("DELETE FROM {$this->entity} {$this->where}");
459
            QueryHelpers::bind($stmt, $this->flatBindings());
460
            $stmt->execute();
461
            return $stmt->rowCount() ?? 1;
462
        } catch (\PDOException $exception) {
463
            $this->handleError($exception);
464
        }
465
    }
466
467
468
    public function beginTransaction(): bool
469
    {
470
        return $this->db->beginTransaction();
471
    }
472
473
474
    public function commit(): bool
475
    {
476
        return $this->db->commit();
477
    }
478
479
480
    public function rollback(): bool
481
    {
482
        return $this->db->rollBack();
483
    }
484
485
    /**
486
     * @param  $exception
487
     * @throws Exception
488
     */
489
    private function handleError(\Exception $exception)
490
    {
491
        if (!$this->silentErrors) {
492
            throw $exception;
493
        }
494
495
        $this->fail = $exception;
0 ignored issues
show
Documentation Bug introduced by
$exception is of type Exception, but the property $fail was declared to be of type PDOException|null. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
496
    }
497
498
499
    private function mountQuery(): void
500
    {
501
502
        $columns = !empty($this->columns) ? $this->columns : ['*'];
503
        $columns = implode(',', $columns);
504
        $this->query = "SELECT $columns FROM $this->entity $this->joins $this->where $this->groupBy $this->having $this->order $this->limit $this->offset";
505
    }
506
507
508
    public function dump(): array
509
    {
510
        $this->mountQuery();
511
512
        return [
513
            "query" => $this->query,
514
            "raw_params" => $this->bindings,
515
        ];
516
    }
517
518
    public function toSQL()
519
    {
520
        $this->mountQuery();
521
        return $this->query;
522
    }
523
524
525
    private function flatBindings()
526
    {
527
        $params = [];
528
        foreach ($this->bindings as $key => $binds) {
529
            $params = array_merge($params, $this->bindings[$key]);
530
        }
531
532
        return $params;
533
    }
534
535
    public function mergeBindFromAnotherQuery(Query $query)
536
    {
537
        $this->bindings = array_merge_recursive($this->getBindings(), $query->getBindings());
538
    }
539
}
540