Passed
Push — main ( d64da0...4e824a )
by William
01:36
created

Query::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 2
b 0
f 0
nc 1
nop 2
dl 0
loc 4
rs 10
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 array
39
     */
40
    protected $params = [];
41
42
    /**
43
     * @var string
44
     */
45
    protected $order;
46
47
    /**
48
     * @var int
49
     */
50
    protected $limit;
51
52
    /**
53
     * @var int
54
     */
55
    protected $offset;
56
57
    /**
58
     * @var string
59
     */
60
    protected $joins;
61
62
    /**
63
     * @var string
64
     */
65
    protected $query;
66
67
    /**
68
     * @var string
69
     */
70
    protected $groupBy;
71
72
    /**
73
     * @var string
74
     */
75
    protected $having;
76
77
    /**
78
     * @var \PDO|null
79
     */
80
    protected $db;
81
82
    /**
83
     * @var array|null
84
     */
85
    protected $connectionConfig;
86
87
    public function __construct(string $connectionName = 'default', bool $regenerateConnection = false)
88
    {
89
        $this->db = Connect::getInstance($connectionName, $regenerateConnection);
90
        $this->connectionConfig = Connect::getConfig($connectionName);
91
    }
92
93
94
    public function from(string $entity)
95
    {
96
        $this->entity = $entity;
97
        return $this;
98
    }
99
100
    public function setSilentErrors(bool $silentErrors = false)
101
    {
102
        $this->silentErrors = $silentErrors;
103
    }
104
105
    public function fromSubQuery(Query $query, string $alias = 'sub')
106
    {
107
        $this->entity = "(" . $query->toSQL() . ") as $alias";
108
        return $this;
109
    }
110
111
112
    /**
113
     * @param string $columns
114
     * @return $this
115
     */
116
    public function selectRaw(string $columns = "*", array $params = []): Query
117
    {
118
        $this->columns = array_merge($this->columns, explode(',', $columns));
119
        $this->params($params);
120
        return $this;
121
    }
122
123
    public function select(array $columns = [], array $params = []): Query
124
    {
125
        $columns = empty($columns) ? ['*'] : $columns;
126
127
        $this->columns = array_merge($this->columns, $columns);
128
        $this->params($params);
129
        return $this;
130
    }
131
132
    /**
133
     * @param string $where
134
     * @return $this
135
     */
136
    public function where(string $where, array $params = []): Query
137
    {
138
        $this->params($params);
139
140
        if (!empty($this->where)) {
141
            $this->where .= " AND {$where}";
142
            return $this;
143
        }
144
145
        $this->where = "WHERE $where";
146
        return $this;
147
    }
148
149
    /**
150
     * @param string $where
151
     * @return $this
152
     */
153
    public function orWhere(string $where, array $params = []): Query
154
    {
155
        $this->params($params);
156
157
        if (!empty($this->where)) {
158
            $this->where .= " OR {$where}";
159
            return $this;
160
        }
161
162
        $this->where = "WHERE $where";
163
        return $this;
164
    }
165
166
    public function whereIn(string $column, array $data): Query
167
    {
168
        $inPlaceHolder = [];
169
        foreach ($data as $d) {
170
            $paramNum = count($this->params) + 1;
171
            $paramName = "in{$paramNum}";
172
            $inPlaceHolder[] = ":{$paramName}";
173
            $this->params([
174
                $paramName => $d
175
            ]);
176
        }
177
178
        $inString = implode(",", $inPlaceHolder);
179
180
        if (!empty($this->where)) {
181
            $this->where .= " AND {$column} IN ({$inString})";
182
            return $this;
183
        }
184
185
        $this->where = "WHERE {$column} IN ({$inString})";
186
        return $this;
187
    }
188
189
    public function orWhereIn(string $column, array $data): Query
190
    {
191
        $columnUppercase = ucfirst($column);
192
193
        $inPlaceHolder = [];
194
        foreach ($data as $d) {
195
            $paramNum = count($this->params) + 1;
196
            $paramName = "in{$columnUppercase}{$paramNum}";
197
            $inPlaceHolder[] = ":{$paramName}";
198
            $this->params([
199
                $paramName => $d
200
            ]);
201
        }
202
203
        $inString = implode(",", $inPlaceHolder);
204
205
        if (!empty($this->where)) {
206
            $this->where .= " OR {$column} IN ($inString)";
207
            return $this;
208
        }
209
210
        $this->where = "WHERE {$column} IN ($inString)";
211
        return $this;
212
    }
213
214
215
    /**
216
     * @param string $columnOrder
217
     */
218
    public function order(string $columnOrder): Query
219
    {
220
        $this->order = "ORDER BY :order";
221
        $this->params(['order' => $columnOrder]);
222
        return $this;
223
    }
224
225
    /**
226
     * @param int $limit
227
     */
228
    public function limit(int $limit): Query
229
    {
230
        $this->limit = "LIMIT :limit";
0 ignored issues
show
Documentation Bug introduced by
The property $limit was declared of type integer, but 'LIMIT :limit' is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
231
        $this->params(['limit' => $limit]);
232
        return $this;
233
    }
234
235
    /**
236
     * @param int $offset
237
     * @return DB
238
     */
239
    public function offset(int $offset): Query
240
    {
241
        $this->offset = "OFFSET :offset";
0 ignored issues
show
Documentation Bug introduced by
The property $offset was declared of type integer, but 'OFFSET :offset' is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
242
        $this->params(['offset' => $offset]);
243
        return $this;
244
    }
245
246
    /**
247
     * @param array $group
248
     * @return DB
249
     */
250
    public function groupBy(array $group): Query
251
    {
252
        $groupBind = implode(',', array_fill(0, count($group), '?'));
253
        foreach ($group as $key => $g) {
254
            $this->params(["group_{$key}" => $g]);
255
        }
256
        $this->groupBy = "GROUP BY $groupBind";
257
        return $this;
258
    }
259
260
    /**
261
     * @param string $having
262
     * @return DB
263
     */
264
    public function having(string $having): Query
265
    {
266
        $this->having = "HAVING $having";
267
        return $this;
268
    }
269
270
    /**
271
     * @param array $params
272
     * @return $this
273
     */
274
    public function params(array $params = []): Query
275
    {
276
        $this->params = array_merge($this->params, $params);
277
278
        return $this;
279
    }
280
281
    /**
282
     * @param string $join
283
     * @return $this
284
     */
285
    public function leftJoin(string $join, array $params = []): Query
286
    {
287
        $this->params($params);
288
289
        if (!empty($this->joins)) {
290
            $this->joins .= PHP_EOL;
291
        }
292
        $this->joins .= "LEFT JOIN $join";
293
        return $this;
294
    }
295
296
    /**
297
     * @param string $join
298
     * @return $this
299
     */
300
    public function rightJoin(string $join, array $params = []): Query
301
    {
302
        $this->params($params);
303
304
        if (!empty($this->joins)) {
305
            $this->joins .= PHP_EOL;
306
        }
307
        $this->joins .= "RIGHT JOIN $join";
308
309
        return $this;
310
    }
311
312
    /**
313
     * @param string $join
314
     * @return $this
315
     */
316
    public function join(string $join, array $params = []): Query
317
    {
318
        $this->params($params);
319
320
        if (!empty($this->joins)) {
321
            $this->joins .= PHP_EOL;
322
        }
323
        $this->joins .= "INNER JOIN $join";
324
        return $this;
325
    }
326
327
    public function joinSub(Query $subquery, string $alias, $condition)
328
    {
329
        if (!empty($this->joins)) {
330
            $this->joins .= PHP_EOL;
331
        }
332
        $this->joins .= "INNER JOIN ({$subquery->toSQL()}) AS $alias ON $condition";
333
        return $this;
334
    }
335
336
    public function leftJoinSub(Query $subquery, string $alias, $condition)
337
    {
338
        if (!empty($this->joins)) {
339
            $this->joins .= PHP_EOL;
340
        }
341
        $this->joins .= "LEFT JOIN ({$subquery->toSQL()}) AS $alias ON $condition";
342
        return $this;
343
    }
344
345
    public function rightJoinSub(Query $subquery, string $alias, $condition)
346
    {
347
        if (!empty($this->joins)) {
348
            $this->joins .= PHP_EOL;
349
        }
350
        $this->joins .= "RIGHT JOIN ({$subquery->toSQL()}) AS $alias ON $condition";
351
        return $this;
352
    }
353
354
    public function get(): ?array
355
    {
356
        $this->mountQuery();
357
358
        try {
359
            $stmt = $this->db->prepare($this->query);
0 ignored issues
show
Bug introduced by
The method prepare() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

359
            /** @scrutinizer ignore-call */ 
360
            $stmt = $this->db->prepare($this->query);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
360
            $this->bind($stmt);
361
            $stmt->execute();
362
363
            if (!$stmt->rowCount()) {
364
                return [];
365
            }
366
367
            return $stmt->fetchAll(\PDO::FETCH_CLASS);
368
        } catch (\PDOException $exception) {
369
            return $this->handleError($exception);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->handleError($exception) targeting Willry\QueryBuilder\Query::handleError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
370
        }
371
    }
372
373
    public function first(): ?stdClass
374
    {
375
        $this->mountQuery();
376
377
        try {
378
            $stmt = $this->db->prepare($this->query);
379
            $this->bind($stmt);
380
            $stmt->execute();
381
382
            if (!$stmt->rowCount()) {
383
                return null;
384
            }
385
386
            return $stmt->fetchObject();
387
        } catch (\PDOException $exception) {
388
            return $this->handleError($exception);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->handleError($exception) targeting Willry\QueryBuilder\Query::handleError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
389
        }
390
    }
391
392
    public function count(): ?int
393
    {
394
        $this->mountQuery();
395
396
        try {
397
            $stmt = $this->db->prepare($this->query);
398
            $this->bind($stmt);
399
            $stmt->execute();
400
401
            return $stmt->rowCount();
402
        } catch (\PDOException $exception) {
403
            return $this->handleError($exception);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->handleError($exception) targeting Willry\QueryBuilder\Query::handleError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
404
        }
405
    }
406
407
408
    /**
409
     * @param array $data
410
     * @return string|null
411
     */
412
    public function create(array $data)
413
    {
414
        try {
415
            $columns = implode(", ", array_keys($data));
416
            $values = ":" . implode(", :", array_keys($data));
417
418
            $stmt = $this->db->prepare("INSERT INTO {$this->entity} ({$columns}) VALUES ({$values})");
419
            $this->params($data);
420
421
            $this->bind($stmt);
422
423
            $stmt->execute();
424
425
            return $this->db->lastInsertId();
426
        } catch (\PDOException $exception) {
427
            return $this->handleError($exception);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->handleError($exception) targeting Willry\QueryBuilder\Query::handleError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
428
        }
429
    }
430
431
    /**
432
     * @param array $data
433
     */
434
    public function update(array $data): ?int
435
    {
436
        try {
437
            $dateSet = [];
438
            foreach ($data as $bind => $value) {
439
                $dateSet[] = "{$bind} = :{$bind}";
440
            }
441
            $dateSet = implode(", ", $dateSet);
442
443
            $stmt = $this->db->prepare("UPDATE {$this->entity} SET {$dateSet} {$this->where}");
444
445
            $this->params($data);
446
447
            $this->bind($stmt);
448
449
            $stmt->execute();
450
451
            return $stmt->rowCount() ?? 1;
452
        } catch (\PDOException $exception) {
453
            return $this->handleError($exception);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->handleError($exception) targeting Willry\QueryBuilder\Query::handleError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
454
        }
455
    }
456
457
    public function delete(): ?int
458
    {
459
        try {
460
            $stmt = $this->db->prepare("DELETE FROM {$this->entity} {$this->where}");
461
            $this->bind($stmt);
462
            $stmt->execute();
463
            return $stmt->rowCount() ?? 1;
464
        } catch (\PDOException $exception) {
465
            return $this->handleError($exception);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->handleError($exception) targeting Willry\QueryBuilder\Query::handleError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
466
        }
467
    }
468
469
470
    public function beginTransaction(): bool
471
    {
472
        return $this->db->beginTransaction();
473
    }
474
475
476
    public function commit(): bool
477
    {
478
        return $this->db->commit();
479
    }
480
481
482
    public function rollback(): bool
483
    {
484
        return $this->db->rollBack();
485
    }
486
487
    /**
488
     * @param  $exception
489
     * @return null
490
     * @throws Exception
491
     */
492
    private function handleError(\Exception $exception)
493
    {
494
        if ($this->silentErrors) {
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
            return null;
497
        }
498
499
        throw $exception;
500
    }
501
502
503
    private function mountQuery(): void
504
    {
505
506
        $columns = !empty($this->columns) ? $this->columns : ['*'];
507
        $columns = implode(',', $columns);
508
        $this->query = "SELECT $columns FROM $this->entity $this->joins $this->where $this->groupBy $this->having $this->order $this->limit $this->offset";
509
    }
510
511
    /**
512
     * @param array $data
513
     * @return array|null
514
     */
515
    private function filter(array $data): ?array
516
    {
517
        $filter = [];
518
        foreach ($data as $key => $value) {
519
            $filter[$key] = (is_null($value) ? null : filter_var($value, FILTER_DEFAULT));
520
        }
521
        return $filter;
522
    }
523
524
    public function dump(): void
525
    {
526
        $this->mountQuery();
527
528
        var_dump(
0 ignored issues
show
Security Debugging Code introduced by
var_dump(array('query' =...$this->finalParams()))) looks like debug code. Are you sure you do not want to remove it?
Loading history...
529
            [
530
                "query" => $this->query,
531
                "raw_params" => $this->params(),
532
                "filtered_params" => $this->filter($this->finalParams())
533
            ]
534
        );
535
        exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
536
    }
537
538
    public function toSQL()
539
    {
540
        $this->mountQuery();
541
        return $this->query;
542
    }
543
544
    public static function dynamicQueryFilters(array &$queryParams, string $queryString, array $bind)
545
    {
546
        $queryParams["filters"][] = $queryString;
547
        $queryParams["binds"] = !empty($queryParams["binds"]) ? array_merge($queryParams["binds"], $bind) : $bind;
548
549
        $queryString = "";
550
551
        if (empty($queryParams["filters"])) return "";
552
553
        foreach ($queryParams["filters"] as $key => $filtro) {
554
            if ($key === 0) {
555
                $queryString .= $filtro;
556
            } else {
557
                $queryString .= " AND {$filtro}";
558
            }
559
        }
560
561
        $queryParams["queryString"] = $queryString;
562
563
        return $queryParams;
564
    }
565
566
    public function finalParams()
567
    {
568
        $result = [];
569
        foreach ($this->params as $key => $param) {
570
            if (is_array($param)) {
571
                $result[$key] = implode(",", $param);
572
            } else {
573
                $result[$key] = $param;
574
            }
575
        }
576
        return $result;
577
    }
578
579
    public function bind(PDOStatement &$stmt)
580
    {
581
        $binds = $this->filter($this->finalParams());
582
583
        foreach ($binds as $key => $bind) {
584
            if ($key == 'limit' || $key == "offset") {
585
                $stmt->bindValue(":$key", $bind, \PDO::PARAM_INT);
586
            } else {
587
                $stmt->bindValue(":$key", $bind);
588
            }
589
        }
590
591
        return $binds;
592
    }
593
594
    public function getConnection()
595
    {
596
        return Connect::getInstance($this->connectionName);
0 ignored issues
show
Bug introduced by
The property connectionName does not exist on Willry\QueryBuilder\Query. Did you mean connectionConfig?
Loading history...
597
    }
598
}
599