Test Failed
Pull Request — master (#4)
by
unknown
03:29
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
     * Compile select to Aql format
574
     * @return string
575
     */
576
    public function toAql()
577
    {
578
        $aql = $this->grammar->compileSelect($this);
579
        return $aql;
580
    }
581
582
    /**
583
     * {@inheritdoc}
584
     */
585
    protected function addDynamic($segment, $connector, $parameters, $index)
586
    {
587
        // Once we have parsed out the columns and formatted the boolean operators we
588
        // are ready to add it to this query as a where clause just like any other
589
        // clause on the query. Then we'll increment the parameter index values.
590
        $bool = strtolower($connector);
591
592
        $this->where(Str::snake($segment), '==', $parameters[$index], $bool);
593
    }
594
595
    /**
596
     * {@inheritdoc}
597
     */
598
    protected function runSelect()
599
    {
600
        return $this->connection->select(
601
            $this->toAql(), $this->getBindings()
602
        );
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
    {
651
        if (empty($this->joins)) {
652
            return;
653
        }
654
        $columnEntityName = getEntityNameFromColumn($column);
655
656
        if (is_null($columnEntityName)) {
657
            throw new Exception("You can't use column ".$column." without entity name, with join.");
658
        }
659
660
        if ($columnEntityName === getEntityName($this->from)) {
661
            return;
662
        }
663
664
        foreach ($this->joins as $join) {
665
            $joinEntityName = getEntityName($join->table);
666
            if ($columnEntityName === $joinEntityName) {
667
                return;
668
            }
669
        }
670
        throw new Exception("You can't use column ".$column.' with this joins.');
671
    }
672
673
    /**
674
     * Prepate columns from values array
675
     * @param $values
676
     * @return array
677
     * @throws \Exception
678
     */
679
    protected function prepareColumns($values)
680
    {
681
        $res = [];
682
        foreach ($values as $key => $value) {
683
            $column = $this->prepareColumn($key);
684
            $res[$column] = $value;
685
        }
686
        return $res;
687
    }
688
689
    /**
690
     * Check column for joins and wrap column (add table name and wrap in ``)
691
     *
692
     * @param $column
693
     * @return string
694
     * @throws Exception
695
     */
696
    protected function prepareColumn($column)
697
    {
698
        $this->checkColumnIfJoin($column);
699
700
        $column = $this->grammar->wrapColumn($column, $this->from);
701
702
        return $column;
703
    }
704
705
    /**
706
     * Get next binding variable name
707
     *
708
     * @return string
709
     */
710
    protected function getBindingVariableName()
711
    {
712
        return "B" . (count($this->bindings) + 1);
713
    }
714
715
    /**
716
     * Return table from prepared column or null
717
     *
718
     * @param string $column
719
     * @return null|string
720
     */
721
    protected function stripTableForPluck($column)
722
    {
723
        if (is_null($column)) {
724
            return null;
725
        }
726
        $column = explode('.', $column)[1];
727
728
        return trim($column, '`');
729
    }
730
}
731