Completed
Push — master ( f40c6a...9a6de3 )
by BENOIT
04:32
created

SelectQueryBuilder::preview()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
4
namespace BenTools\Where\SelectQuery;
5
6
use BenTools\Where\Expression\Expression;
7
use BenTools\Where\Helper\Previewer;
8
use function BenTools\Where\valuesOf;
9
10
/**
11
 * Class SelectQuery
12
 *
13
 * @property $mainKeyword
14
 * @property $flags
15
 * @property $distinct
16
 * @property $columns
17
 * @property $from
18
 * @property $joins
19
 * @property $where
20
 * @property $groupBy
21
 * @property $having
22
 * @property $orderBy
23
 * @property $limit
24
 * @property $offset
25
 * @property $end
26
 */
27
final class SelectQueryBuilder
28
{
29
    /**
30
     * @var string
31
     */
32
    private $mainKeyword = 'SELECT';
33
34
    /**
35
     * @var array
36
     */
37
    private $flags = [];
38
39
    /**
40
     * @var bool
41
     */
42
    private $distinct = false;
43
44
    /**
45
     * @var array
46
     */
47
    private $columns = [];
48
49
    /**
50
     * @var string
51
     */
52
    private $from;
53
54
    /**
55
     * @var array
56
     */
57
    private $joins = [];
58
59
    /**
60
     * @var Expression
61
     */
62
    private $where;
63
64
    /**
65
     * @var array
66
     */
67
    private $groupBy = [];
68
69
    /**
70
     * @var Expression
71
     */
72
    private $having;
73
74
    /**
75
     * @var array
76
     */
77
    private $orderBy = [];
78
79
    /**
80
     * @var int
81
     */
82
    private $limit;
83
84
    /**
85
     * @var int
86
     */
87
    private $offset;
88
89
    /**
90
     * @var string
91
     */
92
    private $end = ';';
93
94
    /**
95
     * @param Expression[]|string[] ...$columns
96
     * @return SelectQueryBuilder
97
     */
98
    public static function make(...$columns): self
99
    {
100
        $select = new self;
101
        if (0 !== \func_num_args()) {
102
            $select->validateColumns(...$columns);
103
            $select->columns = $columns;
104
        }
105
        return $select;
106
    }
107
108
    /**
109
     * @param Expression[]|string[] ...$columns
110
     * @throws \InvalidArgumentException
111
     */
112
    private function validateColumns(...$columns)
113
    {
114
        foreach ($columns as $column) {
115
            if (!($column instanceof Expression || \is_scalar($column))) {
116
                throw new \InvalidArgumentException(
117
                    \sprintf(
118
                        "Expected string or Expression, got %s",
119
                        \is_object($column) ? \get_class($column) : \gettype($column)
120
                    )
121
                );
122
            }
123
        }
124
    }
125
126
    /**
127
     * @param string $keyword
128
     * @return SelectQueryBuilder
129
     */
130
    public function withMainKeyword(string $keyword): self
131
    {
132
        $clone = clone $this;
133
        $clone->mainKeyword = $keyword;
134
        return $clone;
135
    }
136
137
    /**
138
     * @param Expression[]|string[] ...$columns
139
     * @return SelectQueryBuilder
140
     */
141
    public function withColumns(...$columns): self
142
    {
143
        $this->validateColumns(...$columns);
144
        $clone = clone $this;
145
        $clone->columns = $columns;
146
        return $clone;
147
    }
148
149
    /**
150
     * @param Expression[]|string[] ...$columns
151
     * @return SelectQueryBuilder
152
     */
153
    public function withAddedColumns(...$columns): self
154
    {
155
        $this->validateColumns(...$columns);
156
        $clone = clone $this;
157
        $clone->columns = \array_merge($clone->columns, $columns);
158
        return $clone;
159
    }
160
161
    /**
162
     * @param string[] ...$flags
163
     * @return SelectQueryBuilder
164
     */
165
    public function withFlags(string ...$flags): self
166
    {
167
        $clone = clone $this;
168
        $clone->flags = $flags;
169
        return $clone;
170
    }
171
172
    /**
173
     * @param string[] ...$flags
174
     * @return SelectQueryBuilder
175
     */
176
    public function withAddedFlags(string ...$flags): self
177
    {
178
        $clone = clone $this;
179
        $existingFlags = \array_map('strtoupper', $clone->flags);
180
        foreach ($flags as $flag) {
181
            if (!\in_array(\strtoupper($flag), $existingFlags, true)) {
182
                $clone->flags[] = $flag;
183
            }
184
        }
185
        return $clone;
186
    }
187
188
    /**
189
     * @param bool $distinct
190
     * @return SelectQueryBuilder
191
     */
192
    public function distinct(bool $distinct = true): self
193
    {
194
        $clone = clone $this;
195
        $clone->distinct = $distinct;
196
        return $clone;
197
    }
198
199
    /**
200
     * @param string $table
201
     * @return SelectQueryBuilder
202
     */
203
    public function from(string $table = null): self
204
    {
205
        $clone = clone $this;
206
        $clone->from = $table;
207
        return $clone;
208
    }
209
210
    /**
211
     * @param string                 $table
212
     * @param string|Expression|null $expression
213
     * @param array                  ...$values
214
     * @return SelectQueryBuilder
215
     * @throws \InvalidArgumentException
216
     */
217
    public function join(string $table, $expression = null, ...$values): self
218
    {
219
        $clone = clone $this;
220
        $clone->joins[$table] = [
221
            't' => 'JOIN',
222
            'c' => null !== $expression ? Expression::where($expression, ...$values) : null,
223
        ];
224
        return $clone;
225
    }
226
227
    /**
228
     * @param string                 $table
229
     * @param string|Expression|null $expression
230
     * @param array                  ...$values
231
     * @return SelectQueryBuilder
232
     * @throws \InvalidArgumentException
233
     */
234
    public function innerJoin(string $table, $expression = null, ...$values): self
235
    {
236
        $clone = clone $this;
237
        $clone->joins[$table] = [
238
            't' => 'INNER JOIN',
239
            'c' => null !== $expression ? Expression::where($expression, ...$values) : null,
240
        ];
241
        return $clone;
242
    }
243
244
    /**
245
     * @param string                 $table
246
     * @param string|Expression|null $expression
247
     * @param array                  ...$values
248
     * @return SelectQueryBuilder
249
     * @throws \InvalidArgumentException
250
     */
251
    public function outerJoin(string $table, $expression = null, ...$values): self
252
    {
253
        $clone = clone $this;
254
        $clone->joins[$table] = [
255
            't' => 'OUTER JOIN',
256
            'c' => null !== $expression ? Expression::where($expression, ...$values) : null,
257
        ];
258
        return $clone;
259
    }
260
261
    /**
262
     * @param string                 $table
263
     * @param string|Expression|null $expression
264
     * @param array                  ...$values
265
     * @return SelectQueryBuilder
266
     * @throws \InvalidArgumentException
267
     */
268
    public function leftJoin(string $table, $expression = null, ...$values): self
269
    {
270
        $clone = clone $this;
271
        $clone->joins[$table] = [
272
            't' => 'LEFT JOIN',
273
            'c' => null !== $expression ? Expression::where($expression, ...$values) : null,
274
        ];
275
        return $clone;
276
    }
277
278
    /**
279
     * @param string                 $table
280
     * @param string|Expression|null $expression
281
     * @param array                  ...$values
282
     * @return SelectQueryBuilder
283
     * @throws \InvalidArgumentException
284
     */
285
    public function leftOuterJoin(string $table, $expression = null, ...$values): self
286
    {
287
        $clone = clone $this;
288
        $clone->joins[$table] = [
289
            't' => 'LEFT OUTER JOIN',
290
            'c' => null !== $expression ? Expression::where($expression, ...$values) : null,
291
        ];
292
        return $clone;
293
    }
294
295
    /**
296
     * @param string                 $table
297
     * @param string|Expression|null $expression
298
     * @param array                  ...$values
299
     * @return SelectQueryBuilder
300
     * @throws \InvalidArgumentException
301
     */
302
    public function rightJoin(string $table, $expression = null, ...$values): self
303
    {
304
        $clone = clone $this;
305
        $clone->joins[$table] = [
306
            't' => 'RIGHT JOIN',
307
            'c' => null !== $expression ? Expression::where($expression, ...$values) : null,
308
        ];
309
        return $clone;
310
    }
311
312
    /**
313
     * @param string                 $table
314
     * @param string|Expression|null $expression
315
     * @param array                  ...$values
316
     * @return SelectQueryBuilder
317
     * @throws \InvalidArgumentException
318
     */
319
    public function rightOuterJoin(string $table, $expression = null, ...$values): self
320
    {
321
        $clone = clone $this;
322
        $clone->joins[$table] = [
323
            't' => 'RIGHT OUTER JOIN',
324
            'c' => null !== $expression ? Expression::where($expression, ...$values) : null,
325
        ];
326
        return $clone;
327
    }
328
329
    /**
330
     * @param string                 $table
331
     * @param string|Expression|null $expression
332
     * @param array                  ...$values
333
     * @return SelectQueryBuilder
334
     * @throws \InvalidArgumentException
335
     */
336
    public function fullJoin(string $table, $expression = null, ...$values): self
337
    {
338
        $clone = clone $this;
339
        $clone->joins[$table] = [
340
            't' => 'FULL JOIN',
341
            'c' => null !== $expression ? Expression::where($expression, ...$values) : null,
342
        ];
343
        return $clone;
344
    }
345
346
    /**
347
     * @param string                 $table
348
     * @param string|Expression|null $expression
349
     * @param array                  ...$values
350
     * @return SelectQueryBuilder
351
     * @throws \InvalidArgumentException
352
     */
353
    public function fullOuterJoin(string $table, $expression = null, ...$values): self
354
    {
355
        $clone = clone $this;
356
        $clone->joins[$table] = [
357
            't' => 'FULL OUTER JOIN',
358
            'c' => null !== $expression ? Expression::where($expression, ...$values) : null,
359
        ];
360
        return $clone;
361
    }
362
363
    /**
364
     * Reset all JOIN clauses.
365
     *
366
     * @return SelectQueryBuilder
367
     */
368
    public function resetJoins(): self
369
    {
370
        $clone = clone $this;
371
        $clone->joins = [];
372
        return $clone;
373
    }
374
375
    /**
376
     * Remove a specific JOIN clause.
377
     *
378
     * @param string $table
379
     * @return SelectQueryBuilder
380
     */
381
    public function withoutJoin(string $table)
382
    {
383
        $clone = clone $this;
384
        unset($clone->joins[$table]);
385
        return $clone;
386
    }
387
388
    /**
389
     * @param string|Expression|null $expression
390
     * @param array                  ...$values
391
     * @return SelectQueryBuilder
392
     * @throws \InvalidArgumentException
393
     */
394
    public function where($expression = null, ...$values): self
395
    {
396
        $clone = clone $this;
397
        if (1 === \func_num_args() && null === \func_get_arg(0)) {
398
            $clone->where = null;
399
            return $clone;
400
        }
401
        $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\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...
402
        return $clone;
403
    }
404
405
    /**
406
     * @param string|Expression $expression
407
     * @param array             ...$values
408
     * @return SelectQueryBuilder
409
     * @throws \InvalidArgumentException
410
     */
411
    public function andWhere($expression, ...$values): self
412
    {
413
        if (null === $this->where) {
414
            return $this->where(Expression::where($expression, ...$values));
415
        }
416
        $clone = clone $this;
417
        $clone->where = $clone->where->and(Expression::where($expression, ...$values));
418
        return $clone;
419
    }
420
421
    /**
422
     * @param string|Expression $expression
423
     * @param array             ...$values
424
     * @return SelectQueryBuilder
425
     * @throws \InvalidArgumentException
426
     */
427
    public function orWhere($expression, ...$values): self
428
    {
429
        if (null === $this->where) {
430
            return $this->where(Expression::where($expression, ...$values));
431
        }
432
        $clone = clone $this;
433
        $clone->where = $clone->where->or(Expression::where($expression, ...$values));
434
        return $clone;
435
    }
436
437
    /**
438
     * @param string[] ...$groupBy
439
     * @return SelectQueryBuilder
440
     */
441
    public function groupBy(?string ...$groupBy): self
442
    {
443
        $clone = clone $this;
444
        if (1 === \func_num_args() && null === \func_get_arg(0)) {
445
            $clone->groupBy = [];
446
            return $clone;
447
        }
448
        $clone->groupBy = $groupBy;
449
        return $clone;
450
    }
451
452
    /**
453
     * @param string[] ...$groupBy
454
     * @return SelectQueryBuilder
455
     */
456
    public function andGroupBy(string ...$groupBy): self
457
    {
458
        $clone = clone $this;
459
        $clone->groupBy = \array_merge($clone->groupBy, $groupBy);
460
        return $clone;
461
    }
462
463
    /**
464
     * @param string|Expression|null $expression
465
     * @param array                  ...$values
466
     * @return SelectQueryBuilder
467
     * @throws \InvalidArgumentException
468
     */
469
    public function having($expression = null, ...$values): self
470
    {
471
        $clone = clone $this;
472
        if (1 === \func_num_args() && null === \func_get_arg(0)) {
473
            $clone->having = null;
474
            return $clone;
475
        }
476
        $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\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...
477
        return $clone;
478
    }
479
480
    /**
481
     * @param string|Expression $expression
482
     * @param array             ...$values
483
     * @return SelectQueryBuilder
484
     * @throws \InvalidArgumentException
485
     */
486
    public function andHaving($expression, ...$values): self
487
    {
488
        if (null === $this->having) {
489
            return $this->having(Expression::where($expression, ...$values));
490
        }
491
        $clone = clone $this;
492
        $clone->having = $clone->having->and(Expression::where($expression, ...$values));
493
        return $clone;
494
    }
495
496
    /**
497
     * @param string|Expression $expression
498
     * @param array             ...$values
499
     * @return SelectQueryBuilder
500
     * @throws \InvalidArgumentException
501
     */
502
    public function orHaving($expression, ...$values): self
503
    {
504
        if (null === $this->having) {
505
            return $this->having(Expression::where($expression, ...$values));
506
        }
507
        $clone = clone $this;
508
        $clone->having = $clone->having->or(Expression::where($expression, ...$values));
509
        return $clone;
510
    }
511
512
    /**
513
     * @param string[] ...$groupBy
514
     * @return SelectQueryBuilder
515
     */
516
    public function orderBy(?string ...$orderBy): self
517
    {
518
        $clone = clone $this;
519
        if (1 === \func_num_args() && null === \func_get_arg(0)) {
520
            $clone->orderBy = [];
521
            return $clone;
522
        }
523
        $clone->orderBy = $orderBy;
524
        return $clone;
525
    }
526
527
    /**
528
     * @param string[] ...$groupBy
529
     * @return SelectQueryBuilder
530
     */
531
    public function andOrderBy(string ...$orderBy): self
532
    {
533
        $clone = clone $this;
534
        $clone->orderBy = \array_merge($clone->orderBy, $orderBy);
535
        return $clone;
536
    }
537
538
    /**
539
     * @param int|null $limit
540
     * @return SelectQueryBuilder
541
     */
542
    public function limit(int $limit = null): self
543
    {
544
        $clone = clone $this;
545
        $clone->limit = $limit;
546
        return $clone;
547
    }
548
549
    /**
550
     * @param int|null $offset
551
     * @return SelectQueryBuilder
552
     */
553
    public function offset(int $offset = null): self
554
    {
555
        $clone = clone $this;
556
        $clone->offset = $offset;
557
        return $clone;
558
    }
559
560
    /**
561
     * @param string|null $end
562
     * @return SelectQueryBuilder
563
     */
564
    public function end(string $end = null): self
565
    {
566
        $clone = clone $this;
567
        $clone->end = $end;
568
        return $clone;
569
    }
570
571
    /**
572
     * Split current query into multiple sub-queries (with OFFSET and LIMIT).
573
     *
574
     * @param int $buffer
575
     * @param int $max
576
     * @param int $offsetStart
577
     * @return iterable|self[]
578
     */
579
    public function split(int $buffer, int $max, int $offsetStart = 0): iterable
580
    {
581
        for ($offset = $offsetStart; $offset < $max; $offset += $buffer) {
582
            yield $offset => $this->offset($offset)->limit($buffer);
583
        }
584
    }
585
586
    /**
587
     * @return string
588
     */
589
    public function __toString(): string
590
    {
591
        return SelectQueryStringifier::stringify($this);
592
    }
593
594
    /**
595
     * @return array
596
     */
597
    public function getValues(): array
598
    {
599
        $expressions = \array_filter(\array_merge($this->columns, \array_column($this->joins, 'c'), [$this->where, $this->having]), function ($expression) {
600
            return $expression instanceof Expression;
601
        });
602
        return valuesOf(...$expressions);
603
    }
604
605
    /**
606
     * @return string
607
     */
608
    public function preview(): string
609
    {
610
        return Previewer::preview((string) $this, $this->getValues());
611
    }
612
613
    /**
614
     * Read-only properties.
615
     *
616
     * @param $property
617
     * @return mixed
618
     * @throws \InvalidArgumentException
619
     */
620
    public function __get($property)
621
    {
622
        if (!\property_exists($this, $property)) {
623
            throw new \InvalidArgumentException(\sprintf('Property %s::$%s does not exist.', __CLASS__, $property));
624
        }
625
        return $this->{$property};
626
    }
627
}
628