Passed
Pull Request — master (#38)
by Bas
17:19
created

Builder::delete()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace LaravelFreelancerNL\Aranguent\Query;
4
5
use Closure;
6
use Illuminate\Database\ConnectionInterface;
7
use Illuminate\Database\Query\Builder as IlluminateQueryBuilder;
8
use Illuminate\Database\Query\Expression;
9
use Illuminate\Support\Arr;
10
use Illuminate\Support\Collection;
11
use Illuminate\Support\Str;
12
use InvalidArgumentException;
13
use LaravelFreelancerNL\Aranguent\Connection;
14
use LaravelFreelancerNL\FluentAQL\Exceptions\BindException;
15
use LaravelFreelancerNL\FluentAQL\Expressions\ExpressionInterface as ExpressionInterface;
16
use LaravelFreelancerNL\FluentAQL\QueryBuilder;
17
18
class Builder extends IlluminateQueryBuilder
19
{
20
21
    /**
22
     * @var Grammar
23
     */
24
    public $grammar;
25
26
    /**
27
     * @var Connection
28
     */
29
    public $connection;
30
31
    /**
32
     * @var QueryBuilder
33
     */
34
    public $aqb;
35
36
    /**
37
     * @override
38
     * Create a new query builder instance.
39
     *
40
     * @param ConnectionInterface $connection
41
     * @param Grammar             $grammar
42
     * @param Processor           $processor
43
     * @param QueryBuilder|null   $aqb
44
     */
45
    public function __construct(
46
        ConnectionInterface $connection,
47
        Grammar $grammar = null,
48
        Processor $processor = null,
49
        QueryBuilder $aqb = null
50
    ) {
51
        $this->connection = $connection;
0 ignored issues
show
Documentation Bug introduced by
$connection is of type Illuminate\Database\ConnectionInterface, but the property $connection was declared to be of type LaravelFreelancerNL\Aranguent\Connection. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
52
        $this->grammar = $grammar ?: $connection->getQueryGrammar();
53
        $this->processor = $processor ?: $connection->getPostProcessor();
54
        if (!$aqb instanceof QueryBuilder) {
55
            $aqb = new QueryBuilder();
56
        }
57
        $this->aqb = $aqb;
58
    }
59
60
    /**
61
     * Set the table which the query is targeting.
62
     *
63
     * @param  \Closure|IlluminateQueryBuilder|string  $table
64
     * @param  string|null  $as
65
     * @return IlluminateQueryBuilder
66
     */
67
    public function from($table, $as = null)
68
    {
69
        if ($this->isQueryable($table)) {
70
            return $this->fromSub($table, $as);
71
        }
72
73
        $this->grammar->registerTableAlias($table, $as);
0 ignored issues
show
Bug introduced by
It seems like $table can also be of type Closure and Illuminate\Database\Query\Builder; however, parameter $table of LaravelFreelancerNL\Aran...r::registerTableAlias() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

73
        $this->grammar->registerTableAlias(/** @scrutinizer ignore-type */ $table, $as);
Loading history...
74
75
        $this->from = $table;
0 ignored issues
show
Documentation Bug introduced by
It seems like $table can also be of type Closure or Illuminate\Database\Query\Builder. However, the property $from is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
76
77
        return $this;
78
    }
79
80
    /**
81
     * Run the query as a "select" statement against the connection.
82
     *
83
     * @return array
84
     */
85
    protected function runSelect()
86
    {
87
        $response = $this->connection->select($this->grammar->compileSelect($this)->aqb);
88
        $this->aqb = new QueryBuilder();
89
90
        return $response;
91
    }
92
93
    /**
94
     * Run a pagination count query.
95
     *
96
     * @param array $columns
97
     *
98
     * @return array
99
     */
100
    protected function runPaginationCountQuery($columns = ['*'])
101
    {
102
        $without = $this->unions ? ['orders', 'limit', 'offset'] : ['columns', 'orders', 'limit', 'offset'];
103
104
        $closeResults = $this->cloneWithout($without)
105
            ->cloneWithoutBindings($this->unions ? ['order'] : ['select', 'order'])
106
            ->setAggregate('count', $this->withoutSelectAliases($columns))
107
            ->get()->all();
108
109
        $this->aqb = new QueryBuilder();
110
111
        return $closeResults;
112
    }
113
114
    /**
115
     * Set the columns to be selected.
116
     *
117
     * @param  array|mixed  $columns
118
     * @return IlluminateQueryBuilder
119
     */
120
    public function select($columns = ['*'])
121
    {
122
        $this->columns = [];
123
        $this->bindings['select'] = [];
124
        $columns = is_array($columns) ? $columns : func_get_args();
125
126
        foreach ($columns as $as => $column) {
127
            if (is_string($as) && $this->isQueryable($column)) {
128
                $this->selectSub($column, $as);
129
            }
130
            if (! is_string($as) || ! $this->isQueryable($column)) {
131
                $this->columns[$as] = $column;
132
            }
133
        }
134
135
        return $this;
136
    }
137
138
    /**
139
     * Get the SQL representation of the query.
140
     *
141
     * @return string
142
     */
143
    public function toSql()
144
    {
145
        return $this->grammar->compileSelect($this)->aqb->query;
146
    }
147
148
    /**
149
     * Insert a new record into the database.
150
     *
151
     * @param array $values
152
     *
153
     * @throws BindException
154
     *
155
     * @return bool
156
     */
157
    public function insert(array $values): bool
158
    {
159
        $response = $this->getConnection()->insert($this->grammar->compileInsert($this, $values)->aqb);
160
        $this->aqb = new QueryBuilder();
161
162
        return $response;
163
    }
164
165
    /**
166
     * Insert a new record and get the value of the primary key.
167
     *
168
     * @param array       $values
169
     * @param string|null $sequence
170
     *
171
     * @throws BindException
172
     *
173
     * @return int
174
     */
175
    public function insertGetId(array $values, $sequence = null)
176
    {
177
        $response = $this->getConnection()->execute($this->grammar->compileInsertGetId($this, $values, $sequence)->aqb);
0 ignored issues
show
Unused Code introduced by
The call to LaravelFreelancerNL\Aran...r::compileInsertGetId() has too many arguments starting with $sequence. ( Ignorable by Annotation )

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

177
        $response = $this->getConnection()->execute($this->grammar->/** @scrutinizer ignore-call */ compileInsertGetId($this, $values, $sequence)->aqb);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
178
        $this->aqb = new QueryBuilder();
179
180
        return (is_array($response)) ? end($response) : $response;
0 ignored issues
show
introduced by
The condition is_array($response) is always true.
Loading history...
181
    }
182
183
    /**
184
     * Execute the query as a "select" statement.
185
     *
186
     * @param array|string $columns
187
     *
188
     * @return Collection
189
     */
190
    public function get($columns = ['*'])
191
    {
192
        return collect($this->onceWithColumns(Arr::wrap($columns), function () {
193
            return $this->runSelect();
194
        }));
195
    }
196
197
    /**
198
     * Get the current query value bindings in a flattened array.
199
     *
200
     * @return array
201
     */
202
    public function getBindings()
203
    {
204
        return $this->aqb->binds;
205
    }
206
207
    /**
208
     * Update a record in the database.
209
     *
210
     * @param array $values
211
     *
212
     * @return int
213
     */
214
    public function update(array $values)
215
    {
216
        $response = $this->connection->update($this->grammar->compileUpdate($this, $values)->aqb);
217
        $this->aqb = new QueryBuilder();
218
219
        return $response;
220
    }
221
222
    /**
223
     * Delete a record from the database.
224
     *
225
     * @param mixed $id
226
     *
227
     * @return int
228
     */
229
    public function delete($id = null)
230
    {
231
        $response = $this->connection->delete($this->grammar->compileDelete($this, $id)->aqb);
232
        $this->aqb = new QueryBuilder();
233
234
        return $response;
235
    }
236
237
    /**
238
     * Execute an aggregate function on the database.
239
     *
240
     * @param string $function
241
     * @param array  $columns
242
     *
243
     * @return mixed
244
     */
245
    public function aggregate($function, $columns = ['*'])
246
    {
247
        $results = $this->cloneWithout($this->unions ? [] : ['columns'])
248
            ->setAggregate($function, $columns)
249
            ->get($columns);
250
251
        $this->aqb = new QueryBuilder();
252
253
        if (!$results->isEmpty()) {
254
            return array_change_key_case((array) $results[0])['aggregate'];
255
        }
256
257
        return false;
258
    }
259
260
    /**
261
     * Add a basic where clause to the query.
262
     *
263
     * @param Closure|string|array $column
264
     * @param mixed                $operator
265
     * @param mixed                $value
266
     * @param string               $boolean
267
     *
268
     * @return Builder
269
     */
270
    public function where($column, $operator = null, $value = null, $boolean = 'and')
271
    {
272
        // If the column is an array, we will assume it is an array of key-value pairs
273
        // and can add them each as a where clause. We will maintain the boolean we
274
        // received when the method was called and pass it into the nested where.
275
        if (is_array($column)) {
276
            return $this->addArrayOfWheres($column, $boolean);
277
        }
278
279
        // Here we will make some assumptions about the operator. If only 2 values are
280
        // passed to the method, we will assume that the operator is an equals sign
281
        // and keep going. Otherwise, we'll require the operator to be passed in.
282
        [$value, $operator] = $this->prepareValueAndOperator(
283
            $value,
284
            $operator,
285
            func_num_args() === 2
286
        );
287
288
        // If the columns is actually a Closure instance, we will assume the developer
289
        // wants to begin a nested where statement which is wrapped in parenthesis.
290
        // We'll add that Closure to the query then return back out immediately.
291
        if ($column instanceof Closure) {
292
            return $this->whereNested($column, $boolean);
293
        }
294
295
        // If the given operator is not found in the list of valid operators we will
296
        // assume that the developer is just short-cutting the '=' operators and
297
        // we will set the operators to '==' and set the values appropriately.
298
        if ($this->invalidOperator($operator)) {
299
            [$value, $operator] = [$operator, '=='];
300
        }
301
302
        // If the value is a Closure, it means the developer is performing an entire
303
        // sub-select within the query and we will need to compile the sub-select
304
        // within the where clause to get the appropriate query record results.
305
        if ($value instanceof Closure) {
306
            return $this->whereSub($column, $operator, $value, $boolean);
307
        }
308
309
        $type = 'Basic';
310
311
        // Now that we are working with just a simple query we can put the elements
312
        // in our array and add the query binding to our array of bindings that
313
        // will be bound to each SQL statements when it is finally executed.
314
        $this->wheres[] = compact(
315
            'type',
316
            'column',
317
            'operator',
318
            'value',
319
            'boolean'
320
        );
321
322
        if (!$value instanceof Expression) {
323
            $this->addBinding($value, 'where');
324
        }
325
326
        return $this;
327
    }
328
329
    /**
330
     * Add a "where JSON contains" clause to the query.
331
     *
332
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
333
     *
334
     * @param  string  $column
335
     * @param  mixed  $value
336
     * @param  string  $boolean
337
     * @param  bool  $not
338
     * @return IlluminateQueryBuilder
339
     */
340
    public function whereJsonContains($column, $value, $boolean = 'and', $not = false)
341
    {
342
        $type = 'JsonContains';
343
344
        $this->wheres[] = compact('type', 'column', 'value', 'boolean', 'not');
345
346
        return $this;
347
    }
348
349
    /**
350
     * Determine if the given operator is supported.
351
     *
352
     * @param string $operator
353
     *
354
     * @return bool
355
     */
356
    protected function invalidOperator($operator)
357
    {
358
        return !in_array(strtolower($operator), $this->operators, true) &&
359
            !isset($this->grammar->getOperators()[strtoupper($operator)]);
360
    }
361
362
    /**
363
     * Add a join clause to the query.
364
     *
365
     * The boolean argument flag is part of this method's API in Laravel.
366
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
367
     *
368
     * @param string          $table
369
     * @param \Closure|string $first
370
     * @param string|null     $operator
371
     * @param string|null     $second
372
     * @param string          $type
373
     * @param bool            $where
374
     *
375
     * @return $this
376
     */
377
    public function join($table, $first, $operator = null, $second = null, $type = 'inner', $where = false)
378
    {
379
        $join = $this->newJoinClause($this, $type, $table);
380
381
        // If the first "column" of the join is really a Closure instance the developer
382
        // is trying to build a join with a complex "on" clause containing more than
383
        // one condition, so we'll add the join and call a Closure with the query.
384
        if ($first instanceof Closure) {
385
            $first($join);
386
387
            $this->joins[] = $join;
388
        }
389
        if (! $first instanceof Closure) {
390
            // If the column is simply a string, we can assume the join simply has a basic
391
            // "on" clause with a single condition. So we will just build the join with
392
            // this simple join clauses attached to it. There is not a join callback.
393
394
            //where and on are the same for aql
395
            $method = $where ? 'where' : 'on';
396
397
            $this->joins[] = $join->$method($first, $operator, $second);
398
        }
399
400
        return $this;
401
    }
402
403
    /**
404
     * Add an "order by" clause to the query.
405
     *
406
     * @param Closure|IlluminateQueryBuilder|string $column
407
     * @param string                                $direction
408
     *
409
     * @throws InvalidArgumentException
410
     *
411
     * @return $this
412
     */
413
    public function orderBy($column, $direction = 'asc')
414
    {
415
        if ($this->isQueryable($column)) {
416
            [$query, $bindings] = $this->createSub($column);
417
418
            //fixme: Remove binding when implementing subqueries
419
            $bindings = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $bindings is dead and can be removed.
Loading history...
420
421
            $column = new Expression('(' . $query . ')');
422
        }
423
424
        $this->{$this->unions ? 'unionOrders' : 'orders'}[] = [
425
            'column'    => $column,
426
            'direction' => $direction,
427
        ];
428
429
        return $this;
430
    }
431
432
    /**
433
     * Add a raw "order by" clause to the query.
434
     *
435
     * @param string|ExpressionInterface $aql
436
     * @param array                      $bindings
437
     *
438
     * @return $this
439
     */
440
    public function orderByRaw($aql, $bindings = [])
441
    {
442
        $bindings = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $bindings is dead and can be removed.
Loading history...
443
444
        $type = 'Raw';
445
        $column = $aql;
446
        $this->{$this->unions ? 'unionOrders' : 'orders'}[] = compact('type', 'column');
447
448
        return $this;
449
    }
450
451
    /**
452
     * Put the query's results in random order.
453
     *
454
     * @param string $seed
455
     *
456
     * @return $this
457
     */
458
    public function inRandomOrder($seed = '')
459
    {
460
        // ArangoDB's random function doesn't accept a seed.
461
        $seed = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $seed is dead and can be removed.
Loading history...
462
463
        return $this->orderByRaw($this->grammar->compileRandom($this));
464
    }
465
466
    /**
467
     * Get a new join clause.
468
     *
469
     * @param IlluminateQueryBuilder $parentQuery
470
     * @param string                 $type
471
     * @param string                 $table
472
     *
473
     * @return JoinClause
474
     */
475
    protected function newJoinClause(IlluminateQueryBuilder $parentQuery, $type, $table)
476
    {
477
        return new JoinClause($parentQuery, $type, $table);
478
    }
479
}
480