Completed
Push — master ( b02daa...bcbea6 )
by Ivan
11:11
created

TableQuery::having()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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