Failed Conditions
Push — refactor/improve-static-analys... ( a7b39f...bdf823 )
by Bas
12:11
created

Builder::setAql()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 5
ccs 1
cts 1
cp 1
rs 10
cc 1
nc 1
nop 0
crap 1
1
<?php
2
3
namespace LaravelFreelancerNL\Aranguent\Query;
4
5
use Closure;
6
use Illuminate\Database\Query\Builder as IlluminateQueryBuilder;
7
use Illuminate\Database\Query\Expression;
8
use Illuminate\Support\Arr;
9
use InvalidArgumentException;
10
use LaravelFreelancerNL\Aranguent\Connection;
11
use LaravelFreelancerNL\Aranguent\Query\Concerns\BuildsJoinClauses;
12
use LaravelFreelancerNL\Aranguent\Query\Concerns\BuildsSubqueries;
13
use LaravelFreelancerNL\Aranguent\Query\Concerns\BuildsWhereClauses;
14
use LaravelFreelancerNL\FluentAQL\Exceptions\BindException;
15
use LaravelFreelancerNL\FluentAQL\Expressions\ExpressionInterface as ExpressionInterface;
16
use LaravelFreelancerNL\FluentAQL\Expressions\FunctionExpression;
17
use LaravelFreelancerNL\FluentAQL\QueryBuilder;
18
19
class Builder extends IlluminateQueryBuilder
20
{
21
    use BuildsJoinClauses;
22
    use BuildsSubqueries;
23
    use BuildsWhereClauses;
24
25
    /**
26
     * @var QueryBuilder|FunctionExpression
27
     */
28
    public QueryBuilder|FunctionExpression $aqb;
29
30
    /**
31
     * @var Connection
32
     */
33
    public $connection;
34
35
    /**
36
     * @var Grammar
37
     */
38
    public $grammar;
39
40
    /**
41
     * The current query value bindings.
42
     *
43
     * @var null|array{predicates: array<mixed>, options: array<string, string>}
44
     */
45
    public ?array $search = null;
46
47
    /**
48
     * The query variables that should be set.
49
     *
50
     * @var array<mixed>
51
     */
52
    public $variables = [];
53
54
    /**
55
     * ID of the query
56
     * Used as prefix for automatically generated bindings.
57
     *
58
     * @var int
59
     */
60 162
    protected $queryId = 1;
61
62
    /**
63
     * @override
64
     * Create a new query builder instance.
65
     */
66 162
    public function __construct(
67 162
        Connection $connection,
68 162
        Grammar $grammar = null,
69 162
        Processor $processor = null,
70 162
        QueryBuilder $aqb = null
71
    ) {
72 162
        $this->connection = $connection;
73 162
        $this->grammar = $grammar ?: $connection->getQueryGrammar();
74
        $this->processor = $processor ?: $connection->getPostProcessor();
75
        if (!$aqb instanceof QueryBuilder) {
76
            $aqb = new QueryBuilder();
77
        }
78
        $this->aqb = $aqb;
79
80
        $this->queryId = spl_object_id($this);
81
    }
82 12
83
    protected function bindValue($value, string $type = 'where')
84 12
    {
85 12
        if (! $value instanceof Expression) {
86 12
            $this->addBinding($this->flattenValue($value), 'where');
87
            $value = $this->replaceValueWithBindVariable($type);
88
        }
89
        return $value;
90
    }
91
92
    protected function generateBindVariable(string $type = 'where'): string
93
    {
94
        return $this->queryId . '_' . $type . '_' . (count($this->bindings[$type]) + 1);
95
    }
96 162
97
    /**
98 162
     * Add a binding to the query.
99
     *
100
     * @param  mixed  $value
101 162
     * @param  string  $type
102
     * @return IlluminateQueryBuilder
103 162
     *
104
     * @throws \InvalidArgumentException
105 162
     */
106
    public function addBinding($value, $type = 'where')
107
    {
108
        if (! array_key_exists($type, $this->bindings)) {
109
            throw new InvalidArgumentException("Invalid binding type: {$type}.");
110
        }
111
112
        $bindVariable = $this->generateBindVariable($type);
113 112
114
        if (is_array($value)) {
115 112
            $this->bindings[$type] = array_map(
116 112
                [$this, 'castBinding'],
117 112
                array_merge($this->bindings[$type], $value),
118 111
            );
119 111
        } else {
120
            $this->bindings[$type][$bindVariable] = $this->castBinding($value);
121
        }
122
123
        return $this;
124
    }
125
126
    protected function getLastBindVariable(string $type = 'where')
127
    {
128
        return array_key_last($this->bindings[$type]);
129 3
    }
130
131 3
    protected function replaceValueWithBindVariable(string $type = 'where')
132
    {
133 3
        return '@' . $this->getLastBindVariable($type);
134 3
    }
135 3
136 3
    public function getQueryId()
137
    {
138 3
        return $this->queryId;
139
    }
140
141
    /**
142
     * Get the current query value bindings in a flattened array.
143
     *
144
     * @return array
145
     */
146
    public function getBindings()
147 53
    {
148
        $extractedBindings = [];
149 53
        foreach ($this->bindings as $typeBinds) {
150 53
            foreach ($typeBinds as $key => $value) {
151 53
                $extractedBindings[$key] = $value;
152
            }
153 53
        }
154
        return $extractedBindings;
155 53
    }
156
157
    /**
158
     * Delete a record from the database.
159
     *
160
     * @param mixed $id
161
     *
162
     * @return int
163
     */
164 11
    public function delete($id = null)
165
    {
166 11
        $this->aqb = new QueryBuilder();
167
        $this->grammar->compileDelete($this, $id)->setAql();
168 11
        return $this->connection->delete($this->aqb);
169
    }
170 11
171
    /**
172
     * Set the table which the query is targeting.
173
     *
174
     * @param  \Closure|IlluminateQueryBuilder|string  $table
175
     * @param  string|null  $as
176 64
     * @return IlluminateQueryBuilder
177
     */
178 64
    public function from($table, $as = null)
179 64
    {
180
        if ($this->isQueryable($table)) {
181
            return $this->fromSub($table, $as);
182
        }
183
        $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

183
        $this->grammar->registerTableAlias(/** @scrutinizer ignore-type */ $table, $as);
Loading history...
184
185
        $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...
186
187
        return $this;
188
    }
189 64
190 64
    /**
191
     * Run a pagination count query.
192 64
     *
193
     * @param array<mixed> $columns
194
     *
195
     * @return array<mixed>
196
     */
197 64
    protected function runPaginationCountQuery($columns = ['*'])
198
    {
199
        $without = $this->unions ? ['orders', 'limit', 'offset'] : ['columns', 'orders', 'limit', 'offset'];
200
201
        $closeResults = $this->cloneWithout($without)
202
            ->cloneWithoutBindings($this->unions ? ['order'] : ['select', 'order'])
203
            ->setAggregate('count', $this->withoutSelectAliases($columns))
204 40
            ->get()->all();
205
206 40
        return $closeResults;
207 40
    }
208
209
    /**
210
     * Set the columns to be selected.
211
     *
212
     * @param  array<mixed>|mixed  $columns
213
     */
214
    public function select($columns = ['*']): IlluminateQueryBuilder
215
    {
216
        $this->columns = [];
217
        $this->bindings['select'] = [];
218
219 44
        $columns = is_array($columns) ? $columns : func_get_args();
220
        foreach ($columns as $as => $column) {
221 44
            if (is_string($as) && $this->isQueryable($column)) {
222 44
                $this->selectSub($column, $as);
223 44
            } else {
224 44
                $this->addColumns($columns);
225
            }
226
        }
227
228
        return $this;
229
    }
230
231
    /**
232 10
     * Add a new select column to the query.
233
     *
234 10
     * @param  array|mixed  $column
235 10
     * @return $this
236 10
     */
237
    public function addSelect($column)
238 10
    {
239
        $columns = is_array($column) ? $column : func_get_args();
240
241
        $this->addColumns($columns);
242
243
        return $this;
244
    }
245
246
    /**
247
     * @param array<mixed> $columns
248
     */
249
    protected function addColumns(array $columns): void
250 98
    {
251
        foreach ($columns as $as => $column) {
252 98
            if (is_string($as) && $this->isQueryable($column)) {
253 98
                if (is_null($this->columns)) {
254 98
                    $this->select($this->from . '.*');
255
                }
256 98
257
                $this->selectSub($column, $as);
258
259
                continue;
260
            }
261
262
            if (is_string($as)) {
263
                $this->columns[$as] = $column;
264
265 10
                continue;
266
            }
267 10
268
            $this->columns[] = $column;
269
        }
270 162
    }
271
272 162
    /**
273
     * Insert a new record into the database.
274 162
     *
275
     * @param array<mixed> $values
276
     *
277
     * @throws BindException
278
     *
279
     * @return bool
280
     */
281
    public function insert(array $values): bool
282
    {
283
        $this->grammar->compileInsert($this, $values)->setAql();
284 23
        $results = $this->getConnection()->insert($this->aqb);
0 ignored issues
show
Bug introduced by
It seems like $this->aqb can also be of type LaravelFreelancerNL\Flue...ions\FunctionExpression; however, parameter $query of Illuminate\Database\Connection::insert() 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

284
        $results = $this->getConnection()->insert(/** @scrutinizer ignore-type */ $this->aqb);
Loading history...
285
        $this->aqb = new QueryBuilder();
286 23
        return $results;
287
    }
288 23
289 23
    /**
290 23
     * Insert a new record and get the value of the primary key.
291 23
     *
292
     * @param array<mixed> $values
293
     */
294
    public function insertGetId(array $values, $sequence = null)
295
    {
296
        $this->grammar->compileInsertGetId($this, $values, $sequence)->setAql();
297
        $response = $this->getConnection()->execute($this->aqb);
0 ignored issues
show
Bug introduced by
It seems like $this->aqb can also be of type LaravelFreelancerNL\Flue...ions\FunctionExpression; however, parameter $query of LaravelFreelancerNL\Aran...t\Connection::execute() does only seem to accept LaravelFreelancerNL\FluentAQL\QueryBuilder|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

297
        $response = $this->getConnection()->execute(/** @scrutinizer ignore-type */ $this->aqb);
Loading history...
298
        $this->aqb = new QueryBuilder();
299
300
        return (is_array($response)) ? end($response) : $response;
301
    }
302 21
303
    /**
304 21
     * Insert a new record into the database.
305 21
     *
306 21
     * @param array<mixed> $values
307
     *
308 21
     * @throws BindException
309
     *
310 21
     * @return bool
311 21
     */
312
    public function insertOrIgnore(array $values): bool
313
    {
314
        $this->grammar->compileInsertOrIgnore($this, $values)->setAql();
315
        $results = $this->getConnection()->insert($this->aqb);
0 ignored issues
show
Bug introduced by
It seems like $this->aqb can also be of type LaravelFreelancerNL\Flue...ions\FunctionExpression; however, parameter $query of Illuminate\Database\Connection::insert() 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

315
        $results = $this->getConnection()->insert(/** @scrutinizer ignore-type */ $this->aqb);
Loading history...
316
        $this->aqb = new QueryBuilder();
317
318
        return $results;
319
    }
320
321
    protected function setAql()
322
    {
323
        $this->aqb = $this->aqb->get();
0 ignored issues
show
Bug introduced by
The method get() does not exist on LaravelFreelancerNL\Flue...ions\FunctionExpression. ( Ignorable by Annotation )

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

323
        /** @scrutinizer ignore-call */ 
324
        $this->aqb = $this->aqb->get();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
324
325 88
        return $this;
326
    }
327 88
328 88
    /**
329
     * Update a record in the database.
330
     *
331
     * @param array<mixed> $values
332
     *
333
     * @return int
334
     */
335
    public function update(array $values)
336
    {
337
        $this->aqb = new QueryBuilder();
338
339
        $this->grammar->compileUpdate($this, $values)->setAql();
340
        $results = $this->connection->update($this->aqb);
341
        $this->aqb = new QueryBuilder();
342 2
        return $results;
343
    }
344 2
345
    /**
346
     * Execute an aggregate function on the database.
347
     *
348
     * @param string $function
349
     * @param array<mixed>  $columns
350
     *
351
     * @return mixed
352
     */
353 2
    public function aggregate($function, $columns = ['*'])
354 2
    {
355 2
        $results = $this->cloneWithout($this->unions ? [] : ['columns'])
356
            ->setAggregate($function, $columns)
357
            ->get($columns);
358 2
359
        $this->aqb = new QueryBuilder();
360
361
        if (!$results->isEmpty()) {
362
            return array_change_key_case((array) $results[0])['aggregate'];
363
        }
364
365
        return false;
366
    }
367
368
369 1
    /**
370
     * Determine if the given operator is supported.
371 1
     *
372
     * @param string $operator
373 1
     *
374 1
     * @return bool
375 1
     */
376
    protected function invalidOperator($operator)
377 1
    {
378
        return !in_array(strtolower($operator), $this->operators, true) &&
379
            !isset($this->grammar->getOperators()[strtoupper($operator)]);
380
    }
381
382
383
    /**
384
     * Add an "order by" clause to the query.
385
     *
386
     * @param Closure|IlluminateQueryBuilder|string $column
387
     * @param string                                $direction
388 1
     *
389
     * @throws InvalidArgumentException
390
     *
391 1
     * @return $this
392
     */
393 1
    public function orderBy($column, $direction = 'asc')
394
    {
395
        if ($this->isQueryable($column)) {
396
            [$query, $bindings] = $this->createSub($column);
397
398
            //fixme: Remove binding when implementing subqueries
399
            $bindings = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $bindings is dead and can be removed.
Loading history...
400
401
            $column = new Expression('(' . $query . ')');
402
        }
403 7
404
        $this->{$this->unions ? 'unionOrders' : 'orders'}[] = [
405 7
            'column'    => $column,
406 2
            'direction' => $direction,
407
        ];
408
409 7
        return $this;
410 4
    }
411
412
    /**
413 7
     * Add a raw "order by" clause to the query.
414 7
     *
415 7
     * @param string|ExpressionInterface $aql
416
     * @param array<mixed>                      $bindings
417
     *
418 7
     * @return $this
419
     */
420
    public function orderByRaw($aql, $bindings = [])
421
    {
422
        $bindings = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $bindings is dead and can be removed.
Loading history...
423
424
        $type = 'Raw';
425
        $column = $aql;
426 6
        $this->{$this->unions ? 'unionOrders' : 'orders'}[] = compact('type', 'column');
427
428 6
        return $this;
429
    }
430
431
432
    /**
433
     * Put the query's results in random order.
434
     *
435
     * @param string $seed
436 137
     *
437
     * @return $this
438 137
     */
439
    public function inRandomOrder($seed = '')
440
    {
441
        // ArangoDB's random function doesn't accept a seed.
442
        $seed = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $seed is dead and can be removed.
Loading history...
443
444
        return $this->orderByRaw($this->grammar->compileRandom($this));
0 ignored issues
show
Bug introduced by
$this of type LaravelFreelancerNL\Aranguent\Query\Builder is incompatible with the type string expected by parameter $seed of LaravelFreelancerNL\Aran...rammar::compileRandom(). ( Ignorable by Annotation )

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

444
        return $this->orderByRaw($this->grammar->compileRandom(/** @scrutinizer ignore-type */ $this));
Loading history...
445
    }
446
447
    /**
448
     * Search an ArangoSearch view.
449
     *
450
     * @param mixed $predicates
451
     * @param array|null $options
452
     * @return Builder
453
     */
454
    public function search(mixed $predicates, array $options = null): Builder
455
    {
456
        if ($predicates instanceof Closure) {
457
            $predicates = $predicates($this->aqb);
458
        }
459
460
        if (! is_array($predicates)) {
461
            $predicates = [$predicates];
462
        }
463
464
        $this->search = [
465
            'predicates' => $predicates,
466
            'options' => $options
467
        ];
468
469
        return $this;
470
    }
471
472
    /**
473
     * Create a new query instance for a sub-query.
474
     *
475
     * @return $this
476
     */
477
    protected function forSubQuery()
478
    {
479
        return $this->newQuery();
480
    }
481
482
    /**
483
     * Get the database connection instance.
484
     *
485
     * @return Connection
486
     */
487
    public function getConnection()
488
    {
489
        return $this->connection;
490
    }
491
}
492