Completed
Push — master ( 131973...716178 )
by Ivan
03:01
created

TableQuery::offsetGet()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
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 36
    public function __construct(DBInterface $db, $table)
75
    {
76 36
        $this->db = $db;
77 36
        $this->definition = $table instanceof Table ? $table : $this->db->definition((string)$table);
78 36
        $primary = $this->definition->getPrimaryKey();
79 36
        $columns = $this->definition->getColumns();
80 36
        $this->pkey = count($primary) ? $primary : $columns;
81 36
        $this->columns($columns);
82 36
    }
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 36
    protected function getColumn($column)
97
    {
98 36
        $column = explode('.', $column);
99 36
        if (count($column) === 1) {
100 36
            $column = [ $this->definition->getName(), $column[0] ];
101 36
            $col = $this->definition->getColumn($column[1]);
102 36
            if (!$col) {
103 36
                throw new DBException('Invalid column name in own table');
104
            }
105 4
        } elseif (count($column) === 2) {
106 4
            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 4
                if ($this->definition->hasRelation($column[0])) {
113 2
                    $col = $this->definition->getRelation($column[0])->table->getColumn($column[1]);
114 2
                    if (!$col) {
115 2
                        throw new DBException('Invalid column name in related table');
116
                    }
117 2
                } else if (isset($this->joins[$column[0]])) {
118 2
                    $col = $this->joins[$column[0]]->table->getColumn($column[1]);
119 2
                    if (!$col) {
120 2
                        throw new DBException('Invalid column name in related table');
121
                    }
122
                } else {
123 4
                    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 36
        return [ 'name' => implode('.', $column), 'data' => $col ];
141
    }
142 18
    protected function normalizeValue(TableColumn $col, $value)
143
    {
144 18
        $strict = (int)$this->db->driverOption('strict', 0) > 0;
145 18
        if ($value === null && $col->isNullable()) {
146
            return null;
147
        }
148 18
        switch ($col->getBasicType()) {
149 18
            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 18
            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 18
            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 18
            case 'int':
210 5
                return (int)preg_replace('([^+\-0-9]+)', '', $value);
211 15
            case 'float':
212
                return (float)preg_replace('([^+\-0-9.]+)', '', str_replace(',', '.', $value));
213 15
            case 'text':
214
                // check using strlen first, in order to avoid hitting mb_ functions which might be polyfilled
215
                // because the polyfill is quite slow
216 15
                if ($col->hasLength() && strlen($value) > $col->getLength() && mb_strlen($value) > $col->getLength()) {
217 4
                    if ($strict) {
218 2
                        throw new DBException('Invalid value for text column ' . $col->getName());
219
                    }
220 2
                    return mb_substr($value, 0, $col->getLength());
221
                }
222 12
                return $value;
223
            default: // time, blob, etc
224
                return $value;
225
        }
226
    }
227
228 12
    protected function filterSQL(string $column, $value, bool $negate = false) : array
229
    {
230 12
        list($name, $column) = array_values($this->getColumn($column));
231 12
        if (is_array($value) && count($value) === 1 && isset($value['not'])) {
232 2
            $negate = true;
233 2
            $value = $value['not'];
234
        }
235 12
        if (is_array($value) && count($value) === 1 && isset($value['like'])) {
236 2
            $value = $value['like'];
237
            // str_replace(['%', '_'], ['\\%','\\_'], $q)
238 2
            return $negate ?
239
                [
240
                    $name . ' NOT LIKE ?',
241
                    [ $this->normalizeValue($column, $value) ]
242
                ] :
243
                [
244 2
                    $name . ' LIKE ?',
245 2
                    [ $this->normalizeValue($column, $value) ]
246
                ];
247
        }
248 10
        if (is_null($value)) {
249
            return $negate ?
250
                [ $name . ' IS NOT NULL', [] ]:
251
                [ $name . ' IS NULL', [] ];
252
        }
253 10
        if (!is_array($value)) {
254 10
            return $negate ?
255
                [
256 2
                    $name . ' <> ?',
257 2
                    [ $this->normalizeValue($column, $value) ]
258
                ] :
259
                [
260 10
                    $name . ' = ?',
261 10
                    [ $this->normalizeValue($column, $value) ]
262
                ];
263
        }
264 6
        if (isset($value['beg']) && strlen($value['beg']) && (!isset($value['end']) || !strlen($value['end']))) {
265
            $value = [ 'gte' => $value['beg'] ];
266
        }
267 6
        if (isset($value['end']) && strlen($value['end']) && (!isset($value['beg']) || !strlen($value['beg']))) {
268
            $value = [ 'lte' => $value['end'] ];
269
        }
270 6
        if (isset($value['beg']) && isset($value['end'])) {
271
            return $negate ?
272
                [
273
                    $name.' NOT BETWEEN ? AND ?',
274
                    [
275
                        $this->normalizeValue($column, $value['beg']),
276
                        $this->normalizeValue($column, $value['end'])
277
                    ]
278
                ] :
279
                [
280
                    $name.' BETWEEN ? AND ?',
281
                    [
282
                        $this->normalizeValue($column, $value['beg']),
283
                        $this->normalizeValue($column, $value['end'])
284
                    ]
285
                ];
286
        }
287 6
        if (isset($value['gt']) || isset($value['lt']) || isset($value['gte']) || isset($value['lte'])) {
288 4
            $sql = [];
289 4
            $par = [];
290 4
            if (isset($value['gt'])) {
291 2
                $sql[] = $name. ' ' . ($negate ? '<=' : '>') . ' ?';
292 2
                $par[] = $this->normalizeValue($column, $value['gt']);
293
            }
294 4
            if (isset($value['gte'])) {
295 2
                $sql[] = $name. ' ' . ($negate ? '<' : '>=') . ' ?';
296 2
                $par[] = $this->normalizeValue($column, $value['gte']);
297
            }
298 4
            if (isset($value['lt'])) {
299 4
                $sql[] = $name. ' ' . ($negate ? '>=' : '<') . ' ?';
300 4
                $par[] = $this->normalizeValue($column, $value['lt']);
301
            }
302 4
            if (isset($value['lte'])) {
303 2
                $sql[] = $name. ' ' . ($negate ? '>' : '<=') . ' ?';
304 2
                $par[] = $this->normalizeValue($column, $value['lte']);
305
            }
306
            return [
307 4
                '(' . implode(' AND ', $sql) . ')',
308 4
                $par
309
            ];
310
        }
311 4
        return $negate ?
312
            [
313
                $name . ' NOT IN (??)',
314
                [ array_map(function ($v) use ($column) { return $this->normalizeValue($column, $v); }, $value) ]
315
            ] :
316
            [
317 4
                $name . ' IN (??)',
318
                [ array_map(function ($v) use ($column) { return $this->normalizeValue($column, $v); }, $value) ]
319
            ];
320
    }
321
    /**
322
     * Filter the results by a column and a value
323
     * @param  string $column  the column name to filter by (related columns can be used - for example: author.name)
324
     * @param  mixed  $value   a required value, array of values or range of values (range example: ['beg'=>1,'end'=>3])
325
     * @param  bool   $negate  optional boolean indicating that the filter should be negated
326
     * @return $this
327
     */
328 8
    public function filter(string $column, $value, bool $negate = false) : TableQuery
329
    {
330 8
        $sql = $this->filterSQL($column, $value, $negate);
331 8
        return strlen($sql[0]) ? $this->where($sql[0], $sql[1]) : $this;
332
    }
333
    /**
334
     * Filter the results matching any of the criteria
335
     * @param  array $criteria  each row is a column, value and optional negate flag (same as filter method)
336
     * @return $this
337
     */
338 4
    public function any(array $criteria) : TableQuery
339
    {
340 4
        $sql = [];
341 4
        $par = [];
342 4
        foreach ($criteria as $row) {
343 4
            if (isset($row[1])) {
344 4
                $temp = $this->filterSQL($row[0], $row[1] ?? null, $row[2] ?? false);
345 4
                $sql[] = $temp[0];
346 4
                $par = array_merge($par, $temp[1]);
347
            }
348
        }
349 4
        return $this->where('(' . implode(' OR ', $sql) . ')', $par);
350
    }
351
    /**
352
     * Filter the results matching all of the criteria
353
     * @param  array $criteria  each row is a column, value and optional negate flag (same as filter method)
354
     * @return $this
355
     */
356 2
    public function all(array $criteria) : TableQuery
357
    {
358 2
        $sql = [];
359 2
        $par = [];
360 2
        foreach ($criteria as $row) {
361 2
            if (isset($row[1])) {
362 2
                $temp = $this->filterSQL($row[0], $row[1] ?? null, $row[2] ?? false);
363 2
                $sql[] = $temp[0];
364 2
                $par = array_merge($par, $temp[1]);
365
            }
366
        }
367 2
        return $this->where('(' . implode(' AND ', $sql) . ')', $par);
368
    }
369
    /**
370
     * Sort by a column
371
     * @param  string       $column the column name to sort by (related columns can be used - for example: author.name)
372
     * @param  bool|boolean $desc   should the sorting be in descending order, defaults to `false`
373
     * @return $this
374
     */
375
    public function sort(string $column, bool $desc = false) : TableQuery
376
    {
377
        return $this->order($this->getColumn($column)['name'] . ' ' . ($desc ? 'DESC' : 'ASC'));
378
    }
379
    /**
380
     * Group by a column (or columns)
381
     * @param  string|array        $column the column name (or names) to group by
382
     * @return $this
383
     */
384 2
    public function group($column) : TableQuery
385
    {
386 2
        if (!is_array($column)) {
387 2
            $column = [ $column ];
388
        }
389 2
        foreach ($column as $k => $v) {
390 2
            $column[$k] = $this->getColumn($v)['name'];
391
        }
392 2
        return $this->groupBy(implode(', ', $column), []);
393
    }
394
    /**
395
     * Get a part of the data
396
     * @param  int|integer $page    the page number to get (1-based), defaults to 1
397
     * @param  int|integer $perPage the number of records per page - defaults to 25
398
     * @return $this
399
     */
400
    public function paginate(int $page = 1, int $perPage = 25) : TableQuery
401
    {
402
        return $this->limit($perPage, ($page - 1) * $perPage);
403
    }
404 2
    public function __call($name, $data)
405
    {
406 2
        if (strpos($name, 'filterBy') === 0) {
407
            return $this->filter(strtolower(substr($name, 8)), $data[0]);
408
        }
409 2
        if (strpos($name, 'sortBy') === 0) {
410
            return $this->sort(strtolower(substr($name, 6)), $data[0]);
411
        }
412 2
        if (strpos($name, 'groupBy') === 0) {
413 2
            return $this->group(strtolower(substr($name, 7)));
414
        }
415
    }
416
    /**
417
     * Remove all filters, sorting, etc
418
     * @return $this
419
     */
420 6
    public function reset() : TableQuery
421
    {
422 6
        $this->where = [];
423 6
        $this->joins = [];
424 6
        $this->group = [];
425 6
        $this->withr = [];
426 6
        $this->order = [];
427 6
        $this->having = [];
428 6
        $this->aliases = [];
429 6
        $this->li_of = [0,0,0];
430 6
        $this->qiterator = null;
431 6
        return $this;
432
    }
433
    /**
434
     * Apply advanced grouping
435
     * @param  string $sql    SQL statement to use in the GROUP BY clause
436
     * @param  array  $params optional params for the statement (defaults to an empty array)
437
     * @return $this
438
     */
439 2
    public function groupBy(string $sql, array $params = []) : TableQuery
440
    {
441 2
        $this->qiterator = null;
442 2
        $this->group = [ $sql, $params ];
443 2
        return $this;
444
    }
445
    /**
446
     * Join a table to the query (no need to do this for relations defined with foreign keys)
447
     * @param  Table|string $table     the table to join
448
     * @param  array        $fields    what to join on (joined_table_field => other_field) 
449
     * @param  string|null  $name      alias for the join, defaults to the table name 
450
     * @param  bool         $multiple  are multiple rows joined (results in a LEFT JOIN), default to true 
451
     * @return $this
452
     */
453 2
    public function join($table, array $fields, string $name = null, bool $multiple = true)
454
    {
455 2
        $table = $table instanceof Table ? $table : $this->db->definition((string)$table);
456 2
        $name = $name ?? $table->getName();
457 2
        if (isset($this->joins[$name]) || $this->definition->hasRelation($name)) {
458
            throw new DBException('Alias / table name already in use');
459
        }
460 2
        $this->joins[$name] = new TableRelation($name, $table, [], $multiple);
461 2
        foreach ($fields as $k => $v) {
462 2
            $k = explode('.', $k, 2);
463 2
            $k = count($k) == 2 ? $k[1] : $k[0];
464 2
            $this->joins[$name]->keymap[$this->getColumn($name . '.' . $k)['name']] = $this->getColumn($v)['name'];
465
        }
466 2
        return $this;
467
    }
468
    /**
469
     * Apply an advanced filter (can be called multiple times)
470
     * @param  string $sql    SQL statement to be used in the where clause
471
     * @param  array  $params parameters for the SQL statement (defaults to an empty array)
472
     * @return $this
473
     */
474 14
    public function where(string $sql, array $params = []) : TableQuery
475
    {
476 14
        $this->qiterator = null;
477 14
        $this->where[] = [ $sql, $params ];
478 14
        return $this;
479
    }
480
    /**
481
     * Apply an advanced HAVING filter (can be called multiple times)
482
     * @param  string $sql    SQL statement to be used in the HAING clause
483
     * @param  array  $params parameters for the SQL statement (defaults to an empty array)
484
     * @return $this
485
     */
486 2
    public function having(string $sql, array $params = []) : TableQuery
487
    {
488 2
        $this->qiterator = null;
489 2
        $this->having[] = [ $sql, $params ];
490 2
        return $this;
491
    }
492
    /**
493
     * Apply advanced sorting
494
     * @param  string $sql    SQL statement to use in the ORDER clause
495
     * @param  array  $params optional params for the statement (defaults to an empty array)
496
     * @return $this
497
     */
498 2
    public function order(string $sql, array $params = []) : TableQuery
499
    {
500 2
        $this->qiterator = null;
501 2
        $name = null;
502 2
        if (!count($params)) {
503 2
            $name = preg_replace('(\s+(ASC|DESC)\s*$)i', '', $sql);
504
            try {
505 2
                $name = $this->getColumn(trim($name))['name'];
506 2
            } catch (\Exception $e) {
507 2
                $name = null;
508
            }
509
        }
510 2
        $this->order = [ $sql, $params, $name ];
511 2
        return $this;
512
    }
513
    /**
514
     * Apply an advanced limit
515
     * @param  int         $limit  number of rows to return
516
     * @param  int         $offset number of rows to skip from the beginning (defaults to 0)
517
     * @return $this
518
     */
519 2
    public function limit(int $limit, int $offset = 0, bool $limitOnMainTable = false) : TableQuery
520
    {
521 2
        $this->qiterator = null;
522 2
        $this->li_of = [ $limit, $offset, $limitOnMainTable ? 1 : 0 ];
523 2
        return $this;
524
    }
525
    /**
526
     * Get the number of records
527
     * @return int the total number of records (does not respect pagination)
528
     */
529 12
    public function count() : int
530
    {
531 12
        $aliases = [];
532 12
        $getAlias = function ($name) use (&$aliases) {
533
            // to bypass use: return $name;
534 2
            return $aliases[$name] = $aliases[$name] ?? 'alias' . static::SEP . count($aliases);
535 12
        };
536 12
        $table = $this->definition->getName();
537 12
        $sql = 'SELECT COUNT(DISTINCT '.$table.'.'.implode(', '.$table.'.', $this->pkey).') FROM '.$table.' ';
538 12
        $par = [];
539
        
540 12
        $relations = $this->withr;
541 12
        foreach ($relations as $k => $v) {
542
            $getAlias($k);
543
        }
544 12
        $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...
545 12
        $w = $this->where;
546 12
        $h = $this->having;
547 12
        $o = $this->order;
548 12
        $g = $this->group;
549
        $j = array_map(function ($v) { return clone $v; }, $this->joins);
550 12
        foreach ($this->definition->getRelations() as $k => $v) {
551 12
            foreach ($w as $kk => $vv) {
552 8
                if (preg_match('(\b'.preg_quote($k . '.'). ')i', $vv[0])) {
553 2
                    $relations[$k] = [ $v, $table ];
554 8
                    $w[$kk][0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $vv[0]);
555
                }
556
            }
557 12
            if (isset($o[0]) && preg_match('(\b'.preg_quote($k . '.'). ')i', $o[0])) {
558
                $relations[$k] = [ $v, $table ];
559
            }
560 12
            foreach ($h as $kk => $vv) {
561
                if (preg_match('(\b'.preg_quote($k . '.'). ')i', $vv[0])) {
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
                    $h[$kk][0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $vv[0]);
564
                }
565
            }
566 12
            if (isset($g[0]) && preg_match('(\b'.preg_quote($k . '.'). ')i', $g[0])) {
567
                $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...
568
                $g[0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $g[0]);
569
            }
570 12
            foreach ($j as $kk => $v) {
571
                foreach ($v->keymap as $kkk => $vv) {
572
                    if (preg_match('(\b'.preg_quote($k . '.'). ')i', $vv)) {
573
                        $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...
574 12
                        $j[$k]->keymap[$kkk] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $vv);
575
                    }
576
                }
577
            }
578
        }
579
580 12
        foreach ($j as $k => $v) {
581
            $sql .= ($v->many ? 'LEFT ' : '' ) . 'JOIN '.$v->table->getName().' '.$k.' ON ';
582
            $tmp = [];
583
            foreach ($v->keymap as $kk => $vv) {
584
                $tmp[] = $kk.' = '.$vv;
585
            }
586
            $sql .= implode(' AND ', $tmp) . ' ';
587
        }
588 12
        foreach ($relations as $k => $v) {
589 2
            $table = $v[1] !== $this->definition->getName() ? $getAlias($v[1]) : $v[1];
590 2
            $v = $v[0];
591 2
            if ($v->pivot) {
592 2
                $alias = $getAlias($k.'_pivot');
593 2
                $sql .= 'LEFT JOIN '.$v->pivot->getName().' '.$alias.' ON ';
594 2
                $tmp = [];
595 2
                foreach ($v->keymap as $kk => $vv) {
596 2
                    $tmp[] = $table.'.'.$kk.' = '.$alias.'.'.$vv.' ';
597
                }
598 2
                $sql .= implode(' AND ', $tmp) . ' ';
599 2
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$getAlias($k).' ON ';
600 2
                $tmp = [];
601 2
                foreach ($v->pivot_keymap as $kk => $vv) {
602 2
                    $tmp[] = $getAlias($k).'.'.$vv.' = '.$alias.'.'.$kk.' ';
603
                }
604 2
                $sql .= implode(' AND ', $tmp) . ' ';
605
            } else {
606 2
                $alias = $getAlias($k);
607 2
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$alias.' ON ';
608 2
                $tmp = [];
609 2
                foreach ($v->keymap as $kk => $vv) {
610 2
                    $tmp[] = $table.'.'.$kk.' = '.$alias.'.'.$vv.' ';
611
                }
612 2
                if ($v->sql) {
613
                    $tmp[] = $v->sql . ' ';
614
                    $par = array_merge($par, $v->par ?? []);
615
                }
616 2
                $sql .= implode(' AND ', $tmp) . ' ';
617
            }
618
        }
619 12
        if (count($w)) {
620 8
            $sql .= 'WHERE ';
621 8
            $tmp = [];
622 8
            foreach ($w as $v) {
623 8
                $tmp[] = '(' . $v[0] . ')';
624 8
                $par = array_merge($par, $v[1]);
625
            }
626 8
            $sql .= implode(' AND ', $tmp).' ';
627
        }
628 12
        if (count($g)) {
629
            $sql .= 'GROUP BY ' . $g[0] . ' ';
630
            $par = array_merge($par, $g[1]);
631
        }
632 12
        if (count($h)) {
633
            $sql .= 'HAVING ';
634
            $tmp = [];
635
            foreach ($h as $v) {
636
                $tmp[] = '(' . $v[0] . ')';
637
                $par = array_merge($par, $v[1]);
638
            }
639
            $sql .= implode(' AND ', $tmp).' ';
640
        }
641 12
        return $this->db->one($sql, $par);
642
    }
643
    /**
644
     * Specify which columns to fetch (be default all table columns are fetched)
645
     * @param  array $fields optional array of columns to select (related columns can be used too)
646
     * @return $this
647
     */
648 36
    public function columns(array $fields) : TableQuery
649
    {
650 36
        foreach ($fields as $k => $v) {
651 36
            if (strpos($v, '*') !== false) {
652
                $temp = explode('.', $v);
653
                if (count($temp) === 1) {
654
                    $table = $this->definition->getName();
655
                    $cols = $this->definition->getColumns();
656
                } else if (count($temp) === 2) {
657
                    $table = $temp[0];
658
                    if ($this->definition->hasRelation($table)) {
659
                        $cols = $this->definition->getRelation($table)->table->getColumns();
660
                    } else if (isset($this->joins[$table])) {
661
                        $cols = $this->joins[$table]->table->getColumns();
662
                    } else {
663
                        throw new DBException('Invalid foreign table name');
664
                    }
665
                } else {
666
                    array_pop($temp);
667
                    $this->with(implode('.', $temp));
668
                    $table = array_reduce(
669
                        $temp,
670
                        function ($carry, $item) use (&$table) {
671
                            return $table->getRelation($item)->table;
672
                        }
673
                    );
674
                    $cols = $table->getColumns();
675
                    $table = implode(static::SEP, $temp);
676
                }
677
                unset($fields[$k]);
678
                foreach ($cols as $col) {
679 36
                    $fields[] = $table . '.' . $col;
680
                }
681
            }
682
        }
683 36
        $primary = $this->definition->getPrimaryKey();
684 36
        foreach ($fields as $k => $v) {
685
            try {
686 36
                $fields[$k] = $this->getColumn($v)['name'];
687 2
            } catch (DBException $e) {
688 36
                $fields[$k] = $v;
689
            }
690
        }
691 36
        foreach ($primary as $field) {
692 36
            $field = $this->getColumn($field)['name'];
693 36
            if (!in_array($field, $fields)) {
694 36
                $fields[] = $field;
695
            }
696
        }
697 36
        $this->fields = $fields;
698 36
        return $this;
699
    }
700
    /**
701
     * Perform the actual fetch
702
     * @param  array|null $fields optional array of columns to select (related columns can be used too)
703
     * @return TableQueryIterator               the query result as an iterator
704
     */
705 24
    public function iterator(array $fields = null) : TableQueryIterator
706
    {
707 24
        if ($this->qiterator) {
708 10
            return $this->qiterator;
709
        }
710 24
        $aliases = [];
711 24
        $getAlias = function ($name) use (&$aliases) {
712
            // to bypass use: return $name;
713 8
            return $aliases[$name] = $aliases[$name] ?? 'alias' . static::SEP . count($aliases);
714 24
        };
715 24
        $table = $this->definition->getName();
716 24
        if ($fields !== null) {
717 2
            $this->columns($fields);
718
        }
719 24
        $relations = $this->withr;
720 24
        foreach ($relations as $k => $v) {
721 8
            $getAlias($k);
722
        }
723
724 24
        $f = $this->fields;
725 24
        $w = $this->where;
726 24
        $h = $this->having;
727 24
        $o = $this->order;
728 24
        $g = $this->group;
729
        $j = array_map(function ($v) { return clone $v; }, $this->joins);
730
731 24
        $porder = [];
732 24
        foreach ($this->definition->getPrimaryKey() as $field) {
733 24
            $porder[] = $this->getColumn($field)['name'];
734
        }
735
736 24
        if (count($porder) && $this->li_of[2] === 1) {
737
            $ids = $this->ids();
738
            if (count($ids)) {
739
                if (count($porder) > 1) {
740
                    $pkw = [];
741
                    foreach ($porder as $name) {
742
                        $pkw[] = $name . ' = ?';
743
                    }
744
                    $pkw = '(' . implode(' AND ', $pkw) . ')';
745
                    $pkp = [];
746
                    foreach ($ids as $id) {
747
                        foreach ($id as $p) {
748
                            $pkp[] = $p;
749
                        }
750
                    }
751
                    $w[] = [
752
                        implode(' OR ', array_fill(0, count($ids), $pkw)),
753
                        $pkp
754
                    ];
755
                } else {
756
                    $w[] = [ $porder[0] . ' IN ('.implode(',', array_fill(0, count($ids), '?')).')', $ids ];
757
                }
758
            } else {
759
                $w[] = [ '1=0', [] ];
760
            }
761
        }
762
763 24
        foreach ($this->definition->getRelations() as $k => $relation) {
764 24
            foreach ($f as $kk => $field) {
765 24
                if (strpos($field, $k . '.') === 0) {
766
                    $relations[$k] = [ $relation, $table ];
767 24
                    $f[$kk] = str_replace($k . '.', $getAlias($k) . '.', $field);
768
                }
769
            }
770 24
            foreach ($w as $kk => $v) {
771 2
                if (preg_match('(\b'.preg_quote($k . '.'). ')i', $v[0])) {
772
                    $relations[$k] = [ $relation, $table ];
773 2
                    $w[$kk][0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $v[0]);
774
                }
775
            }
776 24
            foreach ($h as $kk => $v) {
777 2
                if (preg_match('(\b'.preg_quote($k . '.'). ')i', $v[0])) {
778
                    $relations[$k] = [ $relation, $table ];
779 2
                    $h[$kk][0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $v[0]);
780
                }
781
            }
782 24
            if (isset($o[0]) && preg_match('(\b'.preg_quote($k . '.'). ')i', $o[0])) {
783
                $relations[$k] = [ $relation, $table ];
784
                $o[0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $o[0]);
785
            }
786 24
            if (isset($g[0]) && preg_match('(\b'.preg_quote($k . '.'). ')i', $g[0])) {
787
                $relations[$k] = [ $relation, $table ];
788
                $g[0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $g[0]);
789
            }
790 24
            foreach ($j as $kk => $v) {
791 2
                foreach ($v->keymap as $kkk => $vv) {
792 2
                    if (preg_match('(\b'.preg_quote($k . '.'). ')i', $vv)) {
793
                        $relations[$k] = [ $relation, $table ];
794 24
                        $j[$k]->keymap[$kkk] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $vv);
795
                    }
796
                }
797
            }
798
        }
799 24
        $select = [];
800 24
        foreach ($f as $k => $field) {
801 24
            $select[] = $field . (!is_numeric($k) ? ' ' . $k : '');
802
        }
803 24
        foreach ($this->withr as $name => $relation) {
804 8
            foreach ($relation[0]->table->getColumns() as $column) {
805 8
                $select[] = $getAlias($name) . '.' . $column . ' ' . $getAlias($name . static::SEP . $column);
806
            }
807
        }
808 24
        $sql = 'SELECT '.implode(', ', $select).' FROM '.$table.' ';
809 24
        $par = [];
810 24
        foreach ($j as $k => $v) {
811 2
            $sql .= ($v->many ? 'LEFT ' : '' ) . 'JOIN '.$v->table->getName().' '.$k.' ON ';
812 2
            $tmp = [];
813 2
            foreach ($v->keymap as $kk => $vv) {
814 2
                $tmp[] = $kk.' = '.$vv;
815
            }
816 2
            $sql .= implode(' AND ', $tmp) . ' ';
817
        }
818 24
        foreach ($relations as $relation => $v) {
819 8
            $table = $v[1] !== $this->definition->getName() ? $getAlias($v[1]) : $v[1];
820 8
            $v = $v[0];
821 8
            if ($v->pivot) {
822 6
                $alias = $getAlias($relation.'_pivot');
823 6
                $sql .= 'LEFT JOIN '.$v->pivot->getName().' '.$alias.' ON ';
824 6
                $tmp = [];
825 6
                foreach ($v->keymap as $kk => $vv) {
826 6
                    $tmp[] = $table.'.'.$kk.' = '.$alias.'.'.$vv.' ';
827
                }
828 6
                $sql .= implode(' AND ', $tmp) . ' ';
829 6
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$getAlias($relation).' ON ';
830 6
                $tmp = [];
831 6
                foreach ($v->pivot_keymap as $kk => $vv) {
832 6
                    $tmp[] = $getAlias($relation).'.'.$vv.' = '.$alias.'.'.$kk.' ';
833
                }
834 6
                $sql .= implode(' AND ', $tmp) . ' ';
835
            } else {
836 8
                $alias = $getAlias($relation);
837
838 8
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$alias.' ON ';
839 8
                $tmp = [];
840 8
                foreach ($v->keymap as $kk => $vv) {
841 8
                    $tmp[] = $table.'.'.$kk.' = '.$alias.'.'.$vv.' ';
842
                }
843 8
                if ($v->sql) {
844
                    $tmp[] = $v->sql . ' ';
845
                    $par = array_merge($par, $v->par ?? []);
846
                }
847 8
                $sql .= implode(' AND ', $tmp) . ' ';
848
            }
849
        }
850 24
        if (count($w)) {
851 2
            $sql .= 'WHERE ';
852 2
            $tmp = [];
853 2
            foreach ($w as $v) {
854 2
                $tmp[] = '(' . $v[0] . ')';
855 2
                $par = array_merge($par, $v[1]);
856
            }
857 2
            $sql .= implode(' AND ', $tmp).' ';
858
        }
859 24
        if (count($g)) {
860 2
            $sql .= 'GROUP BY ' . $g[0] . ' ';
861 2
            $par = array_merge($par, $g[1]);
862
        }
863 24
        if (count($h)) {
864 2
            $sql .= 'HAVING ';
865 2
            $tmp = [];
866 2
            foreach ($h as $v) {
867 2
                $tmp[] = '(' . $v[0] . ')';
868 2
                $par = array_merge($par, $v[1]);
869
            }
870 2
            $sql .= implode(' AND ', $tmp).' ';
871
        }
872 24
        if (count($o)) {
873 2
            $sql .= 'ORDER BY ' . $o[0] . ' ';
874 2
            $par = array_merge($par, $o[1]);
875
        }
876 24
        if (count($porder)) {
877 24
            $pdir = (count($o) && strpos($o[0], 'DESC') !== false) ? 'DESC' : 'ASC';
878
            $porder = array_map(function ($v) use ($pdir) { return $v . ' ' . $pdir; }, $porder);
879 24
            $sql .= (count($o) ? ', ' : 'ORDER BY ') . implode(', ', $porder) . ' ';
880
        }
881 24
        if (($this->li_of[2] === 0 || !count($porder)) && $this->li_of[0]) {
882 2
            if ($this->db->driverName() === 'oracle') {
883
                if ((int)$this->db->driverOption('version', 0) >= 12) {
884
                    $sql .= 'OFFSET ' . $this->li_of[1] . ' ROWS FETCH NEXT ' . $this->li_of[0] . ' ROWS ONLY';
885
                } else {
886
                    $f = array_map(function ($v) {
887
                        $v = explode(' ', trim($v), 2);
888
                        if (count($v) === 2) { return $v[1]; }
889
                        $v = explode('.', $v[0], 2);
890
                        return count($v) === 2 ? $v[1] : $v[0];
891
                    }, $select);
892
                    $sql = "SELECT " . implode(', ', $f) . " 
893
                            FROM (
894
                                SELECT tbl__.*, rownum rnum__ FROM (
895
                                    " . $sql . "
896
                                ) tbl__ 
897
                                WHERE rownum <= " . ($this->li_of[0] + $this->li_of[1]) . "
898
                            ) WHERE rnum__ > " . $this->li_of[1];
899
                }
900
            } else {
901 2
                $sql .= 'LIMIT ' . $this->li_of[0] . ' OFFSET ' . $this->li_of[1];
902
            }
903
        }
904 24
        return $this->qiterator = new TableQueryIterator(
905 24
            $this->db->get($sql, $par), 
906 24
            $this->pkey,
907 24
            $this->withr,
908 24
            $aliases
909
        );
910
    }
911
    /**
912
     * Perform the actual fetch
913
     * @param  array|null $fields optional array of columns to select (related columns can be used too)
914
     * @return array               the query result as an array
915
     */
916 2
    public function select(array $fields = null) : array
917
    {
918 2
        return iterator_to_array($this->iterator($fields));
919
    }
920
    /**
921
     * Insert a new row in the table
922
     * @param  array   $data   key value pairs, where each key is the column name and the value is the value to insert
923
     * @return array           the inserted ID where keys are column names and values are column values
924
     */
925 6
    public function insert(array $data) : array
926
    {
927 6
        $table = $this->definition->getName();
928 6
        $columns = $this->definition->getFullColumns();
929 6
        $insert = [];
930 6
        foreach ($data as $column => $value) {
931 6
            if (isset($columns[$column])) {
932 6
                $insert[$column] = $this->normalizeValue($columns[$column], $value);
933
            }
934
        }
935 4
        if (!count($insert)) {
936
            throw new DBException('No valid columns to insert');
937
        }
938 4
        $sql = 'INSERT INTO '.$table.' ('.implode(', ', array_keys($insert)).') VALUES (??)';
939 4
        $par = [$insert];
940 4
        $primary = $this->definition->getPrimaryKey();
941 4
        if (!count($primary)) {
942
            $this->db->query($sql, $par);
943
            return [];
944
        }
945 4
        if ($this->db->driverName() === 'oracle') {
946
            $ret = [];
947
            foreach ($primary as $k) {
948
                $ret[$k] = str_repeat(' ', 255);
949
                $par[] = &$ret[$k];
950
            }
951
            $sql .= ' RETURNING ' . implode(',', $primary) . ' INTO ' . implode(',', array_fill(0, count($primary), '?'));
952
            $this->db->query($sql, $par);
953
            return $ret;
954
        } else {
955 4
            $ret = [];
956 4
            $ins = $this->db->query($sql, $par)->insertID();
957 4
            foreach ($primary as $k) {
958 4
                $ret[$k] = $data[$k] ?? $ins;
959
            }
960 4
            return $ret;
961
        }
962
    }
963
    /**
964
     * Update the filtered rows with new data
965
     * @param  array  $data key value pairs, where each key is the column name and the value is the value to insert
966
     * @return int          the number of affected rows
967
     */
968 2
    public function update(array $data) : int
969
    {
970 2
        $table = $this->definition->getName();
971 2
        $columns = $this->definition->getFullColumns();
972 2
        $update = [];
973 2
        foreach ($data as $column => $value) {
974 2
            if (isset($columns[$column])) {
975 2
                $update[$column] = $this->normalizeValue($columns[$column], $value);
976
            }
977
        }
978 2
        if (!count($update)) {
979
            throw new DBException('No valid columns to update');
980
        }
981 2
        $sql = 'UPDATE '.$table.' SET ';
982 2
        $par = [];
983
        $sql .= implode(', ', array_map(function ($v) { return $v . ' = ?'; }, array_keys($update))) . ' ';
984 2
        $par = array_merge($par, array_values($update));
985 2
        if (count($this->where)) {
986 2
            $sql .= 'WHERE ';
987 2
            $tmp = [];
988 2
            foreach ($this->where as $v) {
989 2
                $tmp[] = $v[0];
990 2
                $par = array_merge($par, $v[1]);
991
            }
992 2
            $sql .= implode(' AND ', $tmp) . ' ';
993
        }
994 2
        if (count($this->order)) {
995
            $sql .= $this->order[0];
996
            $par = array_merge($par, $this->order[1]);
997
        }
998 2
        return $this->db->query($sql, $par)->affected();
999
    }
1000
    /**
1001
     * Delete the filtered rows from the DB
1002
     * @return int the number of deleted rows
1003
     */
1004 2
    public function delete() : int
1005
    {
1006 2
        $table = $this->definition->getName();
1007 2
        $sql = 'DELETE FROM '.$table.' ';
1008 2
        $par = [];
1009 2
        if (count($this->where)) {
1010 2
            $sql .= 'WHERE ';
1011 2
            $tmp = [];
1012 2
            foreach ($this->where as $v) {
1013 2
                $tmp[] = $v[0];
1014 2
                $par = array_merge($par, $v[1]);
1015
            }
1016 2
            $sql .= implode(' AND ', $tmp) . ' ';
1017
        }
1018 2
        if (count($this->order)) {
1019
            $sql .= $this->order[0];
1020
            $par = array_merge($par, $this->order[1]);
1021
        }
1022 2
        return $this->db->query($sql, $par)->affected();
1023
    }
1024
    /**
1025
     * Solve the n+1 queries problem by prefetching a relation by name
1026
     * @param  string $relation the relation name to fetch along with the data
1027
     * @return $this
1028
     */
1029 8
    public function with(string $relation) : TableQuery
1030
    {
1031 8
        $this->qiterator = null;
1032 8
        $parts = explode('.', $relation);
1033 8
        $table = $this->definition;
1034 8
        array_reduce(
1035 8
            $parts,
1036 8
            function ($carry, $item) use (&$table) {
1037 8
                $relation = $table->getRelation($item);
1038 8
                if (!$relation) {
1039
                    throw new DBException('Invalid relation name');
1040
                }
1041 8
                $name = $carry ? $carry . static::SEP . $item : $item;
1042 8
                $this->withr[$name] = [ $relation, $carry ?? $table->getName() ];
1043 8
                $table = $relation->table;
1044 8
                return $name;
1045 8
            }
1046
        );
1047 8
        return $this;
1048
    }
1049
1050 2
    public function getIterator()
1051
    {
1052 2
        return $this->iterator();
1053
    }
1054
1055 20
    public function offsetGet($offset)
1056
    {
1057 20
        return $this->iterator()->offsetGet($offset);
1058
    }
1059
    public function offsetExists($offset)
1060
    {
1061
        return $this->iterator()->offsetExists($offset);
1062
    }
1063
    public function offsetUnset($offset)
1064
    {
1065
        return $this->iterator()->offsetUnset($offset);
1066
    }
1067
    public function offsetSet($offset, $value)
1068
    {
1069
        return $this->iterator()->offsetSet($offset, $value);
1070
    }
1071
1072
    public function collection(array $fields = null) : Collection
1073
    {
1074
        return new Collection($this->iterator($fields));
1075
    }
1076
1077
    public function ids()
1078
    {
1079
        if (count($this->group)) {
1080
            throw new DBException('Can not LIMIT result set by master table when GROUP BY is used');
1081
        }
1082
        if (count($this->order) && !isset($this->order[2])) {
1083
            throw new DBException('Can not LIMIT result set by master table with a complex ORDER BY query');
1084
        }
1085
1086
        $aliases = [];
1087
        $getAlias = function ($name) use (&$aliases) {
1088
            // to bypass use: return $name;
1089
            return $aliases[$name] = $aliases[$name] ?? 'alias' . static::SEP . count($aliases);
1090
        };
1091
        
1092
        $table = $this->definition->getName();
1093
        $relations = $this->withr;
1094
        foreach ($relations as $k => $v) {
1095
            $getAlias($k);
1096
        }
1097
        $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...
1098
        $w = $this->where;
1099
        $h = $this->having;
1100
        $o = $this->order;
1101
        $g = $this->group;
1102
        $j = array_map(function ($v) { return clone $v; }, $this->joins);
1103
        foreach ($this->definition->getRelations() as $k => $v) {
1104
            foreach ($w as $kk => $vv) {
1105
                if (preg_match('(\b'.preg_quote($k . '.'). ')i', $vv[0])) {
1106
                    $relations[$k] = [ $v, $table ];
1107
                    $w[$kk][0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $vv[0]);
1108
                }
1109
            }
1110
            if (isset($o[0]) && preg_match('(\b'.preg_quote($k . '.'). ')i', $o[0])) {
1111
                $relations[$k] = [ $v, $table ];
1112
                $o[0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $o[0]);
1113
                $o[2] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $o[2]);
1114
            }
1115
            foreach ($h as $kk => $vv) {
1116
                if (preg_match('(\b'.preg_quote($k . '.'). ')i', $vv[0])) {
1117
                    $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...
1118
                    $h[$kk][0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $vv[0]);
1119
                }
1120
            }
1121
            if (isset($g[0]) && preg_match('(\b'.preg_quote($k . '.'). ')i', $g[0])) {
1122
                $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...
1123
                $g[0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $g[0]);
1124
            }
1125
            foreach ($j as $kk => $v) {
1126
                foreach ($v->keymap as $kkk => $vv) {
1127
                    if (preg_match('(\b'.preg_quote($k . '.'). ')i', $vv)) {
1128
                        $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...
1129
                        $j[$k]->keymap[$kkk] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $vv);
1130
                    }
1131
                }
1132
            }
1133
        }
1134
1135
        $key = array_map(function ($v) use ($table) { return $table . '.' . $v; }, $this->pkey);
1136
        $own = false;
1137
        $dir = 'ASC';
1138
        if (count($o)) {
1139
            $dir = strpos($o[0], ' DESC') ? 'DESC' : 'ASC';
1140
            $own = strpos($o[2], $table . '.') === 0;
1141
        }
1142
1143
        $dst = $key;
1144
        if ($own) {
1145
            // if using own table - do not use max/min in order - that will prevent index usage
1146
            $dst[] = $o[2] . ' orderbyfix___';
1147
        }
1148
        $dst = array_unique($dst);
1149
1150
        $par = [];
1151
        $sql  = 'SELECT DISTINCT '.implode(', ', $dst).' FROM '.$table.' ';
1152
        foreach ($j as $k => $v) {
1153
            $sql .= ($v->many ? 'LEFT ' : '' ) . 'JOIN '.$v->table->getName().' '.$k.' ON ';
1154
            $tmp = [];
1155
            foreach ($v->keymap as $kk => $vv) {
1156
                $tmp[] = $kk.' = '.$vv;
1157
            }
1158
            $sql .= implode(' AND ', $tmp) . ' ';
1159
        }
1160
        foreach ($relations as $k => $v) {
1161
            $table = $v[1] !== $this->definition->getName() ? $getAlias($v[1]) : $v[1];
1162
            $v = $v[0];
1163
            if ($v->pivot) {
1164
                $alias = $getAlias($k.'_pivot');
1165
                $sql .= 'LEFT JOIN '.$v->pivot->getName().' '.$alias.' ON ';
1166
                $tmp = [];
1167
                foreach ($v->keymap as $kk => $vv) {
1168
                    $tmp[] = $table.'.'.$kk.' = '.$alias.'.'.$vv.' ';
1169
                }
1170
                $sql .= implode(' AND ', $tmp) . ' ';
1171
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$getAlias($k).' ON ';
1172
                $tmp = [];
1173
                foreach ($v->pivot_keymap as $kk => $vv) {
1174
                    $tmp[] = $getAlias($k).'.'.$vv.' = '.$alias.'.'.$kk.' ';
1175
                }
1176
                $sql .= implode(' AND ', $tmp) . ' ';
1177
            } else {
1178
                $alias = $getAlias($k);
1179
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$alias.' ON ';
1180
                $tmp = [];
1181
                foreach ($v->keymap as $kk => $vv) {
1182
                    $tmp[] = $table.'.'.$kk.' = '.$alias.'.'.$vv.' ';
1183
                }
1184
                if ($v->sql) {
1185
                    $tmp[] = $v->sql . ' ';
1186
                    $par = array_merge($par, $v->par ?? []);
1187
                }
1188
                $sql .= implode(' AND ', $tmp) . ' ';
1189
            }
1190
        }
1191
        if (count($w)) {
1192
            $sql .= 'WHERE ';
1193
            $tmp = [];
1194
            foreach ($w as $v) {
1195
                $tmp[] = '(' . $v[0] . ')';
1196
                $par = array_merge($par, $v[1]);
1197
            }
1198
            $sql .= implode(' AND ', $tmp).' ';
1199
        }
1200
        if (!$own) {
1201
            $sql .= 'GROUP BY ' . implode(', ', $key) . ' ';
1202
        }
1203
        if (count($h)) {
1204
            $sql .= 'HAVING ';
1205
            $tmp = [];
1206
            foreach ($h as $v) {
1207
                $tmp[] = '(' . $v[0] . ')';
1208
                $par = array_merge($par, $v[1]);
1209
            }
1210
            $sql .= implode(' AND ', $tmp).' ';
1211
        }
1212
        if (count($o)) {
1213
            $sql .= 'ORDER BY ';
1214
            if ($own) {
1215
                $sql .= $o[2] . ' ' . $dir;
1216
            } else {
1217
                $sql .= 'MAX('.$o[2].') ' . $dir;
1218
            }
1219
        }
1220
        $porder = [];
1221
        $pdir = (count($o) && strpos($o[0], 'DESC') !== false) ? 'DESC' : 'ASC';
1222
        foreach ($this->definition->getPrimaryKey() as $field) {
1223
            $porder[] = $this->getColumn($field)['name'] . ' ' . $pdir;
1224
        }
1225
        if (count($porder)) {
1226
            $sql .= (count($o) ? ', ' : 'ORDER BY ') . implode(', ', $porder) . ' ';
1227
        }
1228
1229
        if ($this->li_of[0]) {
1230
            if ($this->db->driverName() === 'oracle') {
1231
                if ((int)$this->db->driverOption('version', 0) >= 12) {
1232
                    $sql .= 'OFFSET ' . $this->li_of[1] . ' ROWS FETCH NEXT ' . $this->li_of[0] . ' ROWS ONLY';
1233
                } else {
1234
                    $f = array_map(function ($v) {
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...
1235
                        $v = explode(' ', trim($v), 2);
1236
                        if (count($v) === 2) { return $v[1]; }
1237
                        $v = explode('.', $v[0], 2);
1238
                        return count($v) === 2 ? $v[1] : $v[0];
1239
                    }, $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...
1240
                    $sql = "SELECT " . implode(', ', $dst) . " 
1241
                            FROM (
1242
                                SELECT tbl__.*, rownum rnum__ FROM (
1243
                                    " . $sql . "
1244
                                ) tbl__ 
1245
                                WHERE rownum <= " . ($this->li_of[0] + $this->li_of[1]) . "
1246
                            ) WHERE rnum__ > " . $this->li_of[1];
1247
                }
1248
            } else {
1249
                $sql .= 'LIMIT ' . $this->li_of[0] . ' OFFSET ' . $this->li_of[1];
1250
            }
1251
        }
1252
        return array_map(function ($v) use ($own) {
1253
            if (isset($v['orderbyfix___'])) {
1254
                unset($v['orderbyfix___']);
1255
            }
1256
            return count($v) === 1 ? array_values($v)[0] : $v;
1257
        }, $this->db->all($sql, $par, null, false, false));
1258
    }
1259
}
1260