Test Failed
Pull Request — master (#4)
by
unknown
11:18
created

QueryBuilder::whereIn()   C

Complexity

Conditions 8
Paths 32

Size

Total Lines 45
Code Lines 17

Duplication

Lines 6
Ratio 13.33 %

Importance

Changes 0
Metric Value
cc 8
eloc 17
nc 32
nop 4
dl 6
loc 45
rs 5.3846
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 function sonrac\Arango\Helpers\getEntityName;
17
use function sonrac\Arango\Helpers\getEntityNameFromColumn;
18
use sonrac\Arango\Query\Grammars\Grammar;
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
        
129
        $column = $this->prepareColumn($column);
130
        
131
        $this->{$this->unions ? 'unionOrders' : 'orders'}[] = [
132
            'column' => $column,
133
            'direction' => strtolower($direction) == 'asc' ? 'asc' : 'desc',
134
        ];
135
136
        return $this;
137
    }
138
139
    /**
140
     * @inheritdoc
141
     */
142
    public function whereIn($column, $values, $boolean = 'and', $not = false)
143
    {
144
        $type = $not ? 'NotIn' : 'In';
145
146
        if ($values instanceof Builder) {
147
            $values = $values->getQuery();
148
        }
149
150
        // If the value is a query builder instance we will assume the developer wants to
151
        // look for any values that exists within this given query. So we will add the
152
        // query accordingly so that this query is properly executed when it is run.
153
        if ($values instanceof self) {
154
            return $this->whereInExistingQuery(
155
                $column, $values, $boolean, $not
156
            );
157
        }
158
159
        // If the value of the where in clause is actually a Closure, we will assume that
160
        // the developer is using a full sub-select for this "in" statement, and will
161
        // execute those Closures, then we can re-construct the entire sub-selects.
162
        if ($values instanceof \Closure) {
163
            return $this->whereInSub($column, $values, $boolean, $not);
164
        }
165
166
        // Next, if the value is Arrayable we need to cast it to its raw array form so we
167
        // have the underlying array value instead of an Arrayable object which is not
168
        // able to be added as a binding, etc. We will then add to the wheres array.
169
        if ($values instanceof Arrayable) {
170
            $values = $values->toArray();
171
        }
172
173
        // Finally we'll add a binding for each values unless that value is an expression
174
        // in which case we will just skip over it since it will be the query as a raw
175
        // string and not as a parameterized place-holder to be replaced by the PDO.
176 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...
177
            if (!$value instanceof Expression) {
178
                $this->addBinding($value, 'where');
179
                $values[$index] = $this->getLastBindingKey();
180
            }
181
        }
182
183
        $this->wheres[] = compact('type', 'column', 'values', 'boolean');
184
185
        return $this;
186
    }
187
188
    /**
189
     * You can get last binding key from getLastBindingKey
190
     * @inheritdoc
191
     */
192
    public function addBinding($value, $type = 'where')
193
    {
194
        if (is_array($value)) {
195
            foreach ($value as $variable) {
196
                $this->bindings[$this->getBindingVariableName()] = $variable;
197
            }
198
        } else {
199
            $this->bindings[$this->getBindingVariableName()] = $value;
200
        }
201
202
        return $this;
203
    }
204
205
    /**
206
     * Return last binding key
207
     *
208
     * @return string
209
     */
210
    public function getLastBindingKey()
211
    {
212
        $keys = array_keys($this->getBindings());
213
        return "@" . array_pop($keys);
214
    }
215
216
    /**
217
     * @inheritdoc
218
     */
219
    public function getBindings()
220
    {
221
        return $this->bindings;
222
    }
223
224
    /**
225
     * @inheritdoc
226
     */
227
    public function whereBetween($column, array $values, $boolean = 'and', $not = false)
228
    {
229
        $this->where(function (QueryBuilder $query) use ($column, $values, $boolean, $not) {
230
            list($from, $to) = $values;
231
            if (!$not) {
232
                $query->where($column, '>', $from);
233
                $query->where($column, '<', $to);
234
            } else {
235
                $query->where($column, '<=', $from);
236
                $query->orWhere($column, '>=', $to);
237
            }
238
        }, $boolean);
239
240
        return $this;
241
    }
242
243
    /**
244
     * @inheritdoc
245
     */
246
    public function where($column, $operator = null, $value = null, $boolean = 'and')
247
    {
248
        $column = $this->prepareColumn($column);
249
250
        //For compatibility with internal framework functions
251
        if($operator === '='){
252
            $operator = '==';
253
        }
254
        // If the column is an array, we will assume it is an array of key-value pairs
255
        // and can add them each as a where clause. We will maintain the boolean we
256
        // received when the method was called and pass it into the nested where.
257
        if (is_array($column)) {
258
            return $this->addArrayOfWheres($column, $boolean);
259
        }
260
261
        // Here we will make some assumptions about the operator. If only 2 values are
262
        // passed to the method, we will assume that the operator is an equals sign
263
        // and keep going. Otherwise, we'll require the operator to be passed in.
264
        list($value, $operator) = $this->prepareValueAndOperator(
265
            $value, $operator, func_num_args() == 2
266
        );
267
268
        // If the columns is actually a Closure instance, we will assume the developer
269
        // wants to begin a nested where statement which is wrapped in parenthesis.
270
        // We'll add that Closure to the query then return back out immediately.
271
        if ($column instanceof \Closure) {
272
            return $this->whereNested($column, $boolean);
273
        }
274
275
        // If the given operator is not found in the list of valid operators we will
276
        // assume that the developer is just short-cutting the '=' operators and
277
        // we will set the operators to '=' and set the values appropriately.
278
        if ($this->invalidOperator($operator)) {
279
            list($value, $operator) = [$operator, '=='];
280
        }
281
282
        // If the value is a Closure, it means the developer is performing an entire
283
        // sub-select within the query and we will need to compile the sub-select
284
        // within the where clause to get the appropriate query record results.
285
        if ($value instanceof \Closure) {
286
            return $this->whereSub($column, $operator, $value, $boolean);
287
        }
288
289
        // If the value is "null", we will just assume the developer wants to add a
290
        // where null clause to the query. So, we will allow a short-cut here to
291
        // that method for convenience so the developer doesn't have to check.
292
        if (is_null($value)) {
293
            return $this->whereNull($column, $boolean, $operator !== '==');
294
        }
295
296
        // If the column is making a JSON reference we'll check to see if the value
297
        // is a boolean. If it is, we'll add the raw boolean string as an actual
298
        // value to the query to ensure this is properly handled by the query.
299
        if (Str::contains($column, '->') && is_bool($value)) {
300
            $value = new Expression($value ? 'true' : 'false');
301
        }
302
303
        // Now that we are working with just a simple query we can put the elements
304
        // in our array and add the query binding to our array of bindings that
305
        // will be bound to each SQL statements when it is finally executed.
306
        $type = 'Basic';
307
308
        if (!$value instanceof Expression) {
309
            $this->addBinding($value, 'where');
310
            $value = $this->getLastBindingKey();
311
        }
312
313
        $this->wheres[] = compact(
314
            'type', 'column', 'operator', 'value', 'boolean'
315
        );
316
317
        return $this;
318
    }
319
320
    /**
321
     * @inheritdoc
322
     */
323
    public function whereColumn($first, $operator = null, $second = null, $boolean = 'and')
324
    {
325
326
        if($operator === '='){
327
            $operator = '==';
328
        }
329
330
        // If the column is an array, we will assume it is an array of key-value pairs
331
        // and can add them each as a where clause. We will maintain the boolean we
332
        // received when the method was called and pass it into the nested where.
333
        if (is_array($first)) {
334
            return $this->addArrayOfWheres($first, $boolean, 'whereColumn');
335
        }
336
337
        // If the given operator is not found in the list of valid operators we will
338
        // assume that the developer is just short-cutting the '=' operators and
339
        // we will set the operators to '=' and set the values appropriately.
340
        if ($this->invalidOperator($operator)) {
341
            list($second, $operator) = [$operator, '=='];
342
        }
343
344
        // Finally, we will add this where clause into this array of clauses that we
345
        // are building for the query. All of them will be compiled via a grammar
346
        // once the query is about to be executed and run against the database.
347
        $type = 'Column';
348
349
        $this->wheres[] = compact(
350
            'type', 'first', 'operator', 'second', 'boolean'
351
        );
352
353
        return $this;
354
    }
355
356
    /**
357
     * @inheritdoc
358
     */
359
    public function whereNull($column, $boolean = 'and', $not = false)
360
    {
361
        $column = $this->prepareColumn($column);
362
363
        $type = $not ? 'NotNull' : 'Null';
364
365
        $this->wheres[] = compact('type', 'column', 'boolean');
366
367
        return $this;
368
    }
369
370
    /**
371
     * @inheritdoc
372
     */
373 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...
374
    {
375
        if (! is_numeric($amount)) {
376
            throw new \InvalidArgumentException('Non-numeric value passed to increment method.');
377
        }
378
379
        $wrapped = $this->prepareColumn($column);
380
381
        $columns = array_merge([$column => $this->raw("$wrapped + $amount")], $extra);
382
383
        return $this->update($columns);
384
    }
385
386
    /**
387
     * @inheritdoc
388
     */
389 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...
390
    {
391
        if (! is_numeric($amount)) {
392
            throw new \InvalidArgumentException('Non-numeric value passed to decrement method.');
393
        }
394
395
        $wrapped = $this->prepareColumn($column);
396
397
        $columns = array_merge([$column => $this->raw("$wrapped - $amount")], $extra);
398
399
        return $this->update($columns);
400
    }
401
402
    /**
403
     * @inheritdoc
404
     */
405
    public function update(array $values)
406
    {
407
408
409 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...
410
            if (!$value instanceof Expression) {
411
                $this->addBinding($value, 'update');
412
                $values[$index] = $this->getLastBindingKey();
413
            }
414
        }
415
416
        $aql = $this->grammar->compileUpdate($this, $values);
417
418
        return $this->connection->update($aql, $this->getBindings());
419
    }
420
421
    /**
422
     * @inheritdoc
423
     */
424
    public function delete($id = null)
425
    {
426
        // If an ID is passed to the method, we will set the where clause to check the
427
        // ID to let developers to simply and quickly remove a single row from this
428
        // database without manually specifying the "where" clauses on the query.
429
        if (! is_null($id)) {
430
            $this->where($this->from.'.id', '=', $id);
431
        }
432
433
        return $this->connection->delete(
434
            $this->grammar->compileDelete($this), $this->getBindings()
435
        );
436
    }
437
438
    /**
439
     * @inheritdoc
440
     */
441
    public function truncate()
442
    {
443
        $connection = $this->getConnection();
444
        /**
445
         * @var Connection $connection
446
         */
447
        $arangoDB = $connection->getArangoDB();
448
        $arangoDB->truncate($this->from);
449
    }
450
451
    /**
452
     * @inheritdoc
453
     */
454
    public function find($id, $columns = ['*'])
455
    {
456
        $column = $this->prepareColumn('_key');
457
        return $this->where($column, '==', $id)->limit(1)->first($columns);
458
    }
459
460
    /**
461
     * @inheritdoc
462
     */
463
    public function insertGetId(array $values, $sequence = null)
464
    {
465
        if (!is_array(reset($values))) {
466
            $values = [$values];
467
        }
468
469 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...
470
            foreach ($record as $j => $value) {
471
                $this->addBinding($value, 'insert');
472
                $values[$i][$j] = $this->getLastBindingKey();
473
            }
474
        }
475
476
        $sql = $this->grammar->compileInsertGetId($this, $values, $sequence);
477
478
        return $this->processor->processInsertGetId($this, $sql, $this->getBindings(), $sequence);
479
    }
480
481
    /**
482
     * @inheritdoc
483
     */
484
    public function sum($columns = '*')
485
    {
486
        return (int) $this->aggregate(strtoupper(__FUNCTION__), Arr::wrap($columns));
487
    }
488
489
    /**
490
     * @inheritdoc
491
     */
492
    public function count($columns = '*')
493
    {
494
        return (int) $this->aggregate(strtoupper(__FUNCTION__), Arr::wrap($columns));
495
    }
496
497
    /**
498
     * @inheritdoc
499
     */
500
    public function aggregate($function, $columns = ['*'])
501
    {
502
        $results = $this->cloneWithout(['columns'])
503
            ->cloneWithoutBindings(['select'])
504
            ->setAggregate($function, $columns)
505
            ->get($columns);
506
507
        if (! $results->isEmpty()) {
508
            return array_change_key_case((array) $results[0])['aggregate'];
509
        }
510
511
        return null;
512
    }
513
514
    /**
515
     * @inheritdoc
516
     */
517
    public function cloneWithoutBindings(array $except)
518
    {
519
        return tap(clone $this, function ($clone) use ($except) {
520
            foreach ($except as $type) {
521
                unset($clone->bindings[$type]);
522
            }
523
        });
524
    }
525
526
    /**
527
     * @inheritdoc
528
     */
529
    public function insert(array $values)
530
    {
531
        // Since every insert gets treated like a batch insert, we will make sure the
532
        // bindings are structured in a way that is convenient when building these
533
        // inserts statements by verifying these elements are actually an array.
534
        if (empty($values)) {
535
            return true;
536
        }
537
538
        if (!is_array(reset($values))) {
539
            $values = [$values];
540
        }
541
542
        // Here, we will sort the insert keys for every record so that each insert is
543
        // in the same order for the record. We need to make sure this is the case
544
        // so there are not any errors or problems when inserting these records.
545
        else {
546
            foreach ($values as $key => $value) {
547
                ksort($value);
548
                $values[$key] = $value;
549
            }
550
        }
551
552
        $values = $this->prepareColumns($values);
553
554 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...
555
            foreach ($record as $j => $value) {
556
                $this->addBinding($value, 'insert');
557
                $values[$i][$j] = $this->getLastBindingKey();
558
            }
559
        }
560
561
        $aql = $this->grammar->compileInsert($this, $values);
562
563
        // Finally, we will run this query against the database connection and return
564
        // the results. We will need to also flatten these bindings before running
565
        // the query so they are all in one huge, flattened array for execution.
566
        return $this->connection->insert(
567
            $aql,
568
            $this->getBindings()
569
        );
570
    }
571
572
    /**
573
     * @inheritdoc
574
     */
575
    protected function addDynamic($segment, $connector, $parameters, $index)
576
    {
577
        // Once we have parsed out the columns and formatted the boolean operators we
578
        // are ready to add it to this query as a where clause just like any other
579
        // clause on the query. Then we'll increment the parameter index values.
580
        $bool = strtolower($connector);
581
582
        $this->where(Str::snake($segment), '==', $parameters[$index], $bool);
583
    }
584
585
    /**
586
     * @inheritdoc
587
     */
588
    protected function runSelect()
589
    {
590
        return $this->connection->select(
591
            $this->toAql(), $this->getBindings()
592
        );
593
    }
594
595
    /**
596
     * Compile select to Aql format
597
     * @return string
598
     */
599
    public function toAql()
600
    {
601
        $aql = $this->grammar->compileSelect($this);
602
        return $aql;
603
    }
604
605
    /**
606
     * @inheritdoc
607
     */
608
    protected function setAggregate($function, $columns)
609
    {
610
        $this->aggregate = compact('function', 'columns');
611
612
        if (empty($this->groups)) {
613
            $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...
614
615
            unset($this->bindings['order']);
616
        }
617
618
        return $this;
619
    }
620
621
    /**
622
     * @inheritdoc
623
     */
624
    protected function invalidOperatorAndValue($operator, $value)
625
    {
626
        return is_null($value) && in_array($operator, $this->operators) &&
627
            ! in_array($operator, ['==', '!=']);
628
    }
629
630
    /**
631
     * @inheritdoc
632
     */
633
    protected function prepareValueAndOperator($value, $operator, $useDefault = false)
634
    {
635
        if ($useDefault) {
636
            return [$operator, '=='];
637
        } elseif ($this->invalidOperatorAndValue($operator, $value)) {
638
            throw new \InvalidArgumentException('Illegal operator and value combination.');
639
        }
640
641
        return [$value, $operator];
642
    }
643
644
    /**
645
     * Check exist entity name in joins or it base entity. Throw exception if didn't find.
646
     * @param $column
647
     * @throws Exception
648
     */
649
    protected function checkColumnIfJoin($column){
650
        if(empty($this->joins)){
651
            return;
652
        }
653
        $columnEntityName = getEntityNameFromColumn($column);
654
655
        if(is_null($columnEntityName)){
656
            throw new Exception("You can't use column ".$column." without entity name, with join.");
657
        }
658
659
        if($columnEntityName === getEntityName($this->from)){
660
            return;
661
        }
662
663
        foreach ($this->joins as $join){
664
            $joinEntityName = getEntityName($join->table);
665
            if($columnEntityName === $joinEntityName){
666
                return;
667
            }
668
        }
669
        throw new Exception("You can't use column ".$column.' with this joins.');
670
    }
671
672
    /**
673
     * Prepate columns from values array
674
     * @param $values
675
     * @return array
676
     * @throws \Exception
677
     */
678
    protected function prepareColumns($values){
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
        $this->checkColumnIfJoin($column);
696
697
        $column = $this->grammar->wrapColumn($column, $this->from);
698
699
        return $column;
700
    }
701
702
    /**
703
     * Get next binding variable name
704
     *
705
     * @return string
706
     */
707
    protected function getBindingVariableName()
708
    {
709
        return "B" . (count($this->bindings) + 1);
710
    }
711
712
    /**
713
     * Return table from prepared column or null
714
     *
715
     * @param string $column
716
     * @return null|string
717
     */
718
    protected function stripTableForPluck($column){
719
        if(is_null($column)){
720
            return null;
721
        }
722
        $column = explode('.', $column)[1];
723
724
        return trim($column, '`');
725
    }
726
}
727