Completed
Push — master ( 0ff8e8...37635b )
by Ivan
03:55
created

TableQuery   F

Complexity

Total Complexity 274

Size/Duplication

Total Lines 1203
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 57.29%

Importance

Changes 0
Metric Value
wmc 274
lcom 1
cbo 7
dl 0
loc 1203
ccs 404
cts 705
cp 0.5729
rs 0.8
c 0
b 0
f 0

35 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 3
A __clone() 0 4 1
A getDefinition() 0 4 1
B getColumn() 0 46 10
D normalizeValue() 0 83 29
F filterSQL() 0 93 36
A filter() 0 5 2
A any() 0 13 3
A all() 0 13 3
A sort() 0 4 2
A group() 0 10 3
A paginate() 0 4 1
A __call() 0 12 4
A reset() 0 13 1
A groupBy() 0 6 1
A join() 0 15 6
A where() 0 6 1
A having() 0 6 1
A order() 0 6 1
A limit() 0 6 2
F count() 0 114 29
C columns() 0 52 12
F iterator() 0 204 55
A select() 0 4 1
B insert() 0 38 8
B update() 0 32 7
A delete() 0 20 4
A with() 0 20 3
A getIterator() 0 4 1
A offsetGet() 0 4 1
A offsetExists() 0 4 1
A offsetUnset() 0 4 1
A offsetSet() 0 4 1
A collection() 0 4 1
F ids() 0 150 38

How to fix   Complexity   

Complex Class

Complex classes like TableQuery 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 TableQuery, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace vakata\database\schema;
3
4
use vakata\collection\Collection;
5
use vakata\database\DBInterface;
6
use vakata\database\DBException;
7
use vakata\database\ResultInterface;
8
9
/**
10
 * A database query class
11
 */
12
class TableQuery implements \IteratorAggregate, \ArrayAccess, \Countable
13
{
14
    const SEP = '___';
15
    /**
16
     * @var DBInterface
17
     */
18
    protected $db;
19
    /**
20
     * @var Table
21
     */
22
    protected $definition;
23
    /**
24
     * @var TableQueryIterator|null
25
     */
26
    protected $qiterator;
27
28
    /**
29
     * @var array
30
     */
31
    protected $where = [];
32
    /**
33
     * @var array
34
     */
35
    protected $order = [];
36
    /**
37
     * @var array
38
     */
39
    protected $group = [];
40
    /**
41
     * @var array
42
     */
43
    protected $having = [];
44
    /**
45
     * @var int[]
46
     */
47
    protected $li_of = [0,0,0];
48
    /**
49
     * @var array
50
     */
51
    protected $fields = [];
52
    /**
53
     * @var array
54
     */
55
    protected $withr = [];
56
    /**
57
     * @var array
58
     */
59
    protected $joins = [];
60
    /**
61
     * @var array
62
     */
63
    protected $pkey = [];
64
    /**
65
     * @var array
66
     */
67
    protected $aliases = [];
68
69
    /**
70
     * Create an instance
71
     * @param  DBInterface    $db         the database connection
72
     * @param  Table|string   $table      the name or definition of the main table in the query
73
     */
74 18
    public function __construct(DBInterface $db, $table)
75
    {
76 18
        $this->db = $db;
77 18
        $this->definition = $table instanceof Table ? $table : $this->db->definition((string)$table);
78 18
        $primary = $this->definition->getPrimaryKey();
79 18
        $columns = $this->definition->getColumns();
80 18
        $this->pkey = count($primary) ? $primary : $columns;
81 18
        $this->columns($columns);
82 18
    }
83
    public function __clone()
84
    {
85
        $this->reset();
86
    }
87
    /**
88
     * Get the table definition of the queried table
89
     * @return Table        the definition
90
     */
91
    public function getDefinition() : Table
92
    {
93
        return $this->definition;
94
    }
95
96 18
    protected function getColumn($column)
97
    {
98 18
        $column = explode('.', $column);
99 18
        if (count($column) === 1) {
100 18
            $column = [ $this->definition->getName(), $column[0] ];
101 18
            $col = $this->definition->getColumn($column[1]);
102 18
            if (!$col) {
103 18
                throw new DBException('Invalid column name in own table');
104
            }
105 2
        } elseif (count($column) === 2) {
106 2
            if ($column[0] === $this->definition->getName()) {
107
                $col = $this->definition->getColumn($column[1]);
108
                if (!$col) {
109
                    throw new DBException('Invalid column name in own table');
110
                }
111
            } else {
112 2
                if ($this->definition->hasRelation($column[0])) {
113 1
                    $col = $this->definition->getRelation($column[0])->table->getColumn($column[1]);
114 1
                    if (!$col) {
115 1
                        throw new DBException('Invalid column name in related table');
116
                    }
117 1
                } else if (isset($this->joins[$column[0]])) {
118 1
                    $col = $this->joins[$column[0]]->table->getColumn($column[1]);
119 1
                    if (!$col) {
120 1
                        throw new DBException('Invalid column name in related table');
121
                    }
122
                } else {
123 2
                    throw new DBException('Invalid foreign table name: ' . implode(',', $column));
124
                }
125
            }
126
        } else {
127
            $name = array_pop($column);
128
            $this->with(implode('.', $column));
129
            $table = $this->definition;
130
            $table = array_reduce(
131
                $column,
132
                function ($carry, $item) use (&$table) {
133
                    $table = $table->getRelation($item)->table;
134
                    return $table;
135
                }
136
            );
137
            $col = $table->getColumn($name);
138
            $column = [ implode(static::SEP, $column), $name ];
139
        }
140 18
        return [ 'name' => implode('.', $column), 'data' => $col ];
141
    }
142 9
    protected function normalizeValue(TableColumn $col, $value)
143
    {
144 9
        $strict = (int)$this->db->driverOption('strict', 0) > 0;
145 9
        if ($value === null && $col->isNullable()) {
146
            return null;
147
        }
148 9
        switch ($col->getBasicType()) {
149 9
            case 'date':
150
                if (is_string($value)) {
151
                    $temp = strtotime($value);
152
                    if (!$temp) {
153
                        if ($strict) {
154
                            throw new DBException('Invalid value for date column ' . $col->getName());
155
                        }
156
                        return null;
157
                    }
158
                    return date('Y-m-d', $temp);
159
                }
160
                if (is_int($value)) {
161
                    return date('Y-m-d', $value);
162
                }
163
                if ($value instanceof \DateTime) {
164
                    return $value->format('Y-m-d');
165
                }
166
                if ($strict) {
167
                    throw new DBException('Invalid value (unknown data type) for date column ' . $col->getName());
168
                }
169
                return $value;
170 9
            case 'datetime':
171
                if (is_string($value)) {
172
                    $temp = strtotime($value);
173
                    if (!$temp) {
174
                        if ($strict) {
175
                            throw new DBException('Invalid value for datetime column ' . $col->getName());
176
                        }
177
                        return null;
178
                    }
179
                    return date('Y-m-d H:i:s', $temp);
180
                }
181
                if (is_int($value)) {
182
                    return date('Y-m-d H:i:s', $value);
183
                }
184
                if ($value instanceof \DateTime) {
185
                    return $value->format('Y-m-d H:i:s');
186
                }
187
                if ($strict) {
188
                    throw new DBException('Invalid value (unknown data type) for datetime column ' . $col->getName());
189
                }
190
                return $value;
191 9
            case 'enum':
192
                $values = $col->getValues();
193
                if (is_int($value)) {
194
                    if (!isset($values[$value])) {
195
                        if ($strict) {
196
                            throw new DBException('Invalid value (using integer) for enum ' . $col->getName());
197
                        }
198
                        return $value;
199
                    }
200
                    return $values[$value];
201
                }
202
                if (!in_array($value, $col->getValues())) {
203
                    if ($strict) {
204
                        throw new DBException('Invalid value for enum ' . $col->getName());
205
                    }
206
                    return 0;
207
                }
208
                return $value;
209 9
            case 'int':
210 5
                return (int)preg_replace('([^+\-0-9]+)', '', $value);
211 6
            case 'float':
212
                return (float)preg_replace('([^+\-0-9.]+)', '', str_replace(',', '.', $value));
213 6
            case 'text':
214 6
                if ($col->hasLength() && mb_strlen($value) > $col->getLength()) {
215 2
                    if ($strict) {
216 1
                        throw new DBException('Invalid value for text column ' . $col->getName());
217
                    }
218 1
                    return mb_substr($value, 0, $col->getLength());
219
                }
220 4
                return $value;
221
            default: // time, blob, etc
222
                return $value;
223
        }
224
    }
225
226 6
    protected function filterSQL(string $column, $value, bool $negate = false) : array
227
    {
228 6
        list($name, $column) = array_values($this->getColumn($column));
229 6
        if (is_array($value) && count($value) === 1 && isset($value['not'])) {
230 1
            $negate = true;
231 1
            $value = $value['not'];
232
        }
233 6
        if (is_array($value) && count($value) === 1 && isset($value['like'])) {
234 1
            $value = $value['like'];
235
            // str_replace(['%', '_'], ['\\%','\\_'], $q)
0 ignored issues
show
Unused Code Comprehensibility introduced by
75% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
236 1
            return $negate ?
237
                [
238
                    $name . ' NOT LIKE ?',
239
                    [ $this->normalizeValue($column, $value) ]
240
                ] :
241
                [
242 1
                    $name . ' LIKE ?',
243 1
                    [ $this->normalizeValue($column, $value) ]
244
                ];
245
        }
246 5
        if (is_null($value)) {
247
            return $negate ?
248
                [ $name . ' IS NOT NULL', [] ]:
249
                [ $name . ' IS NULL', [] ];
250
        }
251 5
        if (!is_array($value)) {
252 5
            return $negate ?
253
                [
254 1
                    $name . ' <> ?',
255 1
                    [ $this->normalizeValue($column, $value) ]
256
                ] :
257
                [
258 5
                    $name . ' = ?',
259 5
                    [ $this->normalizeValue($column, $value) ]
260
                ];
261
        }
262 3
        if (isset($value['beg']) && strlen($value['beg']) && (!isset($value['end']) || !strlen($value['end']))) {
263
            $value = [ 'gte' => $value['beg'] ];
264
        }
265 3
        if (isset($value['end']) && strlen($value['end']) && (!isset($value['beg']) || !strlen($value['beg']))) {
266
            $value = [ 'lte' => $value['end'] ];
267
        }
268 3
        if (isset($value['beg']) && isset($value['end'])) {
269
            return $negate ?
270
                [
271
                    $name.' NOT BETWEEN ? AND ?',
272
                    [
273
                        $this->normalizeValue($column, $value['beg']),
274
                        $this->normalizeValue($column, $value['end'])
275
                    ]
276
                ] :
277
                [
278
                    $name.' BETWEEN ? AND ?',
279
                    [
280
                        $this->normalizeValue($column, $value['beg']),
281
                        $this->normalizeValue($column, $value['end'])
282
                    ]
283
                ];
284
        }
285 3
        if (isset($value['gt']) || isset($value['lt']) || isset($value['gte']) || isset($value['lte'])) {
286 2
            $sql = [];
287 2
            $par = [];
288 2
            if (isset($value['gt'])) {
289 1
                $sql[] = $name. ' ' . ($negate ? '<=' : '>') . ' ?';
290 1
                $par[] = $this->normalizeValue($column, $value['gt']);
291
            }
292 2
            if (isset($value['gte'])) {
293 1
                $sql[] = $name. ' ' . ($negate ? '<' : '>=') . ' ?';
294 1
                $par[] = $this->normalizeValue($column, $value['gte']);
295
            }
296 2
            if (isset($value['lt'])) {
297 2
                $sql[] = $name. ' ' . ($negate ? '>=' : '<') . ' ?';
298 2
                $par[] = $this->normalizeValue($column, $value['lt']);
299
            }
300 2
            if (isset($value['lte'])) {
301 1
                $sql[] = $name. ' ' . ($negate ? '>' : '<=') . ' ?';
302 1
                $par[] = $this->normalizeValue($column, $value['lte']);
303
            }
304
            return [
305 2
                '(' . implode(' AND ', $sql) . ')',
306 2
                $par
307
            ];
308
        }
309 2
        return $negate ?
310
            [
311
                $name . ' NOT IN (??)',
312
                [ array_map(function ($v) use ($column) { return $this->normalizeValue($column, $v); }, $value) ]
313
            ] :
314
            [
315 2
                $name . ' IN (??)',
316
                [ array_map(function ($v) use ($column) { return $this->normalizeValue($column, $v); }, $value) ]
317
            ];
318
    }
319
    /**
320
     * Filter the results by a column and a value
321
     * @param  string $column  the column name to filter by (related columns can be used - for example: author.name)
322
     * @param  mixed  $value   a required value, array of values or range of values (range example: ['beg'=>1,'end'=>3])
323
     * @param  bool   $negate  optional boolean indicating that the filter should be negated
324
     * @return $this
325
     */
326 4
    public function filter(string $column, $value, bool $negate = false) : TableQuery
327
    {
328 4
        $sql = $this->filterSQL($column, $value, $negate);
329 4
        return strlen($sql[0]) ? $this->where($sql[0], $sql[1]) : $this;
330
    }
331
    /**
332
     * Filter the results matching any of the criteria
333
     * @param  array $criteria  each row is a column, value and optional negate flag (same as filter method)
334
     * @return $this
335
     */
336 2
    public function any(array $criteria) : TableQuery
337
    {
338 2
        $sql = [];
339 2
        $par = [];
340 2
        foreach ($criteria as $row) {
341 2
            if (isset($row[1])) {
342 2
                $temp = $this->filterSQL($row[0], $row[1] ?? null, $row[2] ?? false);
343 2
                $sql[] = $temp[0];
344 2
                $par = array_merge($par, $temp[1]);
345
            }
346
        }
347 2
        return $this->where('(' . implode(' OR ', $sql) . ')', $par);
348
    }
349
    /**
350
     * Filter the results matching all of the criteria
351
     * @param  array $criteria  each row is a column, value and optional negate flag (same as filter method)
352
     * @return $this
353
     */
354 1
    public function all(array $criteria) : TableQuery
355
    {
356 1
        $sql = [];
357 1
        $par = [];
358 1
        foreach ($criteria as $row) {
359 1
            if (isset($row[1])) {
360 1
                $temp = $this->filterSQL($row[0], $row[1] ?? null, $row[2] ?? false);
361 1
                $sql[] = $temp[0];
362 1
                $par = array_merge($par, $temp[1]);
363
            }
364
        }
365 1
        return $this->where('(' . implode(' AND ', $sql) . ')', $par);
366
    }
367
    /**
368
     * Sort by a column
369
     * @param  string       $column the column name to sort by (related columns can be used - for example: author.name)
370
     * @param  bool|boolean $desc   should the sorting be in descending order, defaults to `false`
371
     * @return $this
372
     */
373
    public function sort(string $column, bool $desc = false) : TableQuery
374
    {
375
        return $this->order($this->getColumn($column)['name'] . ' ' . ($desc ? 'DESC' : 'ASC'));
376
    }
377
    /**
378
     * Group by a column (or columns)
379
     * @param  string|array        $column the column name (or names) to group by
380
     * @return $this
381
     */
382 1
    public function group($column) : TableQuery
383
    {
384 1
        if (!is_array($column)) {
385 1
            $column = [ $column ];
386
        }
387 1
        foreach ($column as $k => $v) {
388 1
            $column[$k] = $this->getColumn($v)['name'];
389
        }
390 1
        return $this->groupBy(implode(', ', $column), []);
391
    }
392
    /**
393
     * Get a part of the data
394
     * @param  int|integer $page    the page number to get (1-based), defaults to 1
395
     * @param  int|integer $perPage the number of records per page - defaults to 25
396
     * @return $this
397
     */
398
    public function paginate(int $page = 1, int $perPage = 25) : TableQuery
399
    {
400
        return $this->limit($perPage, ($page - 1) * $perPage);
401
    }
402 1
    public function __call($name, $data)
403
    {
404 1
        if (strpos($name, 'filterBy') === 0) {
405
            return $this->filter(strtolower(substr($name, 8)), $data[0]);
406
        }
407 1
        if (strpos($name, 'sortBy') === 0) {
408
            return $this->sort(strtolower(substr($name, 6)), $data[0]);
409
        }
410 1
        if (strpos($name, 'groupBy') === 0) {
411 1
            return $this->group(strtolower(substr($name, 7)));
412
        }
413
    }
414
    /**
415
     * Remove all filters, sorting, etc
416
     * @return $this
417
     */
418 3
    public function reset() : TableQuery
419
    {
420 3
        $this->where = [];
421 3
        $this->joins = [];
422 3
        $this->group = [];
423 3
        $this->withr = [];
424 3
        $this->order = [];
425 3
        $this->having = [];
426 3
        $this->aliases = [];
427 3
        $this->li_of = [0,0,0];
428 3
        $this->qiterator = null;
429 3
        return $this;
430
    }
431
    /**
432
     * Apply advanced grouping
433
     * @param  string $sql    SQL statement to use in the GROUP BY clause
434
     * @param  array  $params optional params for the statement (defaults to an empty array)
435
     * @return $this
436
     */
437 1
    public function groupBy(string $sql, array $params = []) : TableQuery
438
    {
439 1
        $this->qiterator = null;
440 1
        $this->group = [ $sql, $params ];
441 1
        return $this;
442
    }
443
    /**
444
     * Join a table to the query (no need to do this for relations defined with foreign keys)
445
     * @param  Table|string $table     the table to join
446
     * @param  array        $fields    what to join on (joined_table_field => other_field) 
447
     * @param  string|null  $name      alias for the join, defaults to the table name 
448
     * @param  bool         $multiple  are multiple rows joined (results in a LEFT JOIN), default to true 
449
     * @return $this
450
     */
451 1
    public function join($table, array $fields, string $name = null, bool $multiple = true)
452
    {
453 1
        $table = $table instanceof Table ? $table : $this->db->definition((string)$table);
454 1
        $name = $name ?? $table->getName();
455 1
        if (isset($this->joins[$name]) || $this->definition->hasRelation($name)) {
456
            throw new DBException('Alias / table name already in use');
457
        }
458 1
        $this->joins[$name] = new TableRelation($name, $table, [], $multiple);
459 1
        foreach ($fields as $k => $v) {
460 1
            $k = explode('.', $k, 2);
461 1
            $k = count($k) == 2 ? $k[1] : $k[0];
462 1
            $this->joins[$name]->keymap[$this->getColumn($name . '.' . $k)['name']] = $this->getColumn($v)['name'];
463
        }
464 1
        return $this;
465
    }
466
    /**
467
     * Apply an advanced filter (can be called multiple times)
468
     * @param  string $sql    SQL statement to be used in the where clause
469
     * @param  array  $params parameters for the SQL statement (defaults to an empty array)
470
     * @return $this
471
     */
472 7
    public function where(string $sql, array $params = []) : TableQuery
473
    {
474 7
        $this->qiterator = null;
475 7
        $this->where[] = [ $sql, $params ];
476 7
        return $this;
477
    }
478
    /**
479
     * Apply an advanced HAVING filter (can be called multiple times)
480
     * @param  string $sql    SQL statement to be used in the HAING clause
481
     * @param  array  $params parameters for the SQL statement (defaults to an empty array)
482
     * @return $this
483
     */
484 1
    public function having(string $sql, array $params = []) : TableQuery
485
    {
486 1
        $this->qiterator = null;
487 1
        $this->having[] = [ $sql, $params ];
488 1
        return $this;
489
    }
490
    /**
491
     * Apply advanced sorting
492
     * @param  string $sql    SQL statement to use in the ORDER clause
493
     * @param  array  $params optional params for the statement (defaults to an empty array)
494
     * @return $this
495
     */
496 1
    public function order(string $sql, array $params = []) : TableQuery
497
    {
498 1
        $this->qiterator = null;
499 1
        $this->order = [ $sql, $params ];
500 1
        return $this;
501
    }
502
    /**
503
     * Apply an advanced limit
504
     * @param  int         $limit  number of rows to return
505
     * @param  int         $offset number of rows to skip from the beginning (defaults to 0)
506
     * @return $this
507
     */
508 1
    public function limit(int $limit, int $offset = 0, bool $limitOnMainTable = false) : TableQuery
509
    {
510 1
        $this->qiterator = null;
511 1
        $this->li_of = [ $limit, $offset, $limitOnMainTable ? 1 : 0 ];
512 1
        return $this;
513
    }
514
    /**
515
     * Get the number of records
516
     * @return int the total number of records (does not respect pagination)
517
     */
518 6
    public function count() : int
519
    {
520 6
        $aliases = [];
521 6
        $getAlias = function ($name) use (&$aliases) {
522
            // to bypass use: return $name;
0 ignored issues
show
Unused Code Comprehensibility introduced by
42% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
523 1
            return $aliases[$name] = $aliases[$name] ?? 'alias' . static::SEP . count($aliases);
524 6
        };
525 6
        $table = $this->definition->getName();
526 6
        $sql = 'SELECT COUNT(DISTINCT '.$table.'.'.implode(', '.$table.'.', $this->pkey).') FROM '.$table.' ';
527 6
        $par = [];
528
        
529 6
        $relations = $this->withr;
530 6
        foreach ($relations as $k => $v) {
531
            $getAlias($k);
532
        }
533 6
        $f = $this->fields;
0 ignored issues
show
Unused Code introduced by
$f is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
534 6
        $w = $this->where;
535 6
        $h = $this->having;
536 6
        $o = $this->order;
537 6
        $g = $this->group;
538
        $j = array_map(function ($v) { return clone $v; }, $this->joins);
539 6
        foreach ($this->definition->getRelations() as $k => $v) {
540 6
            foreach ($w as $kk => $vv) {
541 4
                if (preg_match('(\b'.preg_quote($k . '.'). ')i', $vv[0])) {
542 1
                    $relations[$k] = [ $v, $table ];
543 4
                    $w[$kk][0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $vv[0]);
544
                }
545
            }
546 6
            if (isset($o[0]) && preg_match('(\b'.preg_quote($k . '.'). ')i', $o[0])) {
547
                $relations[$k] = [ $v, $table ];
548
            }
549 6
            foreach ($h as $kk => $vv) {
550
                if (preg_match('(\b'.preg_quote($k . '.'). ')i', $vv[0])) {
551
                    $relations[$k] = [ $relation, $table ];
0 ignored issues
show
Bug introduced by
The variable $relation does not exist. Did you mean $relations?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
552
                    $h[$kk][0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $vv[0]);
553
                }
554
            }
555 6
            if (isset($g[0]) && preg_match('(\b'.preg_quote($k . '.'). ')i', $g[0])) {
556
                $relations[$k] = [ $relation, $table ];
0 ignored issues
show
Bug introduced by
The variable $relation does not exist. Did you mean $relations?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
557
                $g[0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $g[0]);
558
            }
559 6
            foreach ($j as $kk => $v) {
560
                foreach ($v->keymap as $kkk => $vv) {
561
                    if (preg_match('(\b'.preg_quote($k . '.'). ')i', $vv)) {
562
                        $relations[$k] = [ $relation, $table ];
0 ignored issues
show
Bug introduced by
The variable $relation does not exist. Did you mean $relations?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
563 6
                        $j[$k]->keymap[$kkk] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $vv);
564
                    }
565
                }
566
            }
567
        }
568
569 6
        foreach ($j as $k => $v) {
570
            $sql .= ($v->many ? 'LEFT ' : '' ) . 'JOIN '.$v->table->getName().' '.$k.' ON ';
571
            $tmp = [];
572
            foreach ($v->keymap as $kk => $vv) {
573
                $tmp[] = $kk.' = '.$vv;
574
            }
575
            $sql .= implode(' AND ', $tmp) . ' ';
576
        }
577 6
        foreach ($relations as $k => $v) {
578 1
            $table = $v[1] !== $this->definition->getName() ? $getAlias($v[1]) : $v[1];
579 1
            $v = $v[0];
580 1
            if ($v->pivot) {
581 1
                $alias = $getAlias($k.'_pivot');
582 1
                $sql .= 'LEFT JOIN '.$v->pivot->getName().' '.$alias.' ON ';
583 1
                $tmp = [];
584 1
                foreach ($v->keymap as $kk => $vv) {
585 1
                    $tmp[] = $table.'.'.$kk.' = '.$alias.'.'.$vv.' ';
586
                }
587 1
                $sql .= implode(' AND ', $tmp) . ' ';
588 1
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$getAlias($k).' ON ';
589 1
                $tmp = [];
590 1
                foreach ($v->pivot_keymap as $kk => $vv) {
591 1
                    $tmp[] = $getAlias($k).'.'.$vv.' = '.$alias.'.'.$kk.' ';
592
                }
593 1
                $sql .= implode(' AND ', $tmp) . ' ';
594
            } else {
595 1
                $alias = $getAlias($k);
596 1
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$alias.' ON ';
597 1
                $tmp = [];
598 1
                foreach ($v->keymap as $kk => $vv) {
599 1
                    $tmp[] = $table.'.'.$kk.' = '.$alias.'.'.$vv.' ';
600
                }
601 1
                if ($v->sql) {
602
                    $tmp[] = $v->sql . ' ';
603
                    $par = array_merge($par, $v->par ?? []);
604
                }
605 1
                $sql .= implode(' AND ', $tmp) . ' ';
606
            }
607
        }
608 6
        if (count($w)) {
609 4
            $sql .= 'WHERE ';
610 4
            $tmp = [];
611 4
            foreach ($w as $v) {
612 4
                $tmp[] = '(' . $v[0] . ')';
613 4
                $par = array_merge($par, $v[1]);
614
            }
615 4
            $sql .= implode(' AND ', $tmp).' ';
616
        }
617 6
        if (count($g)) {
618
            $sql .= 'GROUP BY ' . $g[0] . ' ';
619
            $par = array_merge($par, $g[1]);
620
        }
621 6
        if (count($h)) {
622
            $sql .= 'HAVING ';
623
            $tmp = [];
624
            foreach ($h as $v) {
625
                $tmp[] = '(' . $v[0] . ')';
626
                $par = array_merge($par, $v[1]);
627
            }
628
            $sql .= implode(' AND ', $tmp).' ';
629
        }
630 6
        return $this->db->one($sql, $par);
631
    }
632
    /**
633
     * Specify which columns to fetch (be default all table columns are fetched)
634
     * @param  array $fields optional array of columns to select (related columns can be used too)
635
     * @return $this
636
     */
637 18
    public function columns(array $fields) : TableQuery
638
    {
639 18
        foreach ($fields as $k => $v) {
640 18
            if (strpos($v, '*') !== false) {
641
                $temp = explode('.', $v);
642
                if (count($temp) === 1) {
643
                    $table = $this->definition->getName();
644
                    $cols = $this->definition->getColumns();
645
                } else if (count($temp) === 2) {
646
                    $table = $temp[0];
647
                    if ($this->definition->hasRelation($table)) {
648
                        $cols = $this->definition->getRelation($table)->table->getColumns();
649
                    } else if (isset($this->joins[$table])) {
650
                        $cols = $this->joins[$table]->table->getColumns();
651
                    } else {
652
                        throw new DBException('Invalid foreign table name');
653
                    }
654
                } else {
655
                    array_pop($temp);
656
                    $this->with(implode('.', $temp));
657
                    $table = array_reduce(
658
                        $temp,
659
                        function ($carry, $item) use (&$table) {
660
                            return $table->getRelation($item)->table;
661
                        }
662
                    );
663
                    $cols = $table->getColumns();
664
                    $table = implode(static::SEP, $temp);
665
                }
666
                unset($fields[$k]);
667
                foreach ($cols as $col) {
668 18
                    $fields[] = $table . '.' . $col;
669
                }
670
            }
671
        }
672 18
        $primary = $this->definition->getPrimaryKey();
673 18
        foreach ($fields as $k => $v) {
674
            try {
675 18
                $fields[$k] = $this->getColumn($v)['name'];
676 1
            } catch (DBException $e) {
677 18
                $fields[$k] = $v;
678
            }
679
        }
680 18
        foreach ($primary as $field) {
681 18
            $field = $this->getColumn($field)['name'];
682 18
            if (!in_array($field, $fields)) {
683 18
                $fields[] = $field;
684
            }
685
        }
686 18
        $this->fields = $fields;
687 18
        return $this;
688
    }
689
    /**
690
     * Perform the actual fetch
691
     * @param  array|null $fields optional array of columns to select (related columns can be used too)
692
     * @return TableQueryIterator               the query result as an iterator
693
     */
694 12
    public function iterator(array $fields = null) : TableQueryIterator
695
    {
696 12
        if ($this->qiterator) {
697 5
            return $this->qiterator;
698
        }
699 12
        $aliases = [];
700 12
        $getAlias = function ($name) use (&$aliases) {
701
            // to bypass use: return $name;
0 ignored issues
show
Unused Code Comprehensibility introduced by
42% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
702 4
            return $aliases[$name] = $aliases[$name] ?? 'alias' . static::SEP . count($aliases);
703 12
        };
704 12
        $table = $this->definition->getName();
705 12
        if ($fields !== null) {
706 1
            $this->columns($fields);
707
        }
708 12
        $relations = $this->withr;
709 12
        foreach ($relations as $k => $v) {
710 4
            $getAlias($k);
711
        }
712
713 12
        $f = $this->fields;
714 12
        $w = $this->where;
715 12
        $h = $this->having;
716 12
        $o = $this->order;
717 12
        $g = $this->group;
718
        $j = array_map(function ($v) { return clone $v; }, $this->joins);
719
720 12
        $porder = [];
721 12
        foreach ($this->definition->getPrimaryKey() as $field) {
722 12
            $porder[] = $this->getColumn($field)['name'];
723
        }
724
725 12
        if (count($porder) && $this->li_of[2] === 1) {
726
            $ids = $this->ids();
727
            if (count($ids)) {
728
                if (count($porder) > 1) {
729
                    $pkw = [];
730
                    foreach ($porder as $name) {
731
                        $pkw[] = $name . ' = ?';
732
                    }
733
                    $pkw = '(' . implode(' AND ', $pkw) . ')';
734
                    $pkp = [];
735
                    foreach ($ids as $id) {
736
                        foreach ($id as $p) {
737
                            $pkp[] = $p;
738
                        }
739
                    }
740
                    $w[] = [
741
                        implode(' OR ', array_fill(0, count($ids), $pkw)),
742
                        $pkp
743
                    ];
744
                } else {
745
                    $w[] = [ $porder[0] . ' IN ('.implode(',', array_fill(0, count($ids), '?')).')', $ids ];
746
                }
747
            } else {
748
                $w[] = [ '1=0', [] ];
749
            }
750
        }
751
752 12
        foreach ($this->definition->getRelations() as $k => $relation) {
753 12
            foreach ($f as $kk => $field) {
754 12
                if (strpos($field, $k . '.') === 0) {
755
                    $relations[$k] = [ $relation, $table ];
756 12
                    $f[$kk] = str_replace($k . '.', $getAlias($k) . '.', $field);
757
                }
758
            }
759 12
            foreach ($w as $kk => $v) {
760 1
                if (preg_match('(\b'.preg_quote($k . '.'). ')i', $v[0])) {
761
                    $relations[$k] = [ $relation, $table ];
762 1
                    $w[$kk][0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $v[0]);
763
                }
764
            }
765 12
            foreach ($h as $kk => $v) {
766 1
                if (preg_match('(\b'.preg_quote($k . '.'). ')i', $v[0])) {
767
                    $relations[$k] = [ $relation, $table ];
768 1
                    $h[$kk][0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $v[0]);
769
                }
770
            }
771 12
            if (isset($o[0]) && preg_match('(\b'.preg_quote($k . '.'). ')i', $o[0])) {
772
                $relations[$k] = [ $relation, $table ];
773
                $o[0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $o[0]);
774
            }
775 12
            if (isset($g[0]) && preg_match('(\b'.preg_quote($k . '.'). ')i', $g[0])) {
776
                $relations[$k] = [ $relation, $table ];
777
                $g[0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $g[0]);
778
            }
779 12
            foreach ($j as $kk => $v) {
780 1
                foreach ($v->keymap as $kkk => $vv) {
781 1
                    if (preg_match('(\b'.preg_quote($k . '.'). ')i', $vv)) {
782
                        $relations[$k] = [ $relation, $table ];
783 12
                        $j[$k]->keymap[$kkk] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $vv);
784
                    }
785
                }
786
            }
787
        }
788 12
        $select = [];
789 12
        foreach ($f as $k => $field) {
790 12
            $select[] = $field . (!is_numeric($k) ? ' ' . $k : '');
791
        }
792 12
        foreach ($this->withr as $name => $relation) {
793 4
            foreach ($relation[0]->table->getColumns() as $column) {
794 4
                $select[] = $getAlias($name) . '.' . $column . ' ' . $getAlias($name . static::SEP . $column);
795
            }
796
        }
797 12
        $sql = 'SELECT '.implode(', ', $select).' FROM '.$table.' ';
798 12
        $par = [];
799 12
        foreach ($j as $k => $v) {
800 1
            $sql .= ($v->many ? 'LEFT ' : '' ) . 'JOIN '.$v->table->getName().' '.$k.' ON ';
801 1
            $tmp = [];
802 1
            foreach ($v->keymap as $kk => $vv) {
803 1
                $tmp[] = $kk.' = '.$vv;
804
            }
805 1
            $sql .= implode(' AND ', $tmp) . ' ';
806
        }
807 12
        foreach ($relations as $relation => $v) {
808 4
            $table = $v[1] !== $this->definition->getName() ? $getAlias($v[1]) : $v[1];
809 4
            $v = $v[0];
810 4
            if ($v->pivot) {
811 3
                $alias = $getAlias($relation.'_pivot');
812 3
                $sql .= 'LEFT JOIN '.$v->pivot->getName().' '.$alias.' ON ';
813 3
                $tmp = [];
814 3
                foreach ($v->keymap as $kk => $vv) {
815 3
                    $tmp[] = $table.'.'.$kk.' = '.$alias.'.'.$vv.' ';
816
                }
817 3
                $sql .= implode(' AND ', $tmp) . ' ';
818 3
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$getAlias($relation).' ON ';
819 3
                $tmp = [];
820 3
                foreach ($v->pivot_keymap as $kk => $vv) {
821 3
                    $tmp[] = $getAlias($relation).'.'.$vv.' = '.$alias.'.'.$kk.' ';
822
                }
823 3
                $sql .= implode(' AND ', $tmp) . ' ';
824
            } else {
825 4
                $alias = $getAlias($relation);
826
827 4
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$alias.' ON ';
828 4
                $tmp = [];
829 4
                foreach ($v->keymap as $kk => $vv) {
830 4
                    $tmp[] = $table.'.'.$kk.' = '.$alias.'.'.$vv.' ';
831
                }
832 4
                if ($v->sql) {
833
                    $tmp[] = $v->sql . ' ';
834
                    $par = array_merge($par, $v->par ?? []);
835
                }
836 4
                $sql .= implode(' AND ', $tmp) . ' ';
837
            }
838
        }
839 12
        if (count($w)) {
840 1
            $sql .= 'WHERE ';
841 1
            $tmp = [];
842 1
            foreach ($w as $v) {
843 1
                $tmp[] = '(' . $v[0] . ')';
844 1
                $par = array_merge($par, $v[1]);
845
            }
846 1
            $sql .= implode(' AND ', $tmp).' ';
847
        }
848 12
        if (count($g)) {
849 1
            $sql .= 'GROUP BY ' . $g[0] . ' ';
850 1
            $par = array_merge($par, $g[1]);
851
        }
852 12
        if (count($h)) {
853 1
            $sql .= 'HAVING ';
854 1
            $tmp = [];
855 1
            foreach ($h as $v) {
856 1
                $tmp[] = '(' . $v[0] . ')';
857 1
                $par = array_merge($par, $v[1]);
858
            }
859 1
            $sql .= implode(' AND ', $tmp).' ';
860
        }
861 12
        if (count($o)) {
862 1
            $sql .= 'ORDER BY ' . $o[0] . ' ';
863 1
            $par = array_merge($par, $o[1]);
864
        }
865 12
        if (count($porder)) {
866 12
            $sql .= (count($o) ? ', ' : 'ORDER BY ') . implode(', ', $porder) . ' ';
867
        }
868 12
        if (($this->li_of[2] === 0 || !count($porder)) && $this->li_of[0]) {
869 1
            if ($this->db->driverName() === 'oracle') {
870
                if ((int)$this->db->driverOption('version', 0) >= 12) {
871
                    $sql .= 'OFFSET ' . $this->li_of[1] . ' ROWS FETCH NEXT ' . $this->li_of[0] . ' ROWS ONLY';
872
                } else {
873
                    $f = array_map(function ($v) {
874
                        $v = explode(' ', trim($v), 2);
875
                        if (count($v) === 2) { return $v[1]; }
876
                        $v = explode('.', $v[0], 2);
877
                        return count($v) === 2 ? $v[1] : $v[0];
878
                    }, $select);
879
                    $sql = "SELECT " . implode(', ', $f) . " 
880
                            FROM (
881
                                SELECT tbl__.*, rownum rnum__ FROM (
882
                                    " . $sql . "
883
                                ) tbl__ 
884
                                WHERE rownum <= " . ($this->li_of[0] + $this->li_of[1]) . "
885
                            ) WHERE rnum__ > " . $this->li_of[1];
886
                }
887
            } else {
888 1
                $sql .= 'LIMIT ' . $this->li_of[0] . ' OFFSET ' . $this->li_of[1];
889
            }
890
        }
891 12
        return $this->qiterator = new TableQueryIterator(
892 12
            $this->db->get($sql, $par), 
893 12
            $this->pkey,
894 12
            $this->withr,
895 12
            $aliases
896
        );
897
    }
898
    /**
899
     * Perform the actual fetch
900
     * @param  array|null $fields optional array of columns to select (related columns can be used too)
901
     * @return array               the query result as an array
902
     */
903 1
    public function select(array $fields = null) : array
904
    {
905 1
        return iterator_to_array($this->iterator($fields));
906
    }
907
    /**
908
     * Insert a new row in the table
909
     * @param  array   $data   key value pairs, where each key is the column name and the value is the value to insert
910
     * @return array           the inserted ID where keys are column names and values are column values
911
     */
912 3
    public function insert(array $data) : array
913
    {
914 3
        $table = $this->definition->getName();
915 3
        $columns = $this->definition->getFullColumns();
916 3
        $insert = [];
917 3
        foreach ($data as $column => $value) {
918 3
            if (isset($columns[$column])) {
919 3
                $insert[$column] = $this->normalizeValue($columns[$column], $value);
920
            }
921
        }
922 2
        if (!count($insert)) {
923
            throw new DBException('No valid columns to insert');
924
        }
925 2
        $sql = 'INSERT INTO '.$table.' ('.implode(', ', array_keys($insert)).') VALUES (??)';
926 2
        $par = [$insert];
927 2
        $primary = $this->definition->getPrimaryKey();
928 2
        if (!count($primary)) {
929
            $this->db->query($sql, $par);
930
            return [];
931
        }
932 2
        if ($this->db->driverName() === 'oracle') {
933
            $ret = [];
934
            foreach ($primary as $k) {
935
                $ret[$k] = str_repeat(' ', 255);
936
                $par[] = &$ret[$k];
937
            }
938
            $sql .= ' RETURNING ' . implode(',', $primary) . ' INTO ' . implode(',', array_fill(0, count($primary), '?'));
939
            $this->db->query($sql, $par);
940
            return $ret;
941
        } else {
942 2
            $ret = [];
943 2
            $ins = $this->db->query($sql, $par)->insertID();
944 2
            foreach ($primary as $k) {
945 2
                $ret[$k] = $data[$k] ?? $ins;
946
            }
947 2
            return $ret;
948
        }
949
    }
950
    /**
951
     * Update the filtered rows with new data
952
     * @param  array  $data key value pairs, where each key is the column name and the value is the value to insert
953
     * @return int          the number of affected rows
954
     */
955 1
    public function update(array $data) : int
956
    {
957 1
        $table = $this->definition->getName();
958 1
        $columns = $this->definition->getFullColumns();
959 1
        $update = [];
960 1
        foreach ($data as $column => $value) {
961 1
            if (isset($columns[$column])) {
962 1
                $update[$column] = $this->normalizeValue($columns[$column], $value);
963
            }
964
        }
965 1
        if (!count($update)) {
966
            throw new DBException('No valid columns to update');
967
        }
968 1
        $sql = 'UPDATE '.$table.' SET ';
969 1
        $par = [];
970
        $sql .= implode(', ', array_map(function ($v) { return $v . ' = ?'; }, array_keys($update))) . ' ';
971 1
        $par = array_merge($par, array_values($update));
972 1
        if (count($this->where)) {
973 1
            $sql .= 'WHERE ';
974 1
            $tmp = [];
975 1
            foreach ($this->where as $v) {
976 1
                $tmp[] = $v[0];
977 1
                $par = array_merge($par, $v[1]);
978
            }
979 1
            $sql .= implode(' AND ', $tmp) . ' ';
980
        }
981 1
        if (count($this->order)) {
982
            $sql .= $this->order[0];
983
            $par = array_merge($par, $this->order[1]);
984
        }
985 1
        return $this->db->query($sql, $par)->affected();
986
    }
987
    /**
988
     * Delete the filtered rows from the DB
989
     * @return int the number of deleted rows
990
     */
991 1
    public function delete() : int
992
    {
993 1
        $table = $this->definition->getName();
994 1
        $sql = 'DELETE FROM '.$table.' ';
995 1
        $par = [];
996 1
        if (count($this->where)) {
997 1
            $sql .= 'WHERE ';
998 1
            $tmp = [];
999 1
            foreach ($this->where as $v) {
1000 1
                $tmp[] = $v[0];
1001 1
                $par = array_merge($par, $v[1]);
1002
            }
1003 1
            $sql .= implode(' AND ', $tmp) . ' ';
1004
        }
1005 1
        if (count($this->order)) {
1006
            $sql .= $this->order[0];
1007
            $par = array_merge($par, $this->order[1]);
1008
        }
1009 1
        return $this->db->query($sql, $par)->affected();
1010
    }
1011
    /**
1012
     * Solve the n+1 queries problem by prefetching a relation by name
1013
     * @param  string $relation the relation name to fetch along with the data
1014
     * @return $this
1015
     */
1016 4
    public function with(string $relation) : TableQuery
1017
    {
1018 4
        $this->qiterator = null;
1019 4
        $parts = explode('.', $relation);
1020 4
        $table = $this->definition;
1021 4
        array_reduce(
1022 4
            $parts,
1023 4
            function ($carry, $item) use (&$table) {
1024 4
                $relation = $table->getRelation($item);
1025 4
                if (!$relation) {
1026
                    throw new DBException('Invalid relation name');
1027
                }
1028 4
                $name = $carry ? $carry . static::SEP . $item : $item;
1029 4
                $this->withr[$name] = [ $relation, $carry ?? $table->getName() ];
1030 4
                $table = $relation->table;
1031 4
                return $name;
1032 4
            }
1033
        );
1034 4
        return $this;
1035
    }
1036
1037 1
    public function getIterator()
1038
    {
1039 1
        return $this->iterator();
1040
    }
1041
1042 10
    public function offsetGet($offset)
1043
    {
1044 10
        return $this->iterator()->offsetGet($offset);
1045
    }
1046
    public function offsetExists($offset)
1047
    {
1048
        return $this->iterator()->offsetExists($offset);
1049
    }
1050
    public function offsetUnset($offset)
1051
    {
1052
        return $this->iterator()->offsetUnset($offset);
1053
    }
1054
    public function offsetSet($offset, $value)
1055
    {
1056
        return $this->iterator()->offsetSet($offset, $value);
1057
    }
1058
1059
    public function collection(array $fields = null) : Collection
1060
    {
1061
        return new Collection($this->iterator($fields));
1062
    }
1063
1064
    public function ids()
1065
    {
1066
        $aliases = [];
1067
        $getAlias = function ($name) use (&$aliases) {
1068
            // to bypass use: return $name;
0 ignored issues
show
Unused Code Comprehensibility introduced by
42% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1069
            return $aliases[$name] = $aliases[$name] ?? 'alias' . static::SEP . count($aliases);
1070
        };
1071
        $table = $this->definition->getName();
1072
        $sql = 'SELECT DISTINCT '.$table.'.'.implode(', '.$table.'.', $this->pkey).' FROM '.$table.' ';
1073
        $par = [];
1074
        
1075
        $relations = $this->withr;
1076
        foreach ($relations as $k => $v) {
1077
            $getAlias($k);
1078
        }
1079
        $f = $this->fields;
0 ignored issues
show
Unused Code introduced by
$f is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1080
        $w = $this->where;
1081
        $h = $this->having;
1082
        $o = $this->order;
1083
        $g = $this->group;
1084
        $j = array_map(function ($v) { return clone $v; }, $this->joins);
1085
        foreach ($this->definition->getRelations() as $k => $v) {
1086
            foreach ($w as $kk => $vv) {
1087
                if (preg_match('(\b'.preg_quote($k . '.'). ')i', $vv[0])) {
1088
                    $relations[$k] = [ $v, $table ];
1089
                    $w[$kk][0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $vv[0]);
1090
                }
1091
            }
1092
            if (isset($o[0]) && preg_match('(\b'.preg_quote($k . '.'). ')i', $o[0])) {
1093
                $relations[$k] = [ $v, $table ];
1094
                $o[0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $o[0]);
1095
            }
1096
            foreach ($h as $kk => $vv) {
1097
                if (preg_match('(\b'.preg_quote($k . '.'). ')i', $vv[0])) {
1098
                    $relations[$k] = [ $relation, $table ];
0 ignored issues
show
Bug introduced by
The variable $relation does not exist. Did you mean $relations?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
1099
                    $h[$kk][0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $vv[0]);
1100
                }
1101
            }
1102
            if (isset($g[0]) && preg_match('(\b'.preg_quote($k . '.'). ')i', $g[0])) {
1103
                $relations[$k] = [ $relation, $table ];
0 ignored issues
show
Bug introduced by
The variable $relation does not exist. Did you mean $relations?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
1104
                $g[0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $g[0]);
1105
            }
1106
            foreach ($j as $kk => $v) {
1107
                foreach ($v->keymap as $kkk => $vv) {
1108
                    if (preg_match('(\b'.preg_quote($k . '.'). ')i', $vv)) {
1109
                        $relations[$k] = [ $relation, $table ];
0 ignored issues
show
Bug introduced by
The variable $relation does not exist. Did you mean $relations?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
1110
                        $j[$k]->keymap[$kkk] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $vv);
1111
                    }
1112
                }
1113
            }
1114
        }
1115
1116
        foreach ($j as $k => $v) {
1117
            $sql .= ($v->many ? 'LEFT ' : '' ) . 'JOIN '.$v->table->getName().' '.$k.' ON ';
1118
            $tmp = [];
1119
            foreach ($v->keymap as $kk => $vv) {
1120
                $tmp[] = $kk.' = '.$vv;
1121
            }
1122
            $sql .= implode(' AND ', $tmp) . ' ';
1123
        }
1124
        foreach ($relations as $k => $v) {
1125
            $table = $v[1] !== $this->definition->getName() ? $getAlias($v[1]) : $v[1];
1126
            $v = $v[0];
1127
            if ($v->pivot) {
1128
                $alias = $getAlias($k.'_pivot');
1129
                $sql .= 'LEFT JOIN '.$v->pivot->getName().' '.$alias.' ON ';
1130
                $tmp = [];
1131
                foreach ($v->keymap as $kk => $vv) {
1132
                    $tmp[] = $table.'.'.$kk.' = '.$alias.'.'.$vv.' ';
1133
                }
1134
                $sql .= implode(' AND ', $tmp) . ' ';
1135
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$getAlias($k).' ON ';
1136
                $tmp = [];
1137
                foreach ($v->pivot_keymap as $kk => $vv) {
1138
                    $tmp[] = $getAlias($k).'.'.$vv.' = '.$alias.'.'.$kk.' ';
1139
                }
1140
                $sql .= implode(' AND ', $tmp) . ' ';
1141
            } else {
1142
                $alias = $getAlias($k);
1143
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$alias.' ON ';
1144
                $tmp = [];
1145
                foreach ($v->keymap as $kk => $vv) {
1146
                    $tmp[] = $table.'.'.$kk.' = '.$alias.'.'.$vv.' ';
1147
                }
1148
                if ($v->sql) {
1149
                    $tmp[] = $v->sql . ' ';
1150
                    $par = array_merge($par, $v->par ?? []);
1151
                }
1152
                $sql .= implode(' AND ', $tmp) . ' ';
1153
            }
1154
        }
1155
        if (count($w)) {
1156
            $sql .= 'WHERE ';
1157
            $tmp = [];
1158
            foreach ($w as $v) {
1159
                $tmp[] = '(' . $v[0] . ')';
1160
                $par = array_merge($par, $v[1]);
1161
            }
1162
            $sql .= implode(' AND ', $tmp).' ';
1163
        }
1164
        if (count($g)) {
1165
            $sql .= 'GROUP BY ' . $g[0] . ' ';
1166
            $par = array_merge($par, $g[1]);
1167
        }
1168
        if (count($h)) {
1169
            $sql .= 'HAVING ';
1170
            $tmp = [];
1171
            foreach ($h as $v) {
1172
                $tmp[] = '(' . $v[0] . ')';
1173
                $par = array_merge($par, $v[1]);
1174
            }
1175
            $sql .= implode(' AND ', $tmp).' ';
1176
        }
1177
        if (count($o)) {
1178
            $sql .= 'ORDER BY ' . $o[0] . ' ';
1179
            $par = array_merge($par, $o[1]);
1180
        }
1181
        $porder = [];
1182
        foreach ($this->definition->getPrimaryKey() as $field) {
1183
            $porder[] = $this->getColumn($field)['name'];
1184
        }
1185
        if (count($porder)) {
1186
            $sql .= (count($o) ? ', ' : 'ORDER BY ') . implode(', ', $porder) . ' ';
1187
        }
1188
1189
        if ($this->li_of[0]) {
1190
            if ($this->db->driverName() === 'oracle') {
1191
                if ((int)$this->db->driverOption('version', 0) >= 12) {
1192
                    $sql .= 'OFFSET ' . $this->li_of[1] . ' ROWS FETCH NEXT ' . $this->li_of[0] . ' ROWS ONLY';
1193
                } else {
1194
                    $f = array_map(function ($v) {
1195
                        $v = explode(' ', trim($v), 2);
1196
                        if (count($v) === 2) { return $v[1]; }
1197
                        $v = explode('.', $v[0], 2);
1198
                        return count($v) === 2 ? $v[1] : $v[0];
1199
                    }, $select);
0 ignored issues
show
Bug introduced by
The variable $select does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
1200
                    $sql = "SELECT " . implode(', ', $f) . " 
1201
                            FROM (
1202
                                SELECT tbl__.*, rownum rnum__ FROM (
1203
                                    " . $sql . "
1204
                                ) tbl__ 
1205
                                WHERE rownum <= " . ($this->li_of[0] + $this->li_of[1]) . "
1206
                            ) WHERE rnum__ > " . $this->li_of[1];
1207
                }
1208
            } else {
1209
                $sql .= 'LIMIT ' . $this->li_of[0] . ' OFFSET ' . $this->li_of[1];
1210
            }
1211
        }
1212
        return $this->db->all($sql, $par);
1213
    }
1214
}
1215