Failed Conditions
Push — refactor/improve-static-analys... ( 740b03...f2a08f )
by Bas
06:58 queued 12s
created

Builder   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 467
Duplicated Lines 0 %

Test Coverage

Coverage 91.94%

Importance

Changes 7
Bugs 3 Features 0
Metric Value
wmc 56
eloc 154
c 7
b 3
f 0
dl 0
loc 467
ccs 114
cts 124
cp 0.9194
rs 5.5199

20 Methods

Rating   Name   Duplication   Size   Complexity  
A union() 0 9 2
A inRandomOrder() 0 6 1
A selectSub() 0 7 1
A select() 0 16 5
A orderBy() 0 22 5
A set() 0 33 4
A getBindings() 0 10 3
A exists() 0 20 2
A from() 0 10 2
A runPaginationCountQuery() 0 10 3
A __construct() 0 15 4
A addSelect() 0 7 2
B orderByRaw() 0 20 7
A delete() 0 15 2
A addColumns() 0 20 6
A aggregate() 0 12 3
A getQueryId() 0 3 1
A getConnection() 0 3 1
A forSubQuery() 0 10 1
A toAql() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Builder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Builder, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace LaravelFreelancerNL\Aranguent\Query;
4
5
use Illuminate\Database\Query\Builder as IlluminateQueryBuilder;
6
use Illuminate\Database\Query\Expression;
7
use InvalidArgumentException;
8
use LaravelFreelancerNL\Aranguent\Connection;
9
use LaravelFreelancerNL\Aranguent\Query\Concerns\BuildsGroups;
10
use LaravelFreelancerNL\Aranguent\Query\Concerns\BuildsSearches;
11
use LaravelFreelancerNL\Aranguent\Query\Concerns\BuildsInserts;
12
use LaravelFreelancerNL\Aranguent\Query\Concerns\BuildsJoins;
13
use LaravelFreelancerNL\Aranguent\Query\Concerns\BuildsSubqueries;
14
use LaravelFreelancerNL\Aranguent\Query\Concerns\BuildsUpdates;
15
use LaravelFreelancerNL\Aranguent\Query\Concerns\BuildsWheres;
16
use LaravelFreelancerNL\Aranguent\Query\Concerns\ConvertsIdToKey;
17
use LaravelFreelancerNL\Aranguent\Query\Concerns\HandlesAliases;
18
use LaravelFreelancerNL\Aranguent\Query\Concerns\HandlesBindings;
19
use LaravelFreelancerNL\FluentAQL\QueryBuilder as AQB;
20
use phpDocumentor\Reflection\Types\Boolean;
21
22
class Builder extends IlluminateQueryBuilder
23
{
24
    use BuildsGroups;
0 ignored issues
show
introduced by
The trait LaravelFreelancerNL\Aran...y\Concerns\BuildsGroups requires some properties which are not provided by LaravelFreelancerNL\Aranguent\Query\Builder: $end, $start
Loading history...
25
    use BuildsInserts;
26
    use BuildsJoins;
27
    use BuildsSearches;
28
    use BuildsSubqueries;
29
    use BuildsUpdates;
30
    use BuildsWheres;
31
    use ConvertsIdToKey;
32
    use HandlesAliases;
33
    use HandlesBindings;
34
35
    public AQB $aqb;
36
37
    /**
38
     * The current query value bindings.
39
     *
40
     * @var array
41
     */
42
    public $bindings = [
43
        'variable' => [],
44
        'from' => [],
45
        'search' => [],
46
        'join' => [],
47
        // TODO: another set of variabes?
48
        'where' => [],
49
        'groupBy' => [],
50
        'having' => [],
51
        'order' => [],
52
        'union' => [],
53
        'unionOrder' => [],
54
        'select' => [],
55
        'insert' => [],
56
        'update' => [],
57
        'upsert' => [],
58
    ];
59
60 162
61
    /**
62
     * @var Connection
63
     */
64
    public $connection;
65
66 162
    /**
67 162
     * @var Grammar
68 162
     */
69 162
    public $grammar;
70 162
71
    /**
72 162
     * The current query value bindings.
73 162
     *
74
     * @var null|array{fields: array<string>, searchTokens: array<string>, analyzer: string|null}
75
     */
76
    public ?array $search = null;
77
78
    /**
79
     * The query variables that should be set.
80
     *
81
     * @var array<mixed>
82 12
     */
83
    public $variables = [];
84 12
85 12
    /**
86 12
     * ID of the query
87
     * Used as prefix for automatically generated bindings.
88
     *
89
     * @var int
90
     */
91
    protected $queryId = 1;
92
93
    /**
94
     * @override
95
     * Create a new query builder instance.
96 162
     */
97
    public function __construct(
98 162
        Connection $connection,
99
        Grammar $grammar = null,
100
        Processor $processor = null,
101 162
        AQB $aqb = null
102
    ) {
103 162
        $this->connection = $connection;
104
        $this->grammar = $grammar ?: $connection->getQueryGrammar();
105 162
        $this->processor = $processor ?: $connection->getPostProcessor();
106
        if (!$aqb instanceof AQB) {
107
            $aqb = new AQB();
108
        }
109
        $this->aqb = $aqb;
110
111
        $this->queryId = spl_object_id($this);
112
    }
113 112
114
    public function getQueryId()
115 112
    {
116 112
        return $this->queryId;
117 112
    }
118 111
119 111
    /**
120
     * Get the current query value bindings in a flattened array.
121
     *
122
     * @return array
123
     */
124
    public function getBindings()
125
    {
126
        $extractedBindings = [];
127
        foreach ($this->bindings as $typeBinds) {
128
            foreach ($typeBinds as $key => $value) {
129 3
                $extractedBindings[$key] = $value;
130
            }
131 3
        }
132
133 3
        return $extractedBindings;
134 3
    }
135 3
136 3
    /**
137
     * Set the table which the query is targeting.
138 3
     *
139
     * @param \Closure|IlluminateQueryBuilder|string $table
140
     * @param string|null $as
141
     * @return IlluminateQueryBuilder
142
     */
143
    public function from($table, $as = null)
144
    {
145
        if ($this->isQueryable($table)) {
146
            return $this->fromSub($table, $as);
147 53
        }
148
        $this->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 Illuminate\Database\Query\Expression|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

148
        $this->registerTableAlias(/** @scrutinizer ignore-type */ $table, $as);
Loading history...
149 53
150 53
        $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...
151 53
152
        return $this;
153 53
    }
154
155 53
    /**
156
     * Run a pagination count query.
157
     *
158
     * @param array<mixed> $columns
159
     * @return array<mixed>
160
     */
161
    protected function runPaginationCountQuery($columns = ['*'])
162
    {
163
        $without = $this->unions ? ['orders', 'limit', 'offset'] : ['columns', 'orders', 'limit', 'offset'];
164 11
165
        $closeResults = $this->cloneWithout($without)
166 11
            ->cloneWithoutBindings($this->unions ? ['order'] : ['select', 'order'])
167
            ->setAggregate('count', $this->withoutSelectAliases($columns))
168 11
            ->get()->all();
169
170 11
        return $closeResults;
171
    }
172
173
    /**
174
     * Set the columns to be selected.
175
     *
176 64
     * @param array<mixed>|mixed $columns
177
     */
178 64
    public function select($columns = ['*']): IlluminateQueryBuilder
179 64
    {
180
        $this->columns = [];
181
        $this->bindings['select'] = [];
182
183
        $columns = is_array($columns) ? $columns : func_get_args();
184
185
        foreach ($columns as $as => $column) {
186
            if (is_string($as) && $this->isQueryable($column)) {
187
                $this->selectSub($column, $as);
188
            } else {
189 64
                $this->addColumns([$as => $column]);
190 64
            }
191
        }
192 64
193
        return $this;
194
    }
195
    /**
196
     * Add a subselect expression to the query.
197 64
     *
198
     * @param  \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder|string  $query
199
     * @param  string  $as
200
     * @return $this
201
     *
202
     * @throws \InvalidArgumentException
203
     */
204 40
    public function selectSub($query, $as)
205
    {
206 40
        [$query, $bindings] = $this->createSub($query);
207 40
208
        $this->addColumns([$as => new Expression('(' . $query . ')')]);
209
        $this->registerTableAlias($as, $as);
210
        return $this;
211
    }
212
213
    /**
214
     * Add a new select column to the query.
215
     *
216
     * @param array|mixed $column
217
     * @return $this
218
     */
219 44
    public function addSelect($column)
220
    {
221 44
        $columns = is_array($column) ? $column : func_get_args();
222 44
223 44
        $this->addColumns($columns);
224 44
225
        return $this;
226
    }
227
228
    /**
229
     * @param array<mixed> $columns
230
     */
231
    protected function addColumns(array $columns): void
232 10
    {
233
        foreach ($columns as $as => $column) {
234 10
            if (is_string($as) && $this->isQueryable($column)) {
235 10
                if (is_null($this->columns)) {
236 10
                    $this->select($this->from . '.*');
237
                }
238 10
239
                $this->selectSub($column, $as);
240
241
                continue;
242
            }
243
244
            if (is_string($as)) {
245
                $this->columns[$as] = $column;
246
247
                continue;
248
            }
249
250 98
            $this->columns[] = $column;
251
        }
252 98
    }
253 98
254 98
    /**
255
     * Add a union statement to the query.
256 98
     *
257
     * @param  \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder  $query
258
     * @param  bool  $all
259
     * @return $this
260
     */
261
    public function union($query, $all = false)
262
    {
263
        if ($query instanceof \Closure) {
264
            $query($query = $this->newQuery());
265 10
        }
266
        $this->importBindings($query);
0 ignored issues
show
Bug introduced by
It seems like $query can also be of type Illuminate\Database\Eloquent\Builder; however, parameter $query of LaravelFreelancerNL\Aran...ilder::importBindings() does only seem to accept Illuminate\Database\Query\Builder, 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

266
        $this->importBindings(/** @scrutinizer ignore-type */ $query);
Loading history...
267 10
        $this->unions[] = compact('query', 'all');
268
269
        return $this;
270 162
    }
271
272 162
273
    /**
274 162
     * Delete records from the database.
275
     *
276
     * @param mixed $id
277
     * @return int
278
     */
279
    public function delete($id = null)
280
    {
281
        // If an ID is passed to the method, we will set the where clause to check the
282
        // ID to let developers to simply and quickly remove a single row from this
283
        // database without manually specifying the "where" clauses on the query.
284 23
        if (!is_null($id)) {
285
            $this->where($this->from . '._key', '=', $id);
286 23
        }
287
288 23
        $this->applyBeforeQueryCallbacks();
289 23
290 23
        return $this->connection->delete(
291 23
            $this->grammar->compileDelete($this),
292
            $this->cleanBindings(
293
                $this->grammar->prepareBindingsForDelete($this->bindings)
294
            )
295
        );
296
    }
297
298
    /**
299
     * Determine if any rows exist for the current query.
300
     *
301
     * @return bool
302 21
     */
303
    public function exists()
304 21
    {
305 21
        $this->applyBeforeQueryCallbacks();
306 21
307
        $results = $this->connection->select(
308 21
            $this->grammar->compileExists($this),
309
            $this->getBindings(),
310 21
            !$this->useWritePdo
311 21
        );
312
313
        // If the results have rows, we will get the row and see if the exists column is a
314
        // boolean true. If there are no results for this query we will return false as
315
        // there are no rows for this query at all, and we can return that info here.
316
        if (isset($results[0])) {
317
            $results = (array) $results[0];
318
319
            return (bool) $results['exists'];
320
        }
321
322
        return false;
323
    }
324
325 88
326
    /**
327 88
     * Execute an aggregate function on the database.
328 88
     *
329
     * @param string $function
330
     * @param array<mixed> $columns
331
     * @return mixed
332
     */
333
    public function aggregate($function, $columns = ['*'])
334
    {
335
        $results = $this->cloneWithout($this->unions ? [] : ['columns'])
336
            ->setAggregate($function, $columns)
337
            ->get($columns);
338
339
340
        if (!$results->isEmpty()) {
341
            return array_change_key_case((array)$results[0])['aggregate'];
342 2
        }
343
344 2
        return false;
345
    }
346
347
    /**
348
     * Add an "order by" clause to the query.
349
     *
350
     * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder|\Illuminate\Contracts\Database\Query\Expression|string $column
351
     * @param string $direction
352
     * @return $this
353 2
     *
354 2
     * @throws \InvalidArgumentException
355 2
     */
356
    public function orderBy($column, $direction = 'asc')
357
    {
358 2
        if ($this->isQueryable($column)) {
359
            [$query, $bindings] = $this->createSub($column);
360
361
            $column = new Expression('(' . $query . ')');
362
363
            $this->addBinding($bindings, $this->unions ? 'unionOrder' : 'order');
364
        }
365
366
        $direction = strtoupper($direction);
367
368
        if (!in_array($direction, ['ASC', 'DESC'], true)) {
369 1
            throw new InvalidArgumentException('Order direction must be "asc" or "desc".');
370
        }
371 1
372
        $this->{$this->unions ? 'unionOrders' : 'orders'}[] = [
373 1
            'column' => $column,
374 1
            'direction' => $direction,
375 1
        ];
376
377 1
        return $this;
378
    }
379
380
    public function orderByRaw($sql, $bindings = [])
381
    {
382
        $type = 'Raw';
383
384
        $sql = new Expression($sql);
385
386
        $this->{$this->unions ? 'unionOrders' : 'orders'}[] = compact('type', 'sql');
387
388 1
        if (!isset($this->bindings[$this->unions ? 'unionOrders' : 'orders'])) {
389
            $this->bindings[$this->unions ? 'unionOrders' : 'orders'] = $bindings;
390
391 1
            return $this;
392
        }
393 1
394
        $this->bindings[$this->unions ? 'unionOrders' : 'orders'] = array_merge(
395
            $this->bindings[$this->unions ? 'unionOrders' : 'orders'],
396
            $bindings
397
        );
398
399
        return $this;
400
    }
401
402
    /**
403 7
     * Put the query's results in random order.
404
     *
405 7
     * @param string $seed
406 2
     * @return $this
407
     */
408
    public function inRandomOrder($seed = '')
409 7
    {
410 4
        // ArangoDB's random function doesn't accept a seed.
411
        unset($seed);
412
413 7
        return $this->orderByRaw($this->grammar->compileRandom());
414 7
    }
415 7
416
417
    /**
418 7
     * Set a variable
419
     */
420
    public function set(string $variable, IlluminateQueryBuilder|Expression|array|Boolean|Int|Float|String $value): Builder
421
    {
422
        if ($value instanceof Expression) {
0 ignored issues
show
introduced by
$value is never a sub-type of Illuminate\Database\Query\Expression.
Loading history...
423
            $this->variables[$variable] = $value->getValue($this->grammar);
424
425
            return $this;
426 6
        }
427
428 6
        if ($value instanceof Builder) {
0 ignored issues
show
introduced by
$value is never a sub-type of LaravelFreelancerNL\Aranguent\Query\Builder.
Loading history...
429
            $value->registerTableAlias($this->from);
430
            $value->grammar->compileSelect($value);
431
432
            $subquery = '(' . $value->toSql() . ')';
433
434
            // ArangoDB always returns an array of results. SQL will return a singular result
435
            // To mimic the same behaviour we take the first result.
436 137
            if ($value->hasLimitOfOne($value)) {
437
                $subquery = 'FIRST(' . $subquery . ')';
438 137
            }
439
440
            $this->bindings = array_merge(
441
                $this->bindings,
442
                $value->bindings
443
            );
444
445
            $this->variables[$variable] = $subquery;
446
447
            return $this;
448
449
        }
450
        $this->variables[$variable] = $this->bindValue($value, 'variable');
451
452
        return $this;
453
    }
454
455
456
    /**
457
     * Create a new query instance for sub-query.
458
     *
459
     * @return \Illuminate\Database\Query\Builder
460
     */
461
    protected function forSubQuery()
462
    {
463
464
        $query = $this->newQuery();
465
466
        assert($query instanceof Builder);
467
468
        $query->importTableAliases($this);
469
470
        return $query;
471
    }
472
473
    /**
474
     * Get the database connection instance.
475
     *
476
     * @return Connection
477
     */
478
    public function getConnection()
479
    {
480
        return $this->connection;
481
    }
482
483
    /**
484
     * Get the AQL representation of the query.
485
     */
486
    public function toAql(): string
487
    {
488
        return $this->toSql();
489
    }
490
}
491