Completed
Push — master ( 1f031e...906125 )
by BENOIT
01:12
created

SelectQuery::buildLimit()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
4
namespace BenTools\Where;
5
6
/**
7
 * Class SelectQuery
8
 *
9
 * @property $flags
10
 * @property $distinct
11
 * @property $columns
12
 * @property $from
13
 * @property $joins
14
 * @property $where
15
 * @property $groupBy
16
 * @property $having
17
 * @property $orderBy
18
 * @property $limit
19
 * @property $offset
20
 */
21
final class SelectQuery
22
{
23
    /**
24
     * @var array
25
     */
26
    private $flags = [];
27
28
    /**
29
     * @var bool
30
     */
31
    private $distinct = false;
32
33
    /**
34
     * @var array
35
     */
36
    private $columns = ['*'];
37
38
    /**
39
     * @var string
40
     */
41
    private $from;
42
43
    /**
44
     * @var array
45
     */
46
    private $joins = [];
47
48
    /**
49
     * @var Expression
50
     */
51
    private $where;
52
53
    /**
54
     * @var array
55
     */
56
    private $groupBy = [];
57
58
    /**
59
     * @var Expression
60
     */
61
    private $having;
62
63
    /**
64
     * @var array
65
     */
66
    private $orderBy = [];
67
68
    /**
69
     * @var int
70
     */
71
    private $limit;
72
73
    /**
74
     * @var int
75
     */
76
    private $offset;
77
78
    /**
79
     * @param array ...$columns
80
     * @return SelectQuery
81
     */
82
    public static function make(string ...$columns): self
83
    {
84
        $select = new self;
85
        if (0 !== func_num_args()) {
86
            $select->columns = $columns;
87
        }
88
        return $select;
89
    }
90
91
    /**
92
     * @param string[] ...$flags
93
     * @return SelectQuery
94
     */
95
    public function withFlags(string ...$flags): self
96
    {
97
        $clone = clone $this;
98
        $clone->flags = $flags;
99
        return $clone;
100
    }
101
102
    /**
103
     * @param string[] ...$flags
104
     * @return SelectQuery
105
     */
106
    public function withAddedFlags(string ...$flags): self
107
    {
108
        $clone = clone $this;
109
        $existingFlags = array_map('strtoupper', $clone->flags);
110
        foreach ($flags as $flag) {
111
            if (!in_array(strtoupper($flag), $existingFlags, true)) {
112
                $clone->flags[] = $flag;
113
            }
114
        }
115
        return $clone;
116
    }
117
118
    /**
119
     * @param bool $distinct
120
     * @return SelectQuery
121
     */
122
    public function distinct(bool $distinct = true): self
123
    {
124
        $clone = clone $this;
125
        $clone->distinct = $distinct;
126
        return $clone;
127
    }
128
129
    /**
130
     * @param string[] ...$columns
131
     * @return SelectQuery
132
     */
133
    public function withColumns(string ...$columns): self
134
    {
135
        $clone = clone $this;
136
        $clone->columns = $columns;
137
        return $clone;
138
    }
139
140
    /**
141
     * @param string[] ...$columns
142
     * @return SelectQuery
143
     */
144
    public function withAddedColumns(string ...$columns): self
145
    {
146
        $clone = clone $this;
147
        $clone->columns = array_merge($clone->columns, $columns);
148
        return $clone;
149
    }
150
151
    /**
152
     * @param string $table
153
     * @return SelectQuery
154
     */
155
    public function from(string $table = null): self
156
    {
157
        $clone = clone $this;
158
        $clone->from = $table;
159
        return $clone;
160
    }
161
162
    /**
163
     * @param string $table
164
     * @param string|Expression|null   $expression
165
     * @param array  ...$values
166
     * @return SelectQuery
167
     * @throws \InvalidArgumentException
168
     */
169
    public function join(string $table, $expression = null, ...$values): self
170
    {
171
        $clone = clone $this;
172
        $clone->joins[$table] = [
173
            't' => 'JOIN',
174
            'c' => null !== $expression ? Expression::where($expression, ...$values) : null,
175
        ];
176
        return $clone;
177
    }
178
179
    /**
180
     * @param string $table
181
     * @param string|Expression|null   $expression
182
     * @param array  ...$values
183
     * @return SelectQuery
184
     * @throws \InvalidArgumentException
185
     */
186
    public function innerJoin(string $table, $expression = null, ...$values): self
187
    {
188
        $clone = clone $this;
189
        $clone->joins[$table] = [
190
            't' => 'INNER JOIN',
191
            'c' => null !== $expression ? Expression::where($expression, ...$values) : null,
192
        ];
193
        return $clone;
194
    }
195
196
    /**
197
     * @param string $table
198
     * @param string|Expression|null   $expression
199
     * @param array  ...$values
200
     * @return SelectQuery
201
     * @throws \InvalidArgumentException
202
     */
203
    public function outerJoin(string $table, $expression = null, ...$values): self
204
    {
205
        $clone = clone $this;
206
        $clone->joins[$table] = [
207
            't' => 'OUTER JOIN',
208
            'c' => null !== $expression ? Expression::where($expression, ...$values) : null,
209
        ];
210
        return $clone;
211
    }
212
213
    /**
214
     * @param string $table
215
     * @param string|Expression|null   $expression
216
     * @param array  ...$values
217
     * @return SelectQuery
218
     * @throws \InvalidArgumentException
219
     */
220
    public function leftJoin(string $table, $expression = null, ...$values): self
221
    {
222
        $clone = clone $this;
223
        $clone->joins[$table] = [
224
            't' => 'LEFT JOIN',
225
            'c' => null !== $expression ? Expression::where($expression, ...$values) : null,
226
        ];
227
        return $clone;
228
    }
229
230
    /**
231
     * @param string $table
232
     * @param string|Expression|null   $expression
233
     * @param array  ...$values
234
     * @return SelectQuery
235
     * @throws \InvalidArgumentException
236
     */
237
    public function leftOuterJoin(string $table, $expression = null, ...$values): self
238
    {
239
        $clone = clone $this;
240
        $clone->joins[$table] = [
241
            't' => 'LEFT OUTER JOIN',
242
            'c' => null !== $expression ? Expression::where($expression, ...$values) : null,
243
        ];
244
        return $clone;
245
    }
246
247
    /**
248
     * @param string $table
249
     * @param string|Expression|null   $expression
250
     * @param array  ...$values
251
     * @return SelectQuery
252
     * @throws \InvalidArgumentException
253
     */
254
    public function rightJoin(string $table, $expression = null, ...$values): self
255
    {
256
        $clone = clone $this;
257
        $clone->joins[$table] = [
258
            't' => 'RIGHT JOIN',
259
            'c' => null !== $expression ? Expression::where($expression, ...$values) : null,
260
        ];
261
        return $clone;
262
    }
263
264
    /**
265
     * @param string $table
266
     * @param string|Expression|null   $expression
267
     * @param array  ...$values
268
     * @return SelectQuery
269
     * @throws \InvalidArgumentException
270
     */
271
    public function rightOuterJoin(string $table, $expression = null, ...$values): self
272
    {
273
        $clone = clone $this;
274
        $clone->joins[$table] = [
275
            't' => 'RIGHT OUTER JOIN',
276
            'c' => null !== $expression ? Expression::where($expression, ...$values) : null,
277
        ];
278
        return $clone;
279
    }
280
281
    /**
282
     * @param string $table
283
     * @param string|Expression|null   $expression
284
     * @param array  ...$values
285
     * @return SelectQuery
286
     * @throws \InvalidArgumentException
287
     */
288
    public function fullJoin(string $table, $expression = null, ...$values): self
289
    {
290
        $clone = clone $this;
291
        $clone->joins[$table] = [
292
            't' => 'FULL JOIN',
293
            'c' => null !== $expression ? Expression::where($expression, ...$values) : null,
294
        ];
295
        return $clone;
296
    }
297
298
    /**
299
     * @param string $table
300
     * @param string|Expression|null   $expression
301
     * @param array  ...$values
302
     * @return SelectQuery
303
     * @throws \InvalidArgumentException
304
     */
305
    public function fullOuterJoin(string $table, $expression = null, ...$values): self
306
    {
307
        $clone = clone $this;
308
        $clone->joins[$table] = [
309
            't' => 'FULL OUTER JOIN',
310
            'c' => null !== $expression ? Expression::where($expression, ...$values) : null,
311
        ];
312
        return $clone;
313
    }
314
315
    /**
316
     * Reset all JOIN clauses.
317
     *
318
     * @return SelectQuery
319
     */
320
    public function resetJoins(): self
321
    {
322
        $clone = clone $this;
323
        $clone->joins = [];
324
        return $clone;
325
    }
326
327
    /**
328
     * Remove a specific JOIN clause.
329
     *
330
     * @param string $table
331
     * @return SelectQuery
332
     */
333
    public function withoutJoin(string $table)
334
    {
335
        $clone = clone $this;
336
        unset($clone->joins[$table]);
337
        return $clone;
338
    }
339
340
    /**
341
     * @param string|Expression|null  $expression
342
     * @param array ...$values
343
     * @return SelectQuery
344
     * @throws \InvalidArgumentException
345
     */
346
    public function where($expression = null, ...$values): self
347
    {
348
        $clone = clone $this;
349
        $clone->where = null !== $expression ? Expression::where($expression, ...$values) : null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null !== $expression ? \...ion, ...$values) : null can also be of type object<self>. However, the property $where is declared as type object<BenTools\Where\Expression>. Maybe add an additional type 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 mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
350
        return $clone;
351
    }
352
353
    /**
354
     * @param string|Expression $expression
355
     * @param array ...$values
356
     * @return SelectQuery
357
     * @throws \InvalidArgumentException
358
     */
359
    public function andWhere($expression, ...$values): self
360
    {
361
        if (null === $this->where) {
362
            return $this->where(Expression::where($expression, ...$values));
363
        }
364
        $clone = clone $this;
365
        $clone->where = $clone->where->and(Expression::where($expression, ...$values));
366
        return $clone;
367
    }
368
369
    /**
370
     * @param string|Expression $expression
371
     * @param array ...$values
372
     * @return SelectQuery
373
     * @throws \InvalidArgumentException
374
     */
375
    public function orWhere($expression, ...$values): self
376
    {
377
        if (null === $this->where) {
378
            return $this->where(Expression::where($expression, ...$values));
379
        }
380
        $clone = clone $this;
381
        $clone->where = $clone->where->or(Expression::where($expression, ...$values));
382
        return $clone;
383
    }
384
385
    /**
386
     * @param string[] ...$groupBy
387
     * @return SelectQuery
388
     */
389
    public function groupBy(string ...$groupBy): self
390
    {
391
        $clone = clone $this;
392
        $clone->groupBy = $groupBy;
393
        return $clone;
394
    }
395
396
    /**
397
     * @param string[] ...$groupBy
398
     * @return SelectQuery
399
     */
400
    public function andGroupBy(string ...$groupBy): self
401
    {
402
        $clone = clone $this;
403
        $clone->groupBy = array_merge($clone->groupBy, $groupBy);
404
        return $clone;
405
    }
406
407
    /**
408
     * @param string|Expression|null $expression
409
     * @param array ...$values
410
     * @return SelectQuery
411
     * @throws \InvalidArgumentException
412
     */
413
    public function having($expression = null, ...$values): self
414
    {
415
        $clone = clone $this;
416
        $clone->having = null !== $expression ? Expression::where($expression, ...$values): null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null !== $expression ? \...ion, ...$values) : null can also be of type object<self>. However, the property $having is declared as type object<BenTools\Where\Expression>. Maybe add an additional type 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 mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
417
        return $clone;
418
    }
419
420
    /**
421
     * @param string|Expression $expression
422
     * @param array ...$values
423
     * @return SelectQuery
424
     * @throws \InvalidArgumentException
425
     */
426
    public function andHaving($expression, ...$values): self
427
    {
428
        if (null === $this->having) {
429
            return $this->having(Expression::where($expression, ...$values));
430
        }
431
        $clone = clone $this;
432
        $clone->having = $clone->having->and(Expression::where($expression, ...$values));
433
        return $clone;
434
    }
435
436
    /**
437
     * @param string|Expression $expression
438
     * @param array ...$values
439
     * @return SelectQuery
440
     * @throws \InvalidArgumentException
441
     */
442
    public function orHaving($expression, ...$values): self
443
    {
444
        if (null === $this->having) {
445
            return $this->having(Expression::where($expression, ...$values));
446
        }
447
        $clone = clone $this;
448
        $clone->having = $clone->having->or(Expression::where($expression, ...$values));
449
        return $clone;
450
    }
451
452
    /**
453
     * @param string[] ...$groupBy
454
     * @return SelectQuery
455
     */
456
    public function orderBy(string ...$orderBy): self
457
    {
458
        $clone = clone $this;
459
        $clone->orderBy = $orderBy;
460
        return $clone;
461
    }
462
463
    /**
464
     * @param string[] ...$groupBy
465
     * @return SelectQuery
466
     */
467
    public function andOrderBy(string ...$orderBy): self
468
    {
469
        $clone = clone $this;
470
        $clone->orderBy = array_merge($clone->orderBy, $orderBy);
471
        return $clone;
472
    }
473
474
    /**
475
     * @param int|null $limit
476
     * @return SelectQuery
477
     */
478
    public function limit(int $limit = null): self
479
    {
480
        $clone = clone $this;
481
        $clone->limit = $limit;
482
        return $clone;
483
    }
484
485
    /**
486
     * @param int|null $offset
487
     * @return SelectQuery
488
     */
489
    public function offset(int $offset = null): self
490
    {
491
        $clone = clone $this;
492
        $clone->offset = $offset;
493
        return $clone;
494
    }
495
496
    /**
497
     * @return array
498
     */
499
    public function getValues(): array
500
    {
501
        $generator = function (Expression ...$expressions) {
502
            foreach ($expressions as $expression) {
503
                foreach ($expression->getValues() as $key => $value) {
504
                    if (is_numeric($key)) {
505
                        yield $value;
506
                    } else {
507
                        yield $key => $value;
508
                    }
509
                }
510
            }
511
        };
512
513
        $expressions = array_filter(array_merge(array_column($this->joins, 'c'), [$this->where, $this->having]));
514
        return iterator_to_array($generator(...$expressions));
515
    }
516
517
    /**
518
     * @return string
519
     */
520
    public function __toString(): string
521
    {
522
        $parts = $this->initBuild();
523
        $this->buildColumns($parts);
524
        $this->buildFrom($parts);
525
        $this->buildJoins($parts);
526
        $this->buildWhere($parts);
527
        $this->buildGroupBy($parts);
528
        $this->buildHaving($parts);
529
        $this->buildOrderBy($parts);
530
        $this->buildLimit($parts);
531
        $this->buildOffset($parts);
532
        return implode(' ', $parts);
533
    }
534
535
    /**
536
     * @return array
537
     */
538
    private function initBuild(): array
539
    {
540
        $parts = array_merge(['SELECT'], $this->flags);
541
542
        if (true === $this->distinct) {
543
            $parts[] = 'DISTINCT';
544
        }
545
        return $parts;
546
    }
547
548
    /**
549
     * @param array $parts
550
     */
551
    private function buildColumns(array &$parts)
552
    {
553
        $parts = array_merge($parts, [implode(', ', $this->columns)]);
554
    }
555
556
    /**
557
     * @param array $parts
558
     */
559
    private function buildFrom(array &$parts)
560
    {
561
        if (null !== $this->from) {
562
            $parts = array_merge($parts, ['FROM', $this->from]);
563
        }
564
    }
565
566
    /**
567
     * @param array $parts
568
     */
569
    private function buildJoins(array &$parts)
570
    {
571
        if ([] !== $this->joins) {
572
            foreach ($this->joins as $table => $join) {
573
                $str = sprintf('%s %s', $join['t'], $table);
574
                if (null !== $join['c']) {
575
                    $str .= sprintf(' ON %s', $join['c']);
576
                }
577
                $parts[] = $str;
578
            }
579
        }
580
    }
581
582
    /**
583
     * @param array $parts
584
     */
585
    private function buildWhere(array &$parts)
586
    {
587
        if (null !== $this->where) {
588
            $parts = array_merge($parts, ['WHERE', (string) $this->where]);
589
        }
590
    }
591
592
    /**
593
     * @param array $parts
594
     */
595
    private function buildGroupBy(array &$parts)
596
    {
597
        if ([] !== $this->groupBy) {
598
            $parts = array_merge($parts, ['GROUP BY'], [implode(', ', $this->groupBy)]);
599
        }
600
    }
601
602
    /**
603
     * @param array $parts
604
     */
605
    private function buildHaving(array &$parts)
606
    {
607
        if (null !== $this->having) {
608
            $parts = array_merge($parts, ['HAVING', (string) $this->having]);
609
        }
610
    }
611
612
    /**
613
     * @param array $parts
614
     */
615
    private function buildOrderBy(array &$parts)
616
    {
617
        if ([] !== $this->orderBy) {
618
            $parts = array_merge($parts, ['ORDER BY'], [implode(', ', $this->orderBy)]);
619
        }
620
    }
621
622
    /**
623
     * @param array $parts
624
     */
625
    private function buildLimit(array &$parts)
626
    {
627
        if (null !== $this->limit) {
628
            $parts[] = sprintf('LIMIT %d', $this->limit);
629
        }
630
    }
631
632
    /**
633
     * @param array $parts
634
     */
635
    private function buildOffset(array &$parts)
636
    {
637
        if (null !== $this->offset) {
638
            $parts[] = sprintf('OFFSET %d', $this->offset);
639
        }
640
    }
641
642
    /**
643
     * Read-only properties.
644
     *
645
     * @param $property
646
     * @return mixed
647
     * @throws \InvalidArgumentException
648
     */
649
    public function __get($property)
650
    {
651
        if (!property_exists($this, $property)) {
652
            throw new \InvalidArgumentException(sprintf('Property %s::$%s does not exist.', __CLASS__, $property));
653
        }
654
        return $this->{$property};
655
    }
656
}
657