Completed
Push — master ( 5fd544...e852d0 )
by BENOIT
01:49
created

SelectQueryBuilder::validateColumns()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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