Passed
Push — master ( 0ba402...934d75 )
by Bas
05:33
created

Builder::from()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 3
nop 2
dl 0
loc 16
rs 9.9666
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
     * @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 string $table
208
     * @param string $alias
209
     */
210
    public function registerAlias(string $table, string $alias): void
211
    {
212
        if (! isset($this->aliasRegistry[$table])) {
213
            $this->aliasRegistry[$table] = $alias;
214
        }
215
    }
216
217
    /**
218
     * @param string $table
219
     * @return string
220
     */
221
    public function getAlias(string $table): ?string
222
    {
223
        if (isset($this->aliasRegistry[$table])) {
224
            return $this->aliasRegistry[$table];
225
        }
226
        return null;
227
    }
228
229
    /**
230
     * Execute an aggregate function on the database.
231
     *
232
     * @param  string  $function
233
     * @param  array   $columns
234
     * @return mixed
235
     */
236
    public function aggregate($function, $columns = ['*'])
237
    {
238
        $results = $this->cloneWithout($this->unions ? [] : ['columns'])
239
            ->setAggregate($function, $columns)
240
            ->get($columns);
241
242
        $this->aqb = new QueryBuilder();
243
244
        if (! $results->isEmpty()) {
245
            return array_change_key_case((array) $results[0])['aggregate'];
246
        }
247
248
        return false;
249
    }
250
251
252
253
    /**
254
     * Add a basic where clause to the query.
255
     *
256
     * @param Closure|string|array $column
257
     * @param mixed $operator
258
     * @param mixed $value
259
     * @param string $boolean
260
     * @return Builder
261
     */
262
    public function where($column, $operator = null, $value = null, $boolean = 'and')
263
    {
264
        // If the column is an array, we will assume it is an array of key-value pairs
265
        // and can add them each as a where clause. We will maintain the boolean we
266
        // received when the method was called and pass it into the nested where.
267
        if (is_array($column)) {
268
            return $this->addArrayOfWheres($column, $boolean);
269
        }
270
271
        // Here we will make some assumptions about the operator. If only 2 values are
272
        // passed to the method, we will assume that the operator is an equals sign
273
        // and keep going. Otherwise, we'll require the operator to be passed in.
274
        [$value, $operator] = $this->prepareValueAndOperator(
275
            $value,
276
            $operator,
277
            func_num_args() === 2
278
        );
279
280
        // If the columns is actually a Closure instance, we will assume the developer
281
        // wants to begin a nested where statement which is wrapped in parenthesis.
282
        // We'll add that Closure to the query then return back out immediately.
283
        if ($column instanceof Closure) {
284
            return $this->whereNested($column, $boolean);
285
        }
286
287
        // If the given operator is not found in the list of valid operators we will
288
        // assume that the developer is just short-cutting the '=' operators and
289
        // we will set the operators to '==' and set the values appropriately.
290
        if ($this->invalidOperator($operator)) {
291
            [$value, $operator] = [$operator, '=='];
292
        }
293
294
        // If the value is a Closure, it means the developer is performing an entire
295
        // sub-select within the query and we will need to compile the sub-select
296
        // within the where clause to get the appropriate query record results.
297
        if ($value instanceof Closure) {
298
            return $this->whereSub($column, $operator, $value, $boolean);
299
        }
300
301
        $type = 'Basic';
302
303
        // If the column is making a JSON reference we'll check to see if the value
304
        // is a boolean. If it is, we'll add the raw boolean string as an actual
305
        // value to the query to ensure this is properly handled by the query.
306
        if (Str::contains($column, '->') && is_bool($value)) {
307
            $value = new Expression($value ? 'true' : 'false');
308
309
            if (is_string($column)) {
0 ignored issues
show
introduced by
The condition is_string($column) is always true.
Loading history...
310
                $type = 'JsonBoolean';
311
            }
312
        }
313
314
        // Now that we are working with just a simple query we can put the elements
315
        // in our array and add the query binding to our array of bindings that
316
        // will be bound to each SQL statements when it is finally executed.
317
        $this->wheres[] = compact(
318
            'type',
319
            'column',
320
            'operator',
321
            'value',
322
            'boolean'
323
        );
324
325
        if (! $value instanceof Expression) {
326
            $this->addBinding($value, 'where');
327
        }
328
329
        return $this;
330
    }
331
332
    /**
333
     * Add a "where null" clause to the query.
334
     *
335
     * @param  string|array  $columns
336
     * @param  string  $boolean
337
     * @param  bool    $not
338
     * @return $this
339
     */
340
    public function whereNull($columns, $boolean = 'and', $not = false)
341
    {
342
        $type = 'Basic';
343
        $operator = $not ? '!=' : '==';
344
        $value = null;
345
346
        foreach (Arr::wrap($columns) as $column) {
347
            $this->wheres[] = compact('type', 'column', 'operator', 'value', 'boolean');
348
        }
349
350
        return $this;
351
    }
352
353
    /**
354
     * Determine if the given operator is supported.
355
     *
356
     * @param  string  $operator
357
     * @return bool
358
     */
359
    protected function invalidOperator($operator)
360
    {
361
        return ! in_array(strtolower($operator), $this->operators, true) &&
362
            ! isset($this->grammar->getOperators()[strtoupper($operator)]);
363
    }
364
365
    /**
366
     * Add an "or where" clause to the query.
367
     *
368
     * @param Closure|string|array  $column
369
     * @param  mixed  $operator
370
     * @param  mixed  $value
371
     * @return IlluminateQueryBuilder|static
372
     */
373
    public function orWhere($column, $operator = null, $value = null)
374
    {
375
        return $this->where($column, $operator, $value, 'or');
376
    }
377
378
    /**
379
     * Add an "order by" clause to the query.
380
     *
381
     * @param  Closure|IlluminateQueryBuilder|string  $column
382
     * @param  string  $direction
383
     * @return $this
384
     *
385
     * @throws InvalidArgumentException
386
     */
387
    public function orderBy($column, $direction = 'asc')
388
    {
389
        if ($this->isQueryable($column)) {
390
            [$query, $bindings] = $this->createSub($column);
391
392
            $column = new Expression('(' . $query . ')');
393
        }
394
395
        $this->{$this->unions ? 'unionOrders' : 'orders'}[] = [
396
            'column' => $column,
397
            'direction' => $direction,
398
        ];
399
400
        return $this;
401
    }
402
403
    /**
404
     * Add a raw "order by" clause to the query.
405
     *
406
     * @param string|ExpressionInterface $aql
407
     * @param array $bindings
408
     * @return $this
409
     */
410
    public function orderByRaw($aql, $bindings = [])
411
    {
412
        $type = 'Raw';
413
        $column = $aql;
414
        $this->{$this->unions ? 'unionOrders' : 'orders'}[] = compact('type', 'column');
415
416
        return $this;
417
    }
418
419
    /**
420
     * Put the query's results in random order.
421
     *
422
     * @param  string  $seed
423
     * @return $this
424
     */
425
    public function inRandomOrder($seed = '')
426
    {
427
        return $this->orderByRaw($this->grammar->compileRandom($this));
428
    }
429
}
430