Test Failed
Pull Request — master (#4)
by
unknown
03:13
created

QueryBuilder::addSelect()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 1
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Donii Sergii <[email protected]>
4
 */
5
6
namespace sonrac\Arango\Query;
7
8
use ArangoDBClient\Exception;
9
use Illuminate\Contracts\Support\Arrayable;
10
use Illuminate\Database\Eloquent\Builder;
11
use Illuminate\Database\Query\Builder as IlluminateBuilder;
12
use Illuminate\Database\Query\Expression;
13
use Illuminate\Support\Arr;
14
use Illuminate\Support\Str;
15
use sonrac\Arango\Connection;
16
use sonrac\Arango\Query\Grammars\Grammar;
17
use function sonrac\Arango\Helpers\getEntityName;
18
use function sonrac\Arango\Helpers\getEntityNameFromColumn;
19
20
/**
21
 * Class QueryBuilder.
22
 *
23
 * @author  Donii Sergii <[email protected]>
24
 */
25
class QueryBuilder extends IlluminateBuilder
26
{
27
    /**
28
     * @var Grammar $grammar
29
     */
30
    public $grammar;
31
32
    public $bindings = [];
33
34
    public $operators = [
35
        '==',     //equality
36
        '!=',     //inequality
37
        '<',      //less than
38
        '<=',     //less or equal
39
        '>',      //greater than
40
        '>=',     //greater or equal
41
        'IN',     //test if a value is contained in an array
42
        'NOT IN', //test if a value is not contained in an array
43
        'LIKE',   //tests if a string value matches a pattern
44
        '=~',     //tests if a string value matches a regular expression
45
        '!~',     //tests if a string value does not match a regular expression
46
    ];
47
48
    /**
49
     * {@inheritdoc}
50
     */
51
    public function pluck($column, $key = null)
52
    {
53
        $column = $this->prepareColumn($column);
54
        if (!is_null($key)) {
55
            $key = $this->prepareColumn($key);
56
        }
57
        $results = $this->get(is_null($key) ? [$column] : [$column, $key]);
58
59
        // If the columns are qualified with a table or have an alias, we cannot use
60
        // those directly in the "pluck" operations since the results from the DB
61
        // are only keyed by the column itself. We'll strip the table out here.
62
        return $results->pluck(
63
            $this->stripTableForPluck($column),
64
            $this->stripTableForPluck($key)
65
        );
66
    }
67
68
    /**
69
     * {@inheritdoc}
70
     */
71
    public function addSelect($column)
72
    {
73
        $column = is_array($column) ? $column : func_get_args();
74
75
        $column = collect($column)->map(function ($column) {
76
            return $this->prepareColumn($column);
77
        })->toArray();
78
79
        $this->columns = array_merge((array) $this->columns, $column);
80
81
        return $this;
82
    }
83
84
    /**
85
     * {@inheritdoc}
86
     */
87
    public function join($table, $first, $operator = null, $second = null, $type = 'inner', $where = false)
88
    {
89
        $join = new JoinClause($this, $type, $table);
90
91
        // If the first "column" of the join is really a Closure instance the developer
92
        // is trying to build a join with a complex "on" clause containing more than
93
        // one condition, so we'll add the join and call a Closure with the query.
94
        if ($first instanceof \Closure) {
95
            call_user_func($first, $join);
96
97
            $this->joins[] = $join;
98
99
            $this->addBinding($join->getBindings(), 'join');
100
        }
101
102
        // If the column is simply a string, we can assume the join simply has a basic
103
        // "on" clause with a single condition. So we will just build the join with
104
        // this simple join clauses attached to it. There is not a join callback.
105
        else {
106
            $method = $where ? 'where' : 'on';
107
108
            $this->joins[] = $join->$method($first, $operator, $second);
109
110
            $this->addBinding($join->getBindings(), 'join');
111
        }
112
113
        //Move wheres from join to main query (arangoDB don't have "on" method)
114
        foreach ($join->wheres as $where) {
115
            $this->wheres[] = $where;
116
        }
117
118
        $join->wheres = [];
119
120
        return $this;
121
    }
122
123
    /**
124
     * {@inheritdoc}
125
     */
126
    public function orderBy($column, $direction = 'asc')
127
    {
128
        $column = $this->prepareColumn($column);
129
        $this->{$this->unions ? 'unionOrders' : 'orders'}[] = [
130
            'column' => $column,
131
            'direction' => strtolower($direction) == 'asc' ? 'asc' : 'desc',
132
        ];
133
134
        return $this;
135
    }
136
137
    /**
138
     * {@inheritdoc}
139
     */
140
    public function whereIn($column, $values, $boolean = 'and', $not = false)
141
    {
142
        $type = $not ? 'NotIn' : 'In';
143
144
        if ($values instanceof Builder) {
145
            $values = $values->getQuery();
146
        }
147
148
        // If the value is a query builder instance we will assume the developer wants to
149
        // look for any values that exists within this given query. So we will add the
150
        // query accordingly so that this query is properly executed when it is run.
151
        if ($values instanceof self) {
152
            return $this->whereInExistingQuery(
153
                $column, $values, $boolean, $not
154
            );
155
        }
156
157
        // If the value of the where in clause is actually a Closure, we will assume that
158
        // the developer is using a full sub-select for this "in" statement, and will
159
        // execute those Closures, then we can re-construct the entire sub-selects.
160
        if ($values instanceof \Closure) {
161
            return $this->whereInSub($column, $values, $boolean, $not);
162
        }
163
164
        // Next, if the value is Arrayable we need to cast it to its raw array form so we
165
        // have the underlying array value instead of an Arrayable object which is not
166
        // able to be added as a binding, etc. We will then add to the wheres array.
167
        if ($values instanceof Arrayable) {
168
            $values = $values->toArray();
169
        }
170
171
        // Finally we'll add a binding for each values unless that value is an expression
172
        // in which case we will just skip over it since it will be the query as a raw
173
        // string and not as a parameterized place-holder to be replaced by the PDO.
174 View Code Duplication
        foreach ($values as $index => $value) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
175
            if (!$value instanceof Expression) {
176
                $this->addBinding($value, 'where');
177
                $values[$index] = $this->getLastBindingKey();
178
            }
179
        }
180
181
        $this->wheres[] = compact('type', 'column', 'values', 'boolean');
182
183
        return $this;
184
    }
185
186
    /**
187
     * You can get last binding key from getLastBindingKey
188
     * {@inheritdoc}
189
     */
190
    public function addBinding($value, $type = 'where')
191
    {
192
        if (is_array($value)) {
193
            foreach ($value as $variable) {
194
                $this->bindings[$this->getBindingVariableName()] = $variable;
195
            }
196
        } else {
197
            $this->bindings[$this->getBindingVariableName()] = $value;
198
        }
199
200
        return $this;
201
    }
202
203
    /**
204
     * Return last binding key
205
     *
206
     * @return string
207
     */
208
    public function getLastBindingKey()
209
    {
210
        $keys = array_keys($this->getBindings());
211
        return '@' . array_pop($keys);
212
    }
213
214
    /**
215
     * {@inheritdoc}
216
     */
217
    public function getBindings()
218
    {
219
        return $this->bindings;
220
    }
221
222
    /**
223
     * {@inheritdoc}
224
     */
225
    public function whereBetween($column, array $values, $boolean = 'and', $not = false)
226
    {
227
        $this->where(function (QueryBuilder $query) use ($column, $values, $boolean, $not) {
228
            list($from, $to) = $values;
229
            if (!$not) {
230
                $query->where($column, '>', $from);
231
                $query->where($column, '<', $to);
232
            } else {
233
                $query->where($column, '<=', $from);
234
                $query->orWhere($column, '>=', $to);
235
            }
236
        }, $boolean);
237
238
        return $this;
239
    }
240
241
    /**
242
     * {@inheritdoc}
243
     */
244
    public function where($column, $operator = null, $value = null, $boolean = 'and')
245
    {
246
        $column = $this->prepareColumn($column);
247
248
        //For compatibility with internal framework functions
249
        if ($operator === '=') {
250
            $operator = '==';
251
        }
252
        // If the column is an array, we will assume it is an array of key-value pairs
253
        // and can add them each as a where clause. We will maintain the boolean we
254
        // received when the method was called and pass it into the nested where.
255
        if (is_array($column)) {
256
            return $this->addArrayOfWheres($column, $boolean);
257
        }
258
259
        // Here we will make some assumptions about the operator. If only 2 values are
260
        // passed to the method, we will assume that the operator is an equals sign
261
        // and keep going. Otherwise, we'll require the operator to be passed in.
262
        list($value, $operator) = $this->prepareValueAndOperator(
263
            $value, $operator, func_num_args() == 2
264
        );
265
266
        // If the columns is actually a Closure instance, we will assume the developer
267
        // wants to begin a nested where statement which is wrapped in parenthesis.
268
        // We'll add that Closure to the query then return back out immediately.
269
        if ($column instanceof \Closure) {
270
            return $this->whereNested($column, $boolean);
271
        }
272
273
        // If the given operator is not found in the list of valid operators we will
274
        // assume that the developer is just short-cutting the '=' operators and
275
        // we will set the operators to '=' and set the values appropriately.
276
        if ($this->invalidOperator($operator)) {
277
            list($value, $operator) = [$operator, '=='];
278
        }
279
280
        // If the value is a Closure, it means the developer is performing an entire
281
        // sub-select within the query and we will need to compile the sub-select
282
        // within the where clause to get the appropriate query record results.
283
        if ($value instanceof \Closure) {
284
            return $this->whereSub($column, $operator, $value, $boolean);
285
        }
286
287
        // If the value is "null", we will just assume the developer wants to add a
288
        // where null clause to the query. So, we will allow a short-cut here to
289
        // that method for convenience so the developer doesn't have to check.
290
        if (is_null($value)) {
291
            return $this->whereNull($column, $boolean, $operator !== '==');
292
        }
293
294
        // If the column is making a JSON reference we'll check to see if the value
295
        // is a boolean. If it is, we'll add the raw boolean string as an actual
296
        // value to the query to ensure this is properly handled by the query.
297
        if (Str::contains($column, '->') && is_bool($value)) {
298
            $value = new Expression($value ? 'true' : 'false');
299
        }
300
301
        // Now that we are working with just a simple query we can put the elements
302
        // in our array and add the query binding to our array of bindings that
303
        // will be bound to each SQL statements when it is finally executed.
304
        $type = 'Basic';
305
306
        if (!$value instanceof Expression) {
307
            $this->addBinding($value, 'where');
308
            $value = $this->getLastBindingKey();
309
        }
310
311
        $this->wheres[] = compact(
312
            'type', 'column', 'operator', 'value', 'boolean'
313
        );
314
315
        return $this;
316
    }
317
318
    /**
319
     * {@inheritdoc}
320
     */
321
    public function whereColumn($first, $operator = null, $second = null, $boolean = 'and')
322
    {
323
324
        if ($operator === '=') {
325
            $operator = '==';
326
        }
327
328
        // If the column is an array, we will assume it is an array of key-value pairs
329
        // and can add them each as a where clause. We will maintain the boolean we
330
        // received when the method was called and pass it into the nested where.
331
        if (is_array($first)) {
332
            return $this->addArrayOfWheres($first, $boolean, 'whereColumn');
333
        }
334
335
        // If the given operator is not found in the list of valid operators we will
336
        // assume that the developer is just short-cutting the '=' operators and
337
        // we will set the operators to '=' and set the values appropriately.
338
        if ($this->invalidOperator($operator)) {
339
            list($second, $operator) = [$operator, '=='];
340
        }
341
342
        // Finally, we will add this where clause into this array of clauses that we
343
        // are building for the query. All of them will be compiled via a grammar
344
        // once the query is about to be executed and run against the database.
345
        $type = 'Column';
346
347
        $this->wheres[] = compact(
348
            'type', 'first', 'operator', 'second', 'boolean'
349
        );
350
351
        return $this;
352
    }
353
354
    /**
355
     * {@inheritdoc}
356
     */
357
    public function whereNull($column, $boolean = 'and', $not = false)
358
    {
359
        $column = $this->prepareColumn($column);
360
361
        $type = $not ? 'NotNull' : 'Null';
362
363
        $this->wheres[] = compact('type', 'column', 'boolean');
364
365
        return $this;
366
    }
367
368
    /**
369
     * {@inheritdoc}
370
     */
371 View Code Duplication
    public function increment($column, $amount = 1, array $extra = [])
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
372
    {
373
        if (!is_numeric($amount)) {
374
            throw new \InvalidArgumentException('Non-numeric value passed to increment method.');
375
        }
376
377
        $wrapped = $this->prepareColumn($column);
378
379
        $columns = array_merge([$column => $this->raw("$wrapped + $amount")], $extra);
380
381
        return $this->update($columns);
382
    }
383
384
    /**
385
     * {@inheritdoc}
386
     */
387 View Code Duplication
    public function decrement($column, $amount = 1, array $extra = [])
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
388
    {
389
        if (!is_numeric($amount)) {
390
            throw new \InvalidArgumentException('Non-numeric value passed to decrement method.');
391
        }
392
393
        $wrapped = $this->prepareColumn($column);
394
395
        $columns = array_merge([$column => $this->raw("$wrapped - $amount")], $extra);
396
397
        return $this->update($columns);
398
    }
399
400
    /**
401
     * {@inheritdoc}
402
     */
403
    public function update(array $values)
404
    {
405
406
407 View Code Duplication
        foreach ($values as $index => $value) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
408
            if (!$value instanceof Expression) {
409
                $this->addBinding($value, 'update');
410
                $values[$index] = $this->getLastBindingKey();
411
            }
412
        }
413
414
        $aql = $this->grammar->compileUpdate($this, $values);
415
416
        return $this->connection->update($aql, $this->getBindings());
417
    }
418
419
    /**
420
     * {@inheritdoc}
421
     */
422
    public function delete($id = null)
423
    {
424
        // If an ID is passed to the method, we will set the where clause to check the
425
        // ID to let developers to simply and quickly remove a single row from this
426
        // database without manually specifying the "where" clauses on the query.
427
        if (!is_null($id)) {
428
            $this->where($this->from.'.id', '=', $id);
429
        }
430
431
        return $this->connection->delete(
432
            $this->grammar->compileDelete($this), $this->getBindings()
433
        );
434
    }
435
436
    /**
437
     * {@inheritdoc}
438
     */
439
    public function truncate()
440
    {
441
        $connection = $this->getConnection();
442
        /**
443
         * @var Connection $connection
444
         */
445
        $arangoDB = $connection->getArangoDB();
446
        $arangoDB->truncate($this->from);
447
    }
448
449
    /**
450
     * {@inheritdoc}
451
     */
452
    public function find($id, $columns = ['*'])
453
    {
454
        $column = $this->prepareColumn('_key');
455
        return $this->where($column, '==', $id)->limit(1)->first($columns);
456
    }
457
458
    /**
459
     * {@inheritdoc}
460
     */
461
    public function insertGetId(array $values, $sequence = null)
462
    {
463
        if (!is_array(reset($values))) {
464
            $values = [$values];
465
        }
466
467 View Code Duplication
        foreach ($values as $i => $record) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
468
            foreach ($record as $j => $value) {
469
                $this->addBinding($value, 'insert');
470
                $values[$i][$j] = $this->getLastBindingKey();
471
            }
472
        }
473
474
        $sql = $this->grammar->compileInsertGetId($this, $values, $sequence);
475
476
        return $this->processor->processInsertGetId($this, $sql, $this->getBindings(), $sequence);
477
    }
478
479
    /**
480
     * {@inheritdoc}
481
     */
482
    public function sum($columns = '*')
483
    {
484
        return (int) $this->aggregate(strtoupper(__FUNCTION__), Arr::wrap($columns));
485
    }
486
487
    /**
488
     * {@inheritdoc}
489
     */
490
    public function count($columns = '*')
491
    {
492
        return (int) $this->aggregate(strtoupper(__FUNCTION__), Arr::wrap($columns));
493
    }
494
495
    /**
496
     * {@inheritdoc}
497
     */
498
    public function aggregate($function, $columns = ['*'])
499
    {
500
        $results = $this->cloneWithout(['columns'])
501
            ->cloneWithoutBindings(['select'])
502
            ->setAggregate($function, $columns)
503
            ->get($columns);
504
505
        if (!$results->isEmpty()) {
506
            return array_change_key_case((array) $results[0])['aggregate'];
507
        }
508
509
        return null;
510
    }
511
512
    /**
513
     * {@inheritdoc}
514
     */
515
    public function cloneWithoutBindings(array $except)
516
    {
517
        return tap(clone $this, function ($clone) use ($except) {
518
            foreach ($except as $type) {
519
                unset($clone->bindings[$type]);
520
            }
521
        });
522
    }
523
524
    /**
525
     * {@inheritdoc}
526
     */
527
    public function insert(array $values)
528
    {
529
        // Since every insert gets treated like a batch insert, we will make sure the
530
        // bindings are structured in a way that is convenient when building these
531
        // inserts statements by verifying these elements are actually an array.
532
        if (empty($values)) {
533
            return true;
534
        }
535
536
        if (!is_array(reset($values))) {
537
            $values = [$values];
538
        }
539
540
        // Here, we will sort the insert keys for every record so that each insert is
541
        // in the same order for the record. We need to make sure this is the case
542
        // so there are not any errors or problems when inserting these records.
543
        else {
544
            foreach ($values as $key => $value) {
545
                ksort($value);
546
                $values[$key] = $value;
547
            }
548
        }
549
550
        $values = $this->prepareColumns($values);
551
552 View Code Duplication
        foreach ($values as $i => $record) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
553
            foreach ($record as $j => $value) {
554
                $this->addBinding($value, 'insert');
555
                $values[$i][$j] = $this->getLastBindingKey();
556
            }
557
        }
558
559
        $aql = $this->grammar->compileInsert($this, $values);
560
561
        // Finally, we will run this query against the database connection and return
562
        // the results. We will need to also flatten these bindings before running
563
        // the query so they are all in one huge, flattened array for execution.
564
        return $this->connection->insert(
565
            $aql,
566
            $this->getBindings()
567
        );
568
    }
569
570
    /**
571
     * Compile select to Aql format
572
     * @return string
573
     */
574
    public function toAql()
575
    {
576
        $aql = $this->grammar->compileSelect($this);
577
        return $aql;
578
    }
579
580
    /**
581
     * {@inheritdoc}
582
     */
583
    protected function addDynamic($segment, $connector, $parameters, $index)
584
    {
585
        // Once we have parsed out the columns and formatted the boolean operators we
586
        // are ready to add it to this query as a where clause just like any other
587
        // clause on the query. Then we'll increment the parameter index values.
588
        $bool = strtolower($connector);
589
590
        $this->where(Str::snake($segment), '==', $parameters[$index], $bool);
591
    }
592
593
    /**
594
     * {@inheritdoc}
595
     */
596
    protected function runSelect()
597
    {
598
        return $this->connection->select(
599
            $this->toAql(), $this->getBindings()
600
        );
601
    }
602
603
    /**
604
     * {@inheritdoc}
605
     */
606
    protected function setAggregate($function, $columns)
607
    {
608
        $this->aggregate = compact('function', 'columns');
609
610
        if (empty($this->groups)) {
611
            $this->orders = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $orders.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
612
613
            unset($this->bindings['order']);
614
        }
615
616
        return $this;
617
    }
618
619
    /**
620
     * {@inheritdoc}
621
     */
622
    protected function invalidOperatorAndValue($operator, $value)
623
    {
624
        return is_null($value) && in_array($operator, $this->operators) &&
625
            !in_array($operator, ['==', '!=']);
626
    }
627
628
    /**
629
     * {@inheritdoc}
630
     */
631
    protected function prepareValueAndOperator($value, $operator, $useDefault = false)
632
    {
633
        if ($useDefault) {
634
            return [$operator, '=='];
635
        } elseif ($this->invalidOperatorAndValue($operator, $value)) {
636
            throw new \InvalidArgumentException('Illegal operator and value combination.');
637
        }
638
639
        return [$value, $operator];
640
    }
641
642
    /**
643
     * Check exist entity name in joins or it base entity. Throw exception if didn't find.
644
     * @param $column
645
     * @throws Exception
646
     */
647
    protected function checkColumnIfJoin($column)
648
    {
649
        if (empty($this->joins)) {
650
            return;
651
        }
652
        $columnEntityName = getEntityNameFromColumn($column);
653
654
        if (is_null($columnEntityName)) {
655
            throw new Exception('You can\'t use column '.$column.' without entity name, with join.');
656
        }
657
658
        if ($columnEntityName === getEntityName($this->from)) {
659
            return;
660
        }
661
662
        foreach ($this->joins as $join) {
663
            $joinEntityName = getEntityName($join->table);
664
            if ($columnEntityName === $joinEntityName) {
665
                return;
666
            }
667
        }
668
        throw new Exception('You can\'t use column '.$column.' with this joins.');
669
    }
670
671
    /**
672
     * Prepate columns from values array
673
     * @param $values
674
     * @return array
675
     * @throws \Exception
676
     */
677
    protected function prepareColumns($values)
678
    {
679
        $res = [];
680
        foreach ($values as $key => $value) {
681
            $column = $this->prepareColumn($key);
682
            $res[$column] = $value;
683
        }
684
        return $res;
685
    }
686
687
    /**
688
     * Check column for joins and wrap column (add table name and wrap in ``)
689
     *
690
     * @param $column
691
     * @return string
692
     * @throws Exception
693
     */
694
    protected function prepareColumn($column)
695
    {
696
        $this->checkColumnIfJoin($column);
697
698
        $column = $this->grammar->wrapColumn($column, $this->from);
699
700
        return $column;
701
    }
702
703
    /**
704
     * Get next binding variable name
705
     *
706
     * @return string
707
     */
708
    protected function getBindingVariableName()
709
    {
710
        return 'B' . (count($this->bindings) + 1);
711
    }
712
713
    /**
714
     * Return table from prepared column or null
715
     *
716
     * @param string $column
717
     * @return null|string
718
     */
719
    protected function stripTableForPluck($column)
720
    {
721
        if (is_null($column)) {
722
            return null;
723
        }
724
        $column = explode('.', $column)[1];
725
726
        return trim($column, '`');
727
    }
728
}
729