Passed
Push — master ( 8d031b...eddbb7 )
by Bas
22:43 queued 12s
created

Builder::replaceTableForAlias()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 7
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 11
rs 10
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
     * @var Grammar
22
     */
23
    public $grammar;
24
25
    /**
26
     * @var Connection
27
     */
28
    public $connection;
29
30
    /**
31
     * @var QueryBuilder
32
     */
33
    public $aqb;
34
35
    /**
36
     * Alias' are AQL variables
37
     * Sticking with the SQL based naming as this is the Laravel driver.
38
     * @var QueryBuilder
39
     */
40
    protected $aliasRegistry = [];
41
42
    /**
43
     * @override
44
     * Create a new query builder instance.
45
     *
46
     * @param ConnectionInterface $connection
47
     * @param Grammar $grammar
48
     * @param Processor $processor
49
     * @param QueryBuilder|null $aqb
50
     */
51
    public function __construct(
52
        ConnectionInterface $connection,
53
        Grammar $grammar = null,
54
        Processor $processor = null,
55
        QueryBuilder $aqb = null
56
    ) {
57
        $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...
58
        $this->grammar = $grammar ?: $connection->getQueryGrammar();
59
        $this->processor = $processor ?: $connection->getPostProcessor();
60
        if (! $aqb instanceof QueryBuilder) {
61
            $aqb = new QueryBuilder();
62
        }
63
        $this->aqb = $aqb;
64
    }
65
66
    /**
67
     * Run the query as a "select" statement against the connection.
68
     *
69
     * @return array
70
     */
71
    protected function runSelect()
72
    {
73
        $response = $this->connection->select($this->grammar->compileSelect($this)->aqb);
74
        $this->aqb = new QueryBuilder();
75
76
        return $response;
77
    }
78
79
    /**
80
     * Run a pagination count query.
81
     *
82
     * @param  array  $columns
83
     * @return array
84
     */
85
    protected function runPaginationCountQuery($columns = ['*'])
86
    {
87
        $without = $this->unions ? ['orders', 'limit', 'offset'] : ['columns', 'orders', 'limit', 'offset'];
88
89
        $closeResults = $this->cloneWithout($without)
90
            ->cloneWithoutBindings($this->unions ? ['order'] : ['select', 'order'])
91
            ->setAggregate('count', $this->withoutSelectAliases($columns))
92
            ->get()->all();
93
94
        $this->aqb = new QueryBuilder();
95
96
        return $closeResults;
97
    }
98
99
    /**
100
     * Get the SQL representation of the query.
101
     *
102
     * @return string
103
     */
104
    public function toSql()
105
    {
106
        return $this->grammar->compileSelect($this)->aqb->query;
107
    }
108
109
    /**
110
     * Insert a new record into the database.
111
     * @param array $values
112
     * @return bool
113
     * @throws BindException
114
     */
115
    public function insert(array $values): bool
116
    {
117
        $response = $this->getConnection()->insert($this->grammar->compileInsert($this, $values)->aqb);
118
        $this->aqb = new QueryBuilder();
119
120
        return $response;
121
    }
122
123
    /**
124
     * Insert a new record and get the value of the primary key.
125
     *
126
     * @param array $values
127
     * @param string|null $sequence
128
     * @return int
129
     * @throws BindException
130
     */
131
    public function insertGetId(array $values, $sequence = null)
132
    {
133
        $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

133
        $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...
134
        $this->aqb = new QueryBuilder();
135
        return (is_array($response)) ? end($response) : $response;
0 ignored issues
show
introduced by
The condition is_array($response) is always true.
Loading history...
136
    }
137
138
    /**
139
     * Set the table which the query is targeting.
140
     *
141
     * @param  Closure|Builder|string  $table
142
     * @param  string|null  $as
143
     * @return $this
144
     */
145
    public function from($table, $as = null)
146
    {
147
        if ($this->isQueryable($table)) {
148
            return $this->fromSub($table, $as);
149
        }
150
151
        if (stripos($table, ' as ') !== false) {
0 ignored issues
show
Bug introduced by
It seems like $table can also be of type Closure and LaravelFreelancerNL\Aranguent\Query\Builder; however, parameter $haystack of stripos() 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

151
        if (stripos(/** @scrutinizer ignore-type */ $table, ' as ') !== false) {
Loading history...
152
            $parts = explode(' as ', $table);
0 ignored issues
show
Bug introduced by
It seems like $table can also be of type Closure and LaravelFreelancerNL\Aranguent\Query\Builder; however, parameter $string of explode() 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

152
            $parts = explode(' as ', /** @scrutinizer ignore-type */ $table);
Loading history...
153
            $table = $parts[0];
154
            $as = $parts[1];
155
            $this->registerAlias($table, $as);
156
        }
157
158
        $this->from = $table;
0 ignored issues
show
Documentation Bug introduced by
It seems like $table can also be of type Closure or LaravelFreelancerNL\Aranguent\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...
159
160
        return $this;
161
    }
162
163
    /**
164
     * Execute the query as a "select" statement.
165
     *
166
     * @param  array|string  $columns
167
     * @return Collection
168
     */
169
    public function get($columns = ['*'])
170
    {
171
        $results = collect($this->onceWithColumns(Arr::wrap($columns), function () {
172
            return $this->runSelect();
173
        }));
174
175
        return $results;
176
    }
177
178
    /**
179
     * Update a record in the database.
180
     *
181
     * @param  array  $values
182
     * @return int
183
     */
184
    public function update(array $values)
185
    {
186
        $response = $this->connection->update($this->grammar->compileUpdate($this, $values)->aqb);
187
        $this->aqb = new QueryBuilder();
188
189
        return $response;
190
    }
191
192
    /**
193
     * Delete a record from the database.
194
     *
195
     * @param  mixed  $_key
196
     * @return int
197
     */
198
    public function delete($_key = null)
199
    {
200
        $response = $this->connection->delete($this->grammar->compileDelete($this, $_key)->aqb);
201
        $this->aqb = new QueryBuilder();
202
203
        return $response;
204
    }
205
206
    /**
207
     * @param Builder $builder
208
     * @param $table
209
     * @param string $postfix
210
     * @return mixed
211
     */
212
    public function generateTableAlias($table, $postfix = 'Doc')
213
    {
214
        $alias = Str::singular($table) . $postfix;
215
        $this->registerAlias($table, $alias);
216
217
        return $alias;
218
    }
219
220
    /**
221
     * @param string $table
222
     * @param string $alias
223
     */
224
    public function registerAlias(string $table, string $alias): void
225
    {
226
        if (! isset($this->aliasRegistry[$table])) {
227
            $this->aliasRegistry[$table] = $alias;
228
        }
229
    }
230
231
    /**
232
     * @param string $table
233
     * @return string
234
     */
235
    public function getAlias(string $table): ?string
236
    {
237
        if (isset($this->aliasRegistry[$table])) {
238
            return $this->aliasRegistry[$table];
239
        }
240
        if (in_array($table, $this->aliasRegistry)) {
241
            return $table;
242
        }
243
        return null;
244
    }
245
246
    public function replaceTableForAlias($reference): string
247
    {
248
        $referenceParts = explode('.', $reference);
249
        $table = array_shift($referenceParts);
250
        $alias = $this->getAlias($table);
251
        if ($alias == null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $alias of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
252
            $alias = $this->generateTableAlias($table);
253
        }
254
        array_unshift($referenceParts, $alias);
255
256
        return implode('.', $referenceParts);
257
    }
258
259
    /**
260
     * Execute an aggregate function on the database.
261
     *
262
     * @param  string  $function
263
     * @param  array   $columns
264
     * @return mixed
265
     */
266
    public function aggregate($function, $columns = ['*'])
267
    {
268
        $results = $this->cloneWithout($this->unions ? [] : ['columns'])
269
            ->setAggregate($function, $columns)
270
            ->get($columns);
271
272
        $this->aqb = new QueryBuilder();
273
274
        if (! $results->isEmpty()) {
275
            return array_change_key_case((array) $results[0])['aggregate'];
276
        }
277
278
        return false;
279
    }
280
281
282
    /**
283
     * Add a basic where clause to the query.
284
     *
285
     * @param Closure|string|array $column
286
     * @param mixed $operator
287
     * @param mixed $value
288
     * @param string $boolean
289
     * @return Builder
290
     */
291
    public function where($column, $operator = null, $value = null, $boolean = 'and')
292
    {
293
        // If the column is an array, we will assume it is an array of key-value pairs
294
        // and can add them each as a where clause. We will maintain the boolean we
295
        // received when the method was called and pass it into the nested where.
296
        if (is_array($column)) {
297
            return $this->addArrayOfWheres($column, $boolean);
298
        }
299
300
        // Here we will make some assumptions about the operator. If only 2 values are
301
        // passed to the method, we will assume that the operator is an equals sign
302
        // and keep going. Otherwise, we'll require the operator to be passed in.
303
        [$value, $operator] = $this->prepareValueAndOperator(
304
            $value,
305
            $operator,
306
            func_num_args() === 2
307
        );
308
309
        // If the columns is actually a Closure instance, we will assume the developer
310
        // wants to begin a nested where statement which is wrapped in parenthesis.
311
        // We'll add that Closure to the query then return back out immediately.
312
        if ($column instanceof Closure) {
313
            return $this->whereNested($column, $boolean);
314
        }
315
316
        // If the given operator is not found in the list of valid operators we will
317
        // assume that the developer is just short-cutting the '=' operators and
318
        // we will set the operators to '==' and set the values appropriately.
319
        if ($this->invalidOperator($operator)) {
320
            [$value, $operator] = [$operator, '=='];
321
        }
322
323
        // If the value is a Closure, it means the developer is performing an entire
324
        // sub-select within the query and we will need to compile the sub-select
325
        // within the where clause to get the appropriate query record results.
326
        if ($value instanceof Closure) {
327
            return $this->whereSub($column, $operator, $value, $boolean);
328
        }
329
330
        $type = 'Basic';
331
332
        // If the column is making a JSON reference we'll check to see if the value
333
        // is a boolean. If it is, we'll add the raw boolean string as an actual
334
        // value to the query to ensure this is properly handled by the query.
335
        if (Str::contains($column, '->') && is_bool($value)) {
336
            $value = new Expression($value ? 'true' : 'false');
337
338
            if (is_string($column)) {
0 ignored issues
show
introduced by
The condition is_string($column) is always true.
Loading history...
339
                $type = 'JsonBoolean';
340
            }
341
        }
342
343
        // Now that we are working with just a simple query we can put the elements
344
        // in our array and add the query binding to our array of bindings that
345
        // will be bound to each SQL statements when it is finally executed.
346
        $this->wheres[] = compact(
347
            'type',
348
            'column',
349
            'operator',
350
            'value',
351
            'boolean'
352
        );
353
354
        if (! $value instanceof Expression) {
355
            $this->addBinding($value, 'where');
356
        }
357
358
        return $this;
359
    }
360
361
    /**
362
     * Add a "where null" clause to the query.
363
     *
364
     * @param  string|array  $columns
365
     * @param  string  $boolean
366
     * @param  bool    $not
367
     * @return $this
368
     */
369
    public function whereNull($columns, $boolean = 'and', $not = false)
370
    {
371
        $type = 'Basic';
372
        $operator = $not ? '!=' : '==';
373
        $value = null;
374
375
        foreach (Arr::wrap($columns) as $column) {
376
            $this->wheres[] = compact('type', 'column', 'operator', 'value', 'boolean');
377
        }
378
379
        return $this;
380
    }
381
382
    /**
383
     * Determine if the given operator is supported.
384
     *
385
     * @param  string  $operator
386
     * @return bool
387
     */
388
    protected function invalidOperator($operator)
389
    {
390
        return ! in_array(strtolower($operator), $this->operators, true) &&
391
            ! isset($this->grammar->getOperators()[strtoupper($operator)]);
392
    }
393
394
    /**
395
     * Add a join clause to the query.
396
     *
397
     * @param  string  $table
398
     * @param  \Closure|string  $first
399
     * @param  string|null  $operator
400
     * @param  string|null  $second
401
     * @param  string  $type
402
     * @param  bool  $where
403
     * @return $this
404
     */
405
    public function join($table, $first, $operator = null, $second = null, $type = 'inner', $where = false)
406
    {
407
        $this->registerAlias($table, $this->generateTableAlias($table));
408
        $first = $this->replaceTableForAlias($first);
409
        $second = $this->replaceTableForAlias($second);
410
411
        $join = $this->newJoinClause($this, $type, $table);
412
413
        // If the first "column" of the join is really a Closure instance the developer
414
        // is trying to build a join with a complex "on" clause containing more than
415
        // one condition, so we'll add the join and call a Closure with the query.
416
        if ($first instanceof Closure) {
0 ignored issues
show
introduced by
$first is never a sub-type of Closure.
Loading history...
417
            $first($join);
418
419
            $this->joins[] = $join;
420
421
            //we'll take care of the bindings when calling fluentaql
422
            $this->addBinding($join->getBindings(), 'join');
423
        }
424
425
        // If the column is simply a string, we can assume the join simply has a basic
426
        // "on" clause with a single condition. So we will just build the join with
427
        // this simple join clauses attached to it. There is not a join callback.
428
        else {
429
            //where and on are the same for aql
430
            $method = $where ? 'where' : 'on';
431
432
            $this->joins[] = $join->$method($first, $operator, $second);
433
434
            //we'll take care of the bindings when calling fluentaql
435
            $this->addBinding($join->getBindings(), 'join');
436
        }
437
438
        return $this;
439
    }
440
441
442
    /**
443
     * Add an "or where" clause to the query.
444
     *
445
     * @param Closure|string|array  $column
446
     * @param  mixed  $operator
447
     * @param  mixed  $value
448
     * @return IlluminateQueryBuilder|static
449
     */
450
    public function orWhere($column, $operator = null, $value = null)
451
    {
452
        return $this->where($column, $operator, $value, 'or');
453
    }
454
455
    /**
456
     * Add an "order by" clause to the query.
457
     *
458
     * @param  Closure|IlluminateQueryBuilder|string  $column
459
     * @param  string  $direction
460
     * @return $this
461
     *
462
     * @throws InvalidArgumentException
463
     */
464
    public function orderBy($column, $direction = 'asc')
465
    {
466
        if ($this->isQueryable($column)) {
467
            [$query, $bindings] = $this->createSub($column);
468
469
            $column = new Expression('(' . $query . ')');
470
        }
471
472
        $this->{$this->unions ? 'unionOrders' : 'orders'}[] = [
473
            'column' => $column,
474
            'direction' => $direction,
475
        ];
476
477
        return $this;
478
    }
479
480
    /**
481
     * Add a raw "order by" clause to the query.
482
     *
483
     * @param string|ExpressionInterface $aql
484
     * @param array $bindings
485
     * @return $this
486
     */
487
    public function orderByRaw($aql, $bindings = [])
488
    {
489
        $type = 'Raw';
490
        $column = $aql;
491
        $this->{$this->unions ? 'unionOrders' : 'orders'}[] = compact('type', 'column');
492
493
        return $this;
494
    }
495
496
    /**
497
     * Put the query's results in random order.
498
     *
499
     * @param  string  $seed
500
     * @return $this
501
     */
502
    public function inRandomOrder($seed = '')
503
    {
504
        return $this->orderByRaw($this->grammar->compileRandom($this));
505
    }
506
507
    /**
508
     * Get a new join clause.
509
     *
510
     * @param  IlluminateQueryBuilder  $parentQuery
511
     * @param  string  $type
512
     * @param  string  $table
513
     * @return JoinClause
514
     */
515
    protected function newJoinClause(IlluminateQueryBuilder $parentQuery, $type, $table)
516
    {
517
        return new JoinClause($parentQuery, $type, $table);
518
    }
519
}
520