QueryBuilder::decrement()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 12
Ratio 100 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 3
dl 12
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
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
        if ($operator === '=') {
324
            $operator = '==';
325
        }
326
327
        // If the column is an array, we will assume it is an array of key-value pairs
328
        // and can add them each as a where clause. We will maintain the boolean we
329
        // received when the method was called and pass it into the nested where.
330
        if (is_array($first)) {
331
            return $this->addArrayOfWheres($first, $boolean, 'whereColumn');
332
        }
333
334
        // If the given operator is not found in the list of valid operators we will
335
        // assume that the developer is just short-cutting the '=' operators and
336
        // we will set the operators to '=' and set the values appropriately.
337
        if ($this->invalidOperator($operator)) {
338
            list($second, $operator) = [$operator, '=='];
339
        }
340
341
        // Finally, we will add this where clause into this array of clauses that we
342
        // are building for the query. All of them will be compiled via a grammar
343
        // once the query is about to be executed and run against the database.
344
        $type = 'Column';
345
346
        $this->wheres[] = compact(
347
            'type', 'first', 'operator', 'second', 'boolean'
348
        );
349
350
        return $this;
351
    }
352
353
    /**
354
     * {@inheritdoc}
355
     */
356
    public function whereNull($column, $boolean = 'and', $not = false)
357
    {
358
        $column = $this->prepareColumn($column);
359
360
        $type = $not ? 'NotNull' : 'Null';
361
362
        $this->wheres[] = compact('type', 'column', 'boolean');
363
364
        return $this;
365
    }
366
367
    /**
368
     * {@inheritdoc}
369
     */
370 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...
371
    {
372
        if (!is_numeric($amount)) {
373
            throw new \InvalidArgumentException('Non-numeric value passed to increment method.');
374
        }
375
376
        $wrapped = $this->prepareColumn($column);
377
378
        $columns = array_merge([$column => $this->raw("$wrapped + $amount")], $extra);
379
380
        return $this->update($columns);
381
    }
382
383
    /**
384
     * {@inheritdoc}
385
     */
386 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...
387
    {
388
        if (!is_numeric($amount)) {
389
            throw new \InvalidArgumentException('Non-numeric value passed to decrement method.');
390
        }
391
392
        $wrapped = $this->prepareColumn($column);
393
394
        $columns = array_merge([$column => $this->raw("$wrapped - $amount")], $extra);
395
396
        return $this->update($columns);
397
    }
398
399
    /**
400
     * {@inheritdoc}
401
     */
402
    public function update(array $values)
403
    {
404 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...
405
            if (!$value instanceof Expression) {
406
                $this->addBinding($value, 'update');
407
                $values[$index] = $this->getLastBindingKey();
408
            }
409
        }
410
411
        $aql = $this->grammar->compileUpdate($this, $values);
412
413
        return $this->connection->update($aql, $this->getBindings());
414
    }
415
416
    /**
417
     * {@inheritdoc}
418
     */
419
    public function delete($id = null)
420
    {
421
        // If an ID is passed to the method, we will set the where clause to check the
422
        // ID to let developers to simply and quickly remove a single row from this
423
        // database without manually specifying the "where" clauses on the query.
424
        if (!is_null($id)) {
425
            $this->where($this->from.'.id', '=', $id);
426
        }
427
428
        return $this->connection->delete(
429
            $this->grammar->compileDelete($this), $this->getBindings()
430
        );
431
    }
432
433
    /**
434
     * {@inheritdoc}
435
     */
436
    public function truncate()
437
    {
438
        $connection = $this->getConnection();
439
        /**
440
         * @var Connection
441
         */
442
        $arangoDB = $connection->getArangoDB();
443
        $arangoDB->truncate($this->from);
444
    }
445
446
    /**
447
     * {@inheritdoc}
448
     */
449
    public function find($id, $columns = ['*'])
450
    {
451
        $column = $this->prepareColumn('_key');
452
        return $this->where($column, '==', $id)->limit(1)->first($columns);
453
    }
454
455
    /**
456
     * {@inheritdoc}
457
     */
458
    public function insertGetId(array $values, $sequence = null)
459
    {
460
        if (!is_array(reset($values))) {
461
            $values = [$values];
462
        }
463
464 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...
465
            foreach ($record as $j => $value) {
466
                $this->addBinding($value, 'insert');
467
                $values[$i][$j] = $this->getLastBindingKey();
468
            }
469
        }
470
471
        $sql = $this->grammar->compileInsertGetId($this, $values, $sequence);
472
473
        return $this->processor->processInsertGetId($this, $sql, $this->getBindings(), $sequence);
474
    }
475
476
    /**
477
     * {@inheritdoc}
478
     */
479
    public function sum($columns = '*')
480
    {
481
        return (int) $this->aggregate(strtoupper(__FUNCTION__), Arr::wrap($columns));
482
    }
483
484
    /**
485
     * {@inheritdoc}
486
     */
487
    public function count($columns = '*')
488
    {
489
        return (int) $this->aggregate(strtoupper(__FUNCTION__), Arr::wrap($columns));
490
    }
491
492
    /**
493
     * {@inheritdoc}
494
     */
495
    public function aggregate($function, $columns = ['*'])
496
    {
497
        $results = $this->cloneWithout(['columns'])
498
            ->cloneWithoutBindings(['select'])
499
            ->setAggregate($function, $columns)
500
            ->get($columns);
501
502
        if (!$results->isEmpty()) {
503
            return array_change_key_case((array) $results[0])['aggregate'];
504
        }
505
506
        return null;
507
    }
508
509
    /**
510
     * {@inheritdoc}
511
     */
512
    public function cloneWithoutBindings(array $except)
513
    {
514
        return tap(clone $this, function ($clone) use ($except) {
515
            foreach ($except as $type) {
516
                unset($clone->bindings[$type]);
517
            }
518
        });
519
    }
520
521
    /**
522
     * {@inheritdoc}
523
     */
524
    public function insert(array $values)
525
    {
526
        // Since every insert gets treated like a batch insert, we will make sure the
527
        // bindings are structured in a way that is convenient when building these
528
        // inserts statements by verifying these elements are actually an array.
529
        if (empty($values)) {
530
            return true;
531
        }
532
533
        if (!is_array(reset($values))) {
534
            $values = [$values];
535
        }
536
537
        // Here, we will sort the insert keys for every record so that each insert is
538
        // in the same order for the record. We need to make sure this is the case
539
        // so there are not any errors or problems when inserting these records.
540
        else {
541
            foreach ($values as $key => $value) {
542
                ksort($value);
543
                $values[$key] = $value;
544
            }
545
        }
546
547
        $values = $this->prepareColumns($values);
548
549 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...
550
            foreach ($record as $j => $value) {
551
                $this->addBinding($value, 'insert');
552
                $values[$i][$j] = $this->getLastBindingKey();
553
            }
554
        }
555
556
        $aql = $this->grammar->compileInsert($this, $values);
557
558
        // Finally, we will run this query against the database connection and return
559
        // the results. We will need to also flatten these bindings before running
560
        // the query so they are all in one huge, flattened array for execution.
561
        return $this->connection->insert(
562
            $aql,
563
            $this->getBindings()
564
        );
565
    }
566
567
    /**
568
     * Compile select to Aql format
569
     * @return string
570
     */
571
    public function toAql()
572
    {
573
        $aql = $this->grammar->compileSelect($this);
574
        return $aql;
575
    }
576
577
    /**
578
     * {@inheritdoc}
579
     */
580
    protected function addDynamic($segment, $connector, $parameters, $index)
581
    {
582
        // Once we have parsed out the columns and formatted the boolean operators we
583
        // are ready to add it to this query as a where clause just like any other
584
        // clause on the query. Then we'll increment the parameter index values.
585
        $bool = strtolower($connector);
586
587
        $this->where(Str::snake($segment), '==', $parameters[$index], $bool);
588
    }
589
590
    /**
591
     * {@inheritdoc}
592
     */
593
    protected function runSelect()
594
    {
595
        return $this->connection->select(
596
            $this->toAql(), $this->getBindings()
597
        );
598
    }
599
600
    /**
601
     * {@inheritdoc}
602
     */
603
    protected function setAggregate($function, $columns)
604
    {
605
        $this->aggregate = compact('function', 'columns');
606
607
        if (empty($this->groups)) {
608
            $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...
609
610
            unset($this->bindings['order']);
611
        }
612
613
        return $this;
614
    }
615
616
    /**
617
     * {@inheritdoc}
618
     */
619
    protected function invalidOperatorAndValue($operator, $value)
620
    {
621
        return is_null($value) && in_array($operator, $this->operators) &&
622
            !in_array($operator, ['==', '!=']);
623
    }
624
625
    /**
626
     * {@inheritdoc}
627
     */
628
    protected function prepareValueAndOperator($value, $operator, $useDefault = false)
629
    {
630
        if ($useDefault) {
631
            return [$operator, '=='];
632
        } elseif ($this->invalidOperatorAndValue($operator, $value)) {
633
            throw new \InvalidArgumentException('Illegal operator and value combination.');
634
        }
635
636
        return [$value, $operator];
637
    }
638
639
    /**
640
     * Check exist entity name in joins or it base entity. Throw exception if didn't find.
641
     * @param $column
642
     * @throws Exception
643
     */
644
    protected function checkColumnIfJoin($column)
645
    {
646
        if (empty($this->joins)) {
647
            return;
648
        }
649
        $columnEntityName = getEntityNameFromColumn($column);
650
651
        if (is_null($columnEntityName)) {
652
            throw new Exception('You can\'t use column '.$column.' without entity name, with join.');
653
        }
654
655
        if ($columnEntityName === getEntityName($this->from)) {
656
            return;
657
        }
658
659
        foreach ($this->joins as $join) {
660
            $joinEntityName = getEntityName($join->table);
661
            if ($columnEntityName === $joinEntityName) {
662
                return;
663
            }
664
        }
665
        throw new Exception('You can\'t use column '.$column.' with this joins.');
666
    }
667
668
    /**
669
     * Prepate columns from values array
670
     * @param $values
671
     * @return array
672
     * @throws \Exception
673
     */
674
    protected function prepareColumns($values)
675
    {
676
        $res = [];
677
        foreach ($values as $key => $value) {
678
            $column = $this->prepareColumn($key);
679
            $res[$column] = $value;
680
        }
681
        return $res;
682
    }
683
684
    /**
685
     * Check column for joins and wrap column (add table name and wrap in ``)
686
     *
687
     * @param $column
688
     * @return string
689
     * @throws Exception
690
     */
691
    protected function prepareColumn($column)
692
    {
693
        $this->checkColumnIfJoin($column);
694
695
        $column = $this->grammar->wrapColumn($column, $this->from);
696
697
        return $column;
698
    }
699
700
    /**
701
     * Get next binding variable name
702
     *
703
     * @return string
704
     */
705
    protected function getBindingVariableName()
706
    {
707
        return 'B' . (count($this->bindings) + 1);
708
    }
709
710
    /**
711
     * Return table from prepared column or null
712
     *
713
     * @param string $column
714
     * @return null|string
715
     */
716
    protected function stripTableForPluck($column)
717
    {
718
        if (is_null($column)) {
719
            return null;
720
        }
721
        $column = explode('.', $column)[1];
722
723
        return trim($column, '`');
724
    }
725
}
726