Grammar::wrap()   B
last analyzed

Complexity

Conditions 7
Paths 6

Size

Total Lines 35
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 15
c 1
b 0
f 0
nc 6
nop 2
dl 0
loc 35
rs 8.8333
1
<?php
2
/**
3
 * Framy Framework
4
 *
5
 * @copyright Copyright Framy
6
 * @Author  Marco Bier <[email protected]>
7
 */
8
9
namespace app\framework\Component\Database\Query\Grammars;
10
11
use app\framework\Component\Database\Query\Builder;
12
use app\framework\Component\Database\Query\Expression;
13
14
class Grammar
15
{
16
    /**
17
     * The grammar table prefix.
18
     *
19
     * @var string
20
     */
21
    protected $tablePrefix = '';
22
23
    /**
24
     * The components that make up a select clause.
25
     *
26
     * @var array
27
     */
28
    protected $selectComponents = [
29
        'aggregate',
30
        'columns',
31
        'from',
32
        'joins',
33
        'wheres',
34
        'groups',
35
        'havings',
36
        'orders',
37
        'limit',
38
        'offset',
39
        'unions',
40
        'lock',
41
    ];
42
43
    /**
44
     * Compile a select query into SQL.
45
     *
46
     * @param  Builder $query
47
     * @return string
48
     */
49
    public function compileSelect(Builder $query)
50
    {
51
        // If the query does not have any columns set, we'll set the columns to the
52
        // * character to just get all of the columns from the database. Then we
53
        // can build the query and concatenate all the pieces together as one.
54
        $original = $query->columns;
55
56
        if (is_null($query->columns)) {
0 ignored issues
show
introduced by
The condition is_null($query->columns) is always false.
Loading history...
57
            $query->columns = ['*'];
58
        }
59
60
        // To compile the query, we'll spin through each component of the query and
61
        // see if that component exists. If it does we'll just call the compiler
62
        // function for the component which is responsible for making the SQL.
63
        $sql = trim($this->concatenate(
64
            $this->compileComponents($query))
65
        );
66
67
        $query->columns = $original;
68
69
        return $sql;
70
    }
71
72
    /**
73
     * Compile an insert and get ID statement into SQL.
74
     *
75
     * @param  Builder  $query
76
     * @param  array   $values
77
     * @return string
78
     */
79
    public function compileInsertGetId(Builder $query, $values)
80
    {
81
        return $this->compileInsert($query, $values);
82
    }
83
84
    /**
85
     * Compile a insert query into SQL
86
     *
87
     * @param Builder $query
88
     * @param array $values
89
     * @return string
90
     */
91
    public function compileInsert(Builder $query, array $values): string
92
    {
93
        // Essentially we will force every insert to be treated as a batch insert which
94
        // simply makes creating the SQL easier for us since we can utilize the same
95
        // basic routine regardless of an amount of records given to us to insert.
96
        $table = $this->wrapTable($query->from);
97
98
        if (! is_array(reset($values))) {
99
            $values = [$values];
100
        }
101
102
        $columns = $this->columnize(array_keys(reset($values)));
103
104
        // We need to build a list of parameter place-holders of values that are bound
105
        // to the query. Each insert should have the exact same amount of parameter
106
        // bindings so we will loop through the record and parameterize them all.
107
        $parameters = [];
108
109
        foreach ($values as $record) {
110
            $parameters[] = '('.$this->parameterize($record).')';
111
        }
112
113
        $parameters = implode(', ', $parameters);
114
115
        return "insert into $table ($columns) values $parameters";
116
    }
117
118
    /**
119
     * Compile a update query into SQL
120
     *
121
     * @param Builder $query
122
     * @param array $values
123
     * @return string
124
     */
125
    public function compileUpdate(Builder $query, array $values): string
126
    {
127
        $table = $this->wrapTable($query->from);
128
129
        // Each one of the columns in the update statements needs to be wrapped in the
130
        // keyword identifiers, also a place-holder needs to be created for each of
131
        // the values in the list of bindings so we can make the sets statements.
132
        $columns = [];
133
134
        foreach ($values as $key => $value) {
135
            $columns[] = $this->wrap($key)." = ?";
136
        }
137
138
        $columns = implode(', ', $columns);
139
140
        // Of course, update queries may also be constrained by where clauses so we'll
141
        // need to compile the where clauses and attach it to the query so only the
142
        // intended records are updated by the SQL statements we generate to run.
143
        $where = $this->compileWheres($query);
144
145
        return "update {$table} set $columns $where";
146
    }
147
148
    /**
149
     * Compile a delete statement into SQL.
150
     *
151
     * @param  Builder  $query
152
     * @return string
153
     */
154
    public function compileDelete(Builder $query)
155
    {
156
        $table = $this->wrapTable($query->from);
157
158
        $where = is_array($query->wheres) ? $this->compileWheres($query) : '';
0 ignored issues
show
introduced by
The condition is_array($query->wheres) is always true.
Loading history...
159
160
        return trim("delete from $table ".$where);
161
    }
162
163
    /**
164
     * Compile a truncate table statement into SQL.
165
     *
166
     * @param  Builder  $query
167
     * @return array
168
     */
169
    public function compileTruncate(Builder $query)
170
    {
171
        return ['truncate '.$this->wrapTable($query->from) => []];
172
    }
173
174
    /**
175
     * Compile the components necessary for a select clause.
176
     *
177
     * @param  Builder $query
178
     * @return array
179
     */
180
    protected function compileComponents(Builder $query)
181
    {
182
        $sql = [];
183
184
        foreach ($this->selectComponents as $component) {
185
            // To compile the query, we'll spin through each component of the query and
186
            // see if that component exists. If it does we'll just call the compiler
187
            // function for the component which is responsible for making the SQL.
188
            if (!is_null($query->$component)) {
189
                $method = 'compile' . ucfirst($component);
190
191
                $sql[$component] = $this->$method($query, $query->$component);
192
            }
193
        }
194
195
        return $sql;
196
    }
197
198
    /**
199
     * Compile the "select *" portion of the query.
200
     *
201
     * @param  Builder $query
202
     * @param  array $columns
203
     * @return string|null
204
     */
205
    protected function compileColumns(Builder $query, $columns)
206
    {
207
        // If the query is actually performing an aggregating select, we will let that
208
        // compiler handle the building of the select clauses, as it will need some
209
        // more syntax that is best handled by that function to keep things neat.
210
        if (!is_null($query->aggregate)) {
0 ignored issues
show
introduced by
The condition is_null($query->aggregate) is always false.
Loading history...
211
            return null;
212
        }
213
214
        $select = $query->distinct ? 'select distinct ' : 'select ';
215
216
        return $select . $this->columnize($columns);
217
    }
218
219
    /**
220
     * Compile the "from" portion of the query.
221
     *
222
     * @param  Builder $query
223
     * @param  string $table
224
     * @return string
225
     */
226
    protected function compileFrom(Builder $query, $table)
227
    {
228
        return 'from ' . $this->wrapTable($table);
229
    }
230
231
    /**
232
     * Compile the "where" portions of the query.
233
     *
234
     * @param  Builder  $query
235
     * @return string
236
     */
237
    protected function compileWheres(Builder $query)
238
    {
239
        $sql = [];
240
241
        if (is_null($query->wheres)) {
0 ignored issues
show
introduced by
The condition is_null($query->wheres) is always false.
Loading history...
242
            return '';
243
        }
244
245
        // Each type of where clauses has its own compiler function which is responsible
246
        // for actually creating the where clauses SQL. This helps keep the code nice
247
        // and maintainable since each clause has a very small method that it uses.
248
        foreach ($query->wheres as $where) {
249
            $method = "where{$where['type']}";
250
251
            $sql[] = $where['boolean'].' '.$this->$method($query, $where);
252
        }
253
254
        // If we actually have some where clauses, we will strip off the first boolean
255
        // operator, which is added by the query builders for convenience so we can
256
        // avoid checking for the first clauses in each of the compilers methods.
257
        if (count($sql) > 0) {
258
            $sql = implode(' ', $sql);
259
260
            return 'where '.$this->removeLeadingBoolean($sql);
261
        }
262
263
        return '';
264
    }
265
266
    /**
267
     * Compile the "order by" portions of the query.
268
     *
269
     * @param  Builder  $query
270
     * @param  array  $orders
271
     * @return string
272
     */
273
    protected function compileOrders(Builder $query, $orders)
274
    {
275
        return 'order by '.implode(', ', $orders[0])." ".$orders[1];
276
    }
277
278
    /**
279
     * Compile the "limit" portions of the query.
280
     *
281
     * @param  Builder  $query
282
     * @param  int  $limit
283
     * @return string
284
     */
285
    protected function compileLimit(Builder $query, $limit)
286
    {
287
        return 'limit '.(int) $limit;
288
    }
289
290
    /**
291
     * Compile an aggregated select clause.
292
     *
293
     * @param  Builder  $query
294
     * @param  array  $aggregate
295
     * @return string
296
     */
297
    protected function compileAggregate(Builder $query, $aggregate)
298
    {
299
        $column = $this->columnize($aggregate['columns']);
300
301
        // If the query has a "distinct" constraint and we're not asking for all columns
302
        // we need to prepend "distinct" onto the column name so that the query takes
303
        // it into account when it performs the aggregating operations on the data.
304
        if ($query->distinct && $column !== '*') {
305
            $column = 'distinct '.$column;
306
        }
307
308
        return 'select '.$aggregate['function'].'('.$column.') as aggregate';
309
    }
310
311
    /**
312
     * Compile an exists statement into SQL.
313
     *
314
     * @param Builder $query
315
     * @return string
316
     */
317
    public function compileExists(Builder $query)
318
    {
319
        $select = $this->compileSelect($query);
320
321
        return "select exists($select) as {$this->wrap('exists')}";
322
    }
323
324
    public function compileOffset(Builder $query)
325
    {
326
        return "offset ".$query->offset;
327
    }
328
329
    /**
330
     * Compile a basic where clause.
331
     * TODO: probably only temporarily so figure out if this is fine
332
     *
333
     * @param  Builder  $query
334
     * @param  array  $where
335
     * @return string
336
     */
337
    protected function whereBasic(Builder $query, $where)
338
    {
339
//        $value = $this->parameter($where['value']);
340
        return $this->wrap($where['column']).' '.$where['operator'].' ?'; //.$value;
341
    }
342
343
    /**
344
     * Compile a "between" where clause.
345
     *
346
     * @param  Builder  $query
347
     * @param  array  $where
348
     * @return string
349
     */
350
    protected function whereBetween(Builder $query, $where)
351
    {
352
        $between = $where['not'] ? 'not between' : 'between';
353
354
        return $this->wrap($where['column']).' '.$between.' ? and ?';
355
    }
356
357
    /**
358
     * Compile a "in" where clause.
359
     *
360
     * @param Builder $query
361
     * @param $where
362
     * @return string
363
     */
364
    protected function whereIn(Builder $query, $where)
365
    {
366
        $in = $where['not'] ? 'not in' : 'in';
367
368
        $values = " (";
369
370
        $count = count($query->getBindings());
371
        for ($i = 1; $i <= $count; $i++) {
372
            $append = ($count >  $i) ? "," : "";
373
            $values .= "?" . $append;
374
        }
375
        $values .= ")";
376
377
        return $this->wrap($where['column']). ' '.$in.$values;
378
    }
379
380
    /**
381
     * Compile a "null" where clause.
382
     *
383
     * @param Builder $query
384
     * @param $where
385
     * @return string
386
     */
387
    protected function whereNull(Builder $query, $where)
388
    {
389
        $null = $where['not'] ? 'is not null' : 'is null';
390
391
        return $this->wrap($where['column']). ' '.$null;
392
    }
393
394
    /**
395
     * Compile a "date" where clause.
396
     *
397
     * @param Builder $builder
398
     * @param $where
399
     * @return string
400
     */
401
    protected function whereDate(Builder $builder, $where)
402
    {
403
        return $this->wrap($where['column']).$where['operator'].$where['value'];
404
    }
405
406
    /**
407
     * Compile a "date year" where clause.
408
     *
409
     * @param Builder $builder
410
     * @param $where
411
     * @return string
412
     */
413
    protected function whereYear(Builder $builder, $where)
414
    {
415
        return "year(".$this->wrap($where['column']).")".$where['operator'].$where['value'];
416
    }
417
418
    /**
419
     * Compile a "date month" where clause.
420
     *
421
     * @param Builder $builder
422
     * @param $where
423
     * @return string
424
     */
425
    protected function whereMonth(Builder $builder, $where)
426
    {
427
        return "month(".$this->wrap($where['column']).")".$where['operator'].$where['value'];
428
    }
429
430
    /**
431
     * Compile a "date day" where clause.
432
     *
433
     * @param Builder $builder
434
     * @param $where
435
     * @return string
436
     */
437
    protected function whereDay(Builder $builder, $where)
438
    {
439
        return "day(".$this->wrap($where['column']).")".$where['operator'].$where['value'];
440
    }
441
442
    /**
443
     * Concatenate an array of segments, removing empties.
444
     *
445
     * @param  array $segments
446
     * @return string
447
     */
448
    protected function concatenate($segments)
449
    {
450
        return implode(' ', array_filter($segments, function ($value) {
451
            return (string)$value !== '';
452
        }));
453
    }
454
455
    /**
456
     * Convert an array of column names into a delimited string.
457
     *
458
     * @param  array $columns
459
     * @return string
460
     */
461
    public function columnize(array $columns)
462
    {
463
        return implode(', ', array_map([$this, 'wrap'], $columns));
464
    }
465
466
    /**
467
     * Create query parameter place-holders for an array.
468
     *
469
     * @param  array   $values
470
     * @return string
471
     */
472
    public function parameterize(array $values)
473
    {
474
        $val = array_map([$this, 'parameter'], $values);
475
476
        foreach ($val as $key => $item) {
477
            $val[$key] = $this->quoteString($item);
478
        }
479
480
        return implode(', ', $val);
481
    }
482
483
    public function parameterizeForUpdate(array $columns, array $values)
484
    {
485
        $queryValues = [];
486
487
        $count = count($values);
488
        for ($i = 0; $i < $count; $i++) {
489
            $queryValues[] = $columns[$i]."=".$this->quoteString(array_values($values)[$i]);
490
        }
491
492
        return implode(", ", $queryValues);
493
    }
494
495
    /**
496
     * Get the appropriate query parameter place-holder for a value.
497
     *
498
     * @param  mixed   $value
499
     * @return string
500
     */
501
    public function parameter($value)
502
    {
503
        return $this->isExpression($value) ? $this->getValue($value) : $value;
504
    }
505
506
    /**
507
     * Quote the given string literal.
508
     *
509
     * @param  string|array  $value
510
     * @return string
511
     */
512
    public function quoteString($value)
513
    {
514
        if (is_array($value)) {
515
            return implode(', ', array_map([$this, __FUNCTION__], $value));
516
        }
517
518
        return "'$value'";
519
    }
520
521
    /**
522
     * Wrap a table in keyword identifiers.
523
     *
524
     * @param  Expression|string $table
525
     * @return string
526
     */
527
    public function wrapTable($table)
528
    {
529
        if (!$this->isExpression($table)) {
530
            return $this->wrap($this->tablePrefix . $table, true);
531
        }
532
533
        return $this->getValue($table);
0 ignored issues
show
Bug introduced by
It seems like $table can also be of type string; however, parameter $expression of app\framework\Component\...ars\Grammar::getValue() does only seem to accept app\framework\Component\Database\Query\Expression, maybe add an additional type check? ( Ignorable by Annotation )

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

533
        return $this->getValue(/** @scrutinizer ignore-type */ $table);
Loading history...
534
    }
535
536
    /**
537
     * Wrap a value in keyword identifiers.
538
     *
539
     * @param  Expression|string $value
540
     * @param  bool $prefixAlias
541
     * @return string
542
     */
543
    public function wrap($value, $prefixAlias = false)
544
    {
545
        if ($this->isExpression($value)) {
546
            return $this->getValue($value);
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type string; however, parameter $expression of app\framework\Component\...ars\Grammar::getValue() does only seem to accept app\framework\Component\Database\Query\Expression, maybe add an additional type check? ( Ignorable by Annotation )

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

546
            return $this->getValue(/** @scrutinizer ignore-type */ $value);
Loading history...
547
        }
548
549
        // If the value being wrapped has a column alias we will need to separate out
550
        // the pieces so we can wrap each of the segments of the expression on it
551
        // own, and then joins them both back together with the "as" connector.
552
        if (strpos(strtolower($value), ' as ') !== false) {
553
            $segments = explode(' ', $value);
554
555
            if ($prefixAlias) {
556
                $segments[2] = $this->tablePrefix.$segments[2];
557
            }
558
559
            return $this->wrap($segments[0]).' as '.$this->wrapValue($segments[2]);
560
        }
561
562
        $wrapped = [];
563
564
        $segments = explode('.', $value);
565
566
        // If the value is not an aliased table expression, we'll just wrap it like
567
        // normal, so if there is more than one segment, we will wrap the first
568
        // segments as if it was a table and the rest as just regular values.
569
        foreach ($segments as $key => $segment) {
570
            if ($key == 0 && count($segments) > 1) {
571
                $wrapped[] = $this->wrapTable($segment);
572
            } else {
573
                $wrapped[] = $this->wrapValue($segment);
574
            }
575
        }
576
577
        return implode('.', $wrapped);
578
    }
579
580
    /**
581
     * Wrap a value that has an alias.
582
     *
583
     * @param  string $value
584
     * @param  bool $prefixAlias
585
     * @return string
586
     */
587
    protected function wrapAliasedValue($value, $prefixAlias = false)
588
    {
589
        $segments = preg_split('/\s+as\s+/i', $value);
590
591
        // If we are wrapping a table we need to prefix the alias with the table prefix
592
        // as well in order to generate proper syntax. If this is a column of course
593
        // no prefix is necessary. The condition will be true when from wrapTable.
594
        if ($prefixAlias) {
595
            $segments[1] = $this->tablePrefix . $segments[1];
596
        }
597
598
        return $this->wrap(
599
                $segments[0]) . ' as ' . $this->wrapValue($segments[1]
600
            );
601
    }
602
603
    /**
604
     * Wrap the given value segments.
605
     *
606
     * @param  array $segments
607
     * @return string
608
     */
609
    protected function wrapSegments($segments)
610
    {
611
        return arr($segments)->map(function ($segment, $key) use ($segments) {
612
            return $key == 0 && count($segments) > 1
613
                ? $this->wrapTable($segment)
614
                : $this->wrapValue($segment);
615
        })->implode('.');
616
    }
617
618
    /**
619
     * Wrap a single string in keyword identifiers.
620
     *
621
     * @param  string $value
622
     * @return string
623
     */
624
    protected function wrapValue($value)
625
    {
626
        if ($value !== '*') {
627
            return '`' . str_replace('"', '""', $value) . '`';
628
        }
629
630
        return $value;
631
    }
632
633
    /**
634
     * Get the value of a raw expression.
635
     *
636
     * @param  Expression $expression
637
     * @return string
638
     */
639
    public function getValue($expression)
640
    {
641
        return $expression->getValue();
642
    }
643
644
    /**
645
     * Determine if the given value is a raw expression.
646
     *
647
     * @param  mixed $value
648
     * @return bool
649
     */
650
    public function isExpression($value)
651
    {
652
        return $value instanceof Expression;
653
    }
654
655
    /**
656
     * Remove the leading boolean from a statement.
657
     *
658
     * @param  string  $value
659
     * @return string
660
     */
661
    protected function removeLeadingBoolean($value)
662
    {
663
        return preg_replace('/and |or /i', '', $value, 1);
664
    }
665
}
666