Completed
Push — master ( 72179e...eb4f06 )
by Ivan
02:58
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\database\DBInterface;
5
use vakata\database\DBException;
6
use vakata\database\ResultInterface;
7
8
/**
9
 * A database query class
10
 */
11
class TableQuery implements \IteratorAggregate, \ArrayAccess, \Countable
12
{
13
    const SEP = '___';
14
    /**
15
     * @var DBInterface
16
     */
17
    protected $db;
18
    /**
19
     * @var Table
20
     */
21
    protected $definition;
22
    /**
23
     * @var TableQueryIterator|null
24
     */
25
    protected $qiterator;
26
27
    /**
28
     * @var array
29
     */
30
    protected $where = [];
31
    /**
32
     * @var array
33
     */
34
    protected $order = [];
35
    /**
36
     * @var array
37
     */
38
    protected $group = [];
39
    /**
40
     * @var array
41
     */
42
    protected $having = [];
43
    /**
44
     * @var int[]
45
     */
46
    protected $li_of = [0,0];
47
    /**
48
     * @var array
49
     */
50
    protected $fields = [];
51
    /**
52
     * @var array
53
     */
54
    protected $withr = [];
55
    /**
56
     * @var array
57
     */
58
    protected $joins = [];
59
    /**
60
     * @var array
61
     */
62
    protected $pkey = [];
63
    /**
64
     * @var array
65
     */
66
    protected $aliases = [];
67
68
    /**
69
     * Create an instance
70
     * @param  DBInterface    $db         the database connection
71
     * @param  Table|string   $table      the name or definition of the main table in the query
72
     */
73 13
    public function __construct(DBInterface $db, $table)
74
    {
75 13
        $this->db = $db;
76 13
        $this->definition = $table instanceof Table ? $table : $this->db->definition((string)$table);
77 13
        $primary = $this->definition->getPrimaryKey();
78 13
        $columns = $this->definition->getColumns();
79 13
        $this->pkey = count($primary) ? $primary : $columns;
80 13
        $this->columns($columns);
81 13
    }
82
    public function __clone()
83
    {
84
        $this->reset();
85
    }
86
    /**
87
     * Get the table definition of the queried table
88
     * @return Table        the definition
89
     */
90
    public function getDefinition() : Table
91
    {
92
        return $this->definition;
93
    }
94
95 13
    protected function getColumn($column)
96
    {
97 13
        $column = explode('.', $column);
98 13
        if (count($column) === 1) {
99 13
            $column = [ $this->definition->getName(), $column[0] ];
100 13
            $col = $this->definition->getColumn($column[1]);
101 13
            if (!$col) {
102 13
                throw new DBException('Invalid column name in own table');
103
            }
104 2
        } elseif (count($column) === 2) {
105 2
            if ($column[0] === $this->definition->getName()) {
106
                $col = $this->definition->getColumn($column[1]);
107
                if (!$col) {
108
                    throw new DBException('Invalid column name in own table');
109
                }
110
            } else {
111 2
                if ($this->definition->hasRelation($column[0])) {
112 1
                    $col = $this->definition->getRelation($column[0])->table->getColumn($column[1]);
113 1
                    if (!$col) {
114 1
                        throw new DBException('Invalid column name in related table');
115
                    }
116 1
                } else if (isset($this->joins[$column[0]])) {
117 1
                    $col = $this->joins[$column[0]]->table->getColumn($column[1]);
118 1
                    if (!$col) {
119 1
                        throw new DBException('Invalid column name in related table');
120
                    }
121
                } else {
122 2
                    throw new DBException('Invalid foreign table name: ' . implode(',', $column));
123
                }
124
            }
125
        } else {
126
            $name = array_pop($column);
127
            $this->with(implode('.', $column));
128
            $table = $this->definition;
129
            $table = array_reduce(
130
                $column,
131
                function ($carry, $item) use (&$table) {
132
                    $table = $table->getRelation($item)->table;
133
                    return $table;
134
                }
135
            );
136
            $col = $table->getColumn($name);
137
            $column = [ implode(static::SEP, $column), $name ];
138
        }
139 13
        return [ 'name' => implode('.', $column), 'data' => $col ];
140
    }
141 4
    protected function normalizeValue(TableColumn $col, $value)
142
    {
143 4
        if ($value === null && $col->isNullable()) {
144
            return null;
145
        }
146 4
        switch ($col->getBasicType()) {
147 4
            case 'date':
148
                if (is_string($value)) {
149
                    $temp = strtotime($value);
150
                    if (!$temp) {
151
                        return null;
152
                    }
153
                    return date('Y-m-d', strtotime($value));
154
                }
155
                if (is_int($value)) {
156
                    return date('Y-m-d', $value);
157
                }
158
                if ($value instanceof \DateTime) {
159
                    return $value->format('Y-m-d');
160
                }
161
                return $value;
162 4
            case 'datetime':
163
                if (is_string($value)) {
164
                    $temp = strtotime($value);
165
                    if (!$temp) {
166
                        return null;
167
                    }
168
                    return date('Y-m-d H:i:s', strtotime($value));
169
                }
170
                if (is_int($value)) {
171
                    return date('Y-m-d H:i:s', $value);
172
                }
173
                if ($value instanceof \DateTime) {
174
                    return $value->format('Y-m-d H:i:s');
175
                }
176
                return $value;
177 4
            case 'enum':
178
                if (is_int($value)) {
179
                    return $value;
180
                }
181
                if (!in_array($value, $col->getValues())) {
182
                    return 0;
183
                }
184
                return $value;
185 4
            case 'int':
186 2
                return (int)$value;
187
            default:
188 3
                return $value;
189
        }
190
    }
191
192
    /**
193
     * Filter the results by a column and a value
194
     * @param  string $column  the column name to filter by (related columns can be used - for example: author.name)
195
     * @param  mixed  $value   a required value, array of values or range of values (range example: ['beg'=>1,'end'=>3])
196
     * @param  bool   $negate  optional boolean indicating that the filter should be negated
197
     * @return $this
198
     */
199 2
    public function filter(string $column, $value, bool $negate = false) : TableQuery
200
    {
201 2
        list($name, $column) = array_values($this->getColumn($column));
202 2
        if (is_null($value)) {
203
            return $negate ?
204
                $this->where($name . ' IS NOT NULL') :
205
                $this->where($name . ' IS NULL');
206
        }
207 2
        if (!is_array($value)) {
208 2
            return $negate ?
209
                $this->where(
210
                    $name . ' <> ?',
211
                    [ $this->normalizeValue($column, $value) ]
212
                ) :
213 2
                $this->where(
214 2
                    $name . ' = ?',
215 2
                    [ $this->normalizeValue($column, $value) ]
216
                );
217
        }
218 1
        if (isset($value['beg']) && strlen($value['beg']) && (!isset($value['end']) || !strlen($value['end']))) {
219
            $value = [ 'gte' => $value['beg'] ];
220
        }
221 1
        if (isset($value['end']) && strlen($value['end']) && (!isset($value['beg']) || !strlen($value['beg']))) {
222
            $value = [ 'lte' => $value['end'] ];
223
        }
224 1
        if (isset($value['beg']) && isset($value['end'])) {
225
            return $negate ?
226
                $this->where(
227
                    $name.' NOT BETWEEN ? AND ?',
228
                    [
229
                        $this->normalizeValue($column, $value['beg']),
230
                        $this->normalizeValue($column, $value['end'])
231
                    ]
232
                ) :
233
                $this->where(
234
                    $name.' BETWEEN ? AND ?',
235
                    [
236
                        $this->normalizeValue($column, $value['beg']),
237
                        $this->normalizeValue($column, $value['end'])
238
                    ]
239
                );
240
        }
241 1
        if (isset($value['gt']) || isset($value['lt']) || isset($value['gte']) || isset($value['lte'])) {
242 1
            if (isset($value['gt'])) {
243 1
                $this->where(
244 1
                    $name. ' ' . ($negate ? '<=' : '>') . ' ?',
245 1
                    [ $this->normalizeValue($column, $value['gt']) ]
246
                );
247
            }
248 1
            if (isset($value['gte'])) {
249 1
                $this->where(
250 1
                    $name. ' ' . ($negate ? '<' : '>=') . ' ?',
251 1
                    [ $this->normalizeValue($column, $value['gte']) ]
252
                );
253
            }
254 1
            if (isset($value['lt'])) {
255 1
                $this->where(
256 1
                    $name. ' ' . ($negate ? '>=' : '<') . ' ?',
257 1
                    [ $this->normalizeValue($column, $value['lt']) ]
258
                );
259
            }
260 1
            if (isset($value['lte'])) {
261 1
                $this->where(
262 1
                    $name. ' ' . ($negate ? '>' : '<=') . ' ?',
263 1
                    [ $this->normalizeValue($column, $value['lte']) ]
264
                );
265
            }
266 1
            return $this;
267
        }
268
        return $negate ?
269
            $this->where(
270
                $name . ' NOT IN (??)',
271
                [ array_map(function ($v) use ($column) { return $this->normalizeValue($column, $v); }, $value) ]
272
            ) :
273
            $this->where(
274
                $name . ' IN (??)',
275
                [ array_map(function ($v) use ($column) { return $this->normalizeValue($column, $v); }, $value) ]
276
            );
277
    }
278
    /**
279
     * Sort by a column
280
     * @param  string       $column the column name to sort by (related columns can be used - for example: author.name)
281
     * @param  bool|boolean $desc   should the sorting be in descending order, defaults to `false`
282
     * @return $this
283
     */
284
    public function sort(string $column, bool $desc = false) : TableQuery
285
    {
286
        return $this->order($this->getColumn($column)['name'] . ' ' . ($desc ? 'DESC' : 'ASC'));
287
    }
288
    /**
289
     * Group by a column (or columns)
290
     * @param  string|array        $column the column name (or names) to group by
291
     * @return $this
292
     */
293 1
    public function group($column) : TableQuery
294
    {
295 1
        if (!is_array($column)) {
296 1
            $column = [ $column ];
297
        }
298 1
        foreach ($column as $k => $v) {
299 1
            $column[$k] = $this->getColumn($v)['name'];
300
        }
301 1
        return $this->groupBy(implode(', ', $column), []);
302
    }
303
    /**
304
     * Get a part of the data
305
     * @param  int|integer $page    the page number to get (1-based), defaults to 1
306
     * @param  int|integer $perPage the number of records per page - defaults to 25
307
     * @return $this
308
     */
309
    public function paginate(int $page = 1, int $perPage = 25) : TableQuery
310
    {
311
        return $this->limit($perPage, ($page - 1) * $perPage);
312
    }
313 1
    public function __call($name, $data)
314
    {
315 1
        if (strpos($name, 'filterBy') === 0) {
316
            return $this->filter(strtolower(substr($name, 8)), $data[0]);
317
        }
318 1
        if (strpos($name, 'sortBy') === 0) {
319
            return $this->sort(strtolower(substr($name, 6)), $data[0]);
320
        }
321 1
        if (strpos($name, 'groupBy') === 0) {
322 1
            return $this->group(strtolower(substr($name, 7)));
323
        }
324
    }
325
    /**
326
     * Remove all filters, sorting, etc
327
     * @return $this
328
     */
329
    public function reset() : TableQuery
330
    {
331
        $this->where = [];
332
        $this->joins = [];
333
        $this->group = [];
334
        $this->withr = [];
335
        $this->order = [];
336
        $this->having = [];
337
        $this->aliases = [];
338
        $this->li_of = [0,0];
339
        $this->qiterator = null;
340
        return $this;
341
    }
342
    /**
343
     * Apply advanced grouping
344
     * @param  string $sql    SQL statement to use in the GROUP BY clause
345
     * @param  array  $params optional params for the statement (defaults to an empty array)
346
     * @return $this
347
     */
348 1
    public function groupBy(string $sql, array $params = []) : TableQuery
349
    {
350 1
        $this->qiterator = null;
351 1
        $this->group = [ $sql, $params ];
352 1
        return $this;
353
    }
354
    /**
355
     * Join a table to the query (no need to do this for relations defined with foreign keys)
356
     * @param  Table|string $table     the table to join
357
     * @param  array        $fields    what to join on (joined_table_field => other_field) 
358
     * @param  string|null  $name      alias for the join, defaults to the table name 
359
     * @param  bool         $multiple  are multiple rows joined (results in a LEFT JOIN), default to true 
360
     * @return $this
361
     */
362 1
    public function join($table, array $fields, string $name = null, bool $multiple = true)
363
    {
364 1
        $table = $table instanceof Table ? $table : $this->db->definition((string)$table);
365 1
        $name = $name ?? $table->getName();
366 1
        if (isset($this->joins[$name]) || $this->definition->hasRelation($name)) {
367
            throw new DBException('Alias / table name already in use');
368
        }
369 1
        $this->joins[$name] = new TableRelation($name, $table, [], $multiple);
370 1
        foreach ($fields as $k => $v) {
371 1
            $k = explode('.', $k, 2);
372 1
            $k = count($k) == 2 ? $k[1] : $k[0];
373 1
            $this->joins[$name]->keymap[$this->getColumn($name . '.' . $k)['name']] = $this->getColumn($v)['name'];
374
        }
375 1
        return $this;
376
    }
377
    /**
378
     * Apply an advanced filter (can be called multiple times)
379
     * @param  string $sql    SQL statement to be used in the where clause
380
     * @param  array  $params parameters for the SQL statement (defaults to an empty array)
381
     * @return $this
382
     */
383 3
    public function where(string $sql, array $params = []) : TableQuery
384
    {
385 3
        $this->qiterator = null;
386 3
        $this->where[] = [ $sql, $params ];
387 3
        return $this;
388
    }
389
    /**
390
     * Apply an advanced HAVING filter (can be called multiple times)
391
     * @param  string $sql    SQL statement to be used in the HAING clause
392
     * @param  array  $params parameters for the SQL statement (defaults to an empty array)
393
     * @return $this
394
     */
395 1
    public function having(string $sql, array $params = []) : TableQuery
396
    {
397 1
        $this->qiterator = null;
398 1
        $this->having[] = [ $sql, $params ];
399 1
        return $this;
400
    }
401
    /**
402
     * Apply advanced sorting
403
     * @param  string $sql    SQL statement to use in the ORDER clause
404
     * @param  array  $params optional params for the statement (defaults to an empty array)
405
     * @return $this
406
     */
407 1
    public function order(string $sql, array $params = []) : TableQuery
408
    {
409 1
        $this->qiterator = null;
410 1
        $this->order = [ $sql, $params ];
411 1
        return $this;
412
    }
413
    /**
414
     * Apply an advanced limit
415
     * @param  int         $limit  number of rows to return
416
     * @param  int         $offset number of rows to skip from the beginning (defaults to 0)
417
     * @return $this
418
     */
419 1
    public function limit(int $limit, int $offset = 0) : TableQuery
420
    {
421 1
        $this->qiterator = null;
422 1
        $this->li_of = [ $limit, $offset ];
423 1
        return $this;
424
    }
425
    /**
426
     * Get the number of records
427
     * @return int the total number of records (does not respect pagination)
428
     */
429 3
    public function count() : int
430
    {
431 3
        $aliases = [];
432 3
        $getAlias = function ($name) use (&$aliases) {
433
            // 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...
434 1
            return $aliases[$name] = $aliases[$name] ?? 'alias' . static::SEP . count($aliases);
435 3
        };
436 3
        $table = $this->definition->getName();
437 3
        $sql = 'SELECT COUNT(DISTINCT '.$table.'.'.implode(', '.$table.'.', $this->pkey).') FROM '.$table.' ';
438 3
        $par = [];
439
        
440 3
        $relations = $this->withr;
441 3
        foreach ($relations as $k => $v) {
442
            $getAlias($k);
443
        }
444 3
        $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...
445 3
        $w = $this->where;
446 3
        $h = $this->having;
447 3
        $o = $this->order;
448 3
        $g = $this->group;
449
        $j = array_map(function ($v) { return clone $v; }, $this->joins);
450 3
        foreach ($this->definition->getRelations() as $k => $v) {
451 3
            foreach ($w as $kk => $vv) {
452 1
                if (preg_match('(\b'.preg_quote($k . '.'). ')i', $vv[0])) {
453 1
                    $relations[$k] = [ $v, $table ];
454 1
                    $w[$kk][0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $vv[0]);
455
                }
456
            }
457 3
            if (isset($o[0]) && preg_match('(\b'.preg_quote($k . '.'). ')i', $o[0])) {
458
                $relations[$k] = [ $v, $table ];
459
            }
460 3
            foreach ($h as $kk => $vv) {
461
                if (preg_match('(\b'.preg_quote($k . '.'). ')i', $vv[0])) {
462
                    $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...
463
                    $h[$kk][0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $vv[0]);
464
                }
465
            }
466 3
            if (isset($g[0]) && preg_match('(\b'.preg_quote($k . '.'). ')i', $g[0])) {
467
                $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...
468
                $g[0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $g[0]);
469
            }
470 3
            foreach ($j as $kk => $v) {
471
                foreach ($v->keymap as $kkk => $vv) {
472
                    if (preg_match('(\b'.preg_quote($k . '.'). ')i', $vv)) {
473
                        $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...
474 3
                        $j[$k]->keymap[$kkk] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $vv);
475
                    }
476
                }
477
            }
478
        }
479
480 3
        foreach ($j as $k => $v) {
481
            $sql .= ($v->many ? 'LEFT ' : '' ) . 'JOIN '.$v->table->getName().' '.$k.' ON ';
482
            $tmp = [];
483
            foreach ($v->keymap as $kk => $vv) {
484
                $tmp[] = $kk.' = '.$vv;
485
            }
486
            $sql .= implode(' AND ', $tmp) . ' ';
487
        }
488 3
        foreach ($relations as $k => $v) {
489 1
            $table = $v[1] !== $this->definition->getName() ? $getAlias($v[1]) : $v[1];
490 1
            $v = $v[0];
491 1
            if ($v->pivot) {
492 1
                $alias = $getAlias($k.'_pivot');
493 1
                $sql .= 'LEFT JOIN '.$v->pivot->getName().' '.$alias.' ON ';
494 1
                $tmp = [];
495 1
                foreach ($v->keymap as $kk => $vv) {
496 1
                    $tmp[] = $table.'.'.$kk.' = '.$alias.'.'.$vv.' ';
497
                }
498 1
                $sql .= implode(' AND ', $tmp) . ' ';
499 1
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$getAlias($k).' ON ';
500 1
                $tmp = [];
501 1
                foreach ($v->pivot_keymap as $kk => $vv) {
502 1
                    $tmp[] = $getAlias($k).'.'.$vv.' = '.$alias.'.'.$kk.' ';
503
                }
504 1
                $sql .= implode(' AND ', $tmp) . ' ';
505
            } else {
506 1
                $alias = $getAlias($k);
507 1
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$alias.' ON ';
508 1
                $tmp = [];
509 1
                foreach ($v->keymap as $kk => $vv) {
510 1
                    $tmp[] = $table.'.'.$kk.' = '.$alias.'.'.$vv.' ';
511
                }
512 1
                if ($v->sql) {
513
                    $tmp[] = $v->sql . ' ';
514
                    $par = array_merge($par, $v->par ?? []);
515
                }
516 1
                $sql .= implode(' AND ', $tmp) . ' ';
517
            }
518
        }
519 3
        if (count($w)) {
520 1
            $sql .= 'WHERE ';
521 1
            $tmp = [];
522 1
            foreach ($w as $v) {
523 1
                $tmp[] = '(' . $v[0] . ')';
524 1
                $par = array_merge($par, $v[1]);
525
            }
526 1
            $sql .= implode(' AND ', $tmp).' ';
527
        }
528 3
        if (count($g)) {
529
            $sql .= 'GROUP BY ' . $g[0] . ' ';
530
            $par = array_merge($par, $g[1]);
531
        }
532 3
        if (count($h)) {
533
            $sql .= 'HAVING ';
534
            $tmp = [];
535
            foreach ($h as $v) {
536
                $tmp[] = '(' . $v[0] . ')';
537
                $par = array_merge($par, $v[1]);
538
            }
539
            $sql .= implode(' AND ', $tmp).' ';
540
        }
541 3
        return $this->db->one($sql, $par);
542
    }
543
    /**
544
     * Specify which columns to fetch (be default all table columns are fetched)
545
     * @param  array $fields optional array of columns to select (related columns can be used too)
546
     * @return $this
547
     */
548 13
    public function columns(array $fields) : TableQuery
549
    {
550 13
        foreach ($fields as $k => $v) {
551 13
            if (strpos($v, '*') !== false) {
552
                $temp = explode('.', $v);
553
                if (count($temp) === 1) {
554
                    $table = $this->definition->getName();
555
                    $cols = $this->definition->getColumns();
556
                } else if (count($temp) === 2) {
557
                    $table = $temp[0];
558
                    if ($this->definition->hasRelation($table)) {
559
                        $cols = $this->definition->getRelation($table)->table->getColumns();
560
                    } else if (isset($this->joins[$table])) {
561
                        $cols = $this->joins[$table]->table->getColumns();
562
                    } else {
563
                        throw new DBException('Invalid foreign table name');
564
                    }
565
                } else {
566
                    array_pop($temp);
567
                    $this->with(implode('.', $temp));
568
                    $table = array_reduce(
569
                        $temp,
570
                        function ($carry, $item) use (&$table) {
571
                            return $table->getRelation($item)->table;
572
                        }
573
                    );
574
                    $cols = $table->getColumns();
575
                    $table = implode(static::SEP, $temp);
576
                }
577
                unset($fields[$k]);
578
                foreach ($cols as $col) {
579 13
                    $fields[] = $table . '.' . $col;
580
                }
581
            }
582
        }
583 13
        $primary = $this->definition->getPrimaryKey();
584 13
        foreach ($fields as $k => $v) {
585
            try {
586 13
                $fields[$k] = $this->getColumn($v)['name'];
587 1
            } catch (DBException $e) {
588 13
                $fields[$k] = $v;
589
            }
590
        }
591 13
        foreach ($primary as $field) {
592 13
            $field = $this->getColumn($field)['name'];
593 13
            if (!in_array($field, $fields)) {
594 13
                $fields[] = $field;
595
            }
596
        }
597 13
        $this->fields = $fields;
598 13
        return $this;
599
    }
600
    /**
601
     * Perform the actual fetch
602
     * @param  array|null $fields optional array of columns to select (related columns can be used too)
603
     * @return TableQueryIterator               the query result as an iterator
604
     */
605 11
    public function iterator(array $fields = null) : TableQueryIterator
606
    {
607 11
        if ($this->qiterator) {
608 5
            return $this->qiterator;
609
        }
610 11
        $aliases = [];
611 11
        $getAlias = function ($name) use (&$aliases) {
612
            // 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...
613 4
            return $aliases[$name] = $aliases[$name] ?? 'alias' . static::SEP . count($aliases);
614 11
        };
615 11
        $table = $this->definition->getName();
616 11
        if ($fields !== null) {
617 1
            $this->columns($fields);
618
        }
619 11
        $relations = $this->withr;
620 11
        foreach ($relations as $k => $v) {
621 4
            $getAlias($k);
622
        }
623
624 11
        $f = $this->fields;
625 11
        $w = $this->where;
626 11
        $h = $this->having;
627 11
        $o = $this->order;
628 11
        $g = $this->group;
629
        $j = array_map(function ($v) { return clone $v; }, $this->joins);
630 11
        foreach ($this->definition->getRelations() as $k => $relation) {
631 11
            foreach ($f as $kk => $field) {
632 11
                if (strpos($field, $k . '.') === 0) {
633
                    $relations[$k] = [ $relation, $table ];
634 11
                    $f[$kk] = str_replace($k . '.', $getAlias($k) . '.', $field);
635
                }
636
            }
637 11
            foreach ($w as $kk => $v) {
638
                if (preg_match('(\b'.preg_quote($k . '.'). ')i', $v[0])) {
639
                    $relations[$k] = [ $relation, $table ];
640
                    $w[$kk][0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $v[0]);
641
                }
642
            }
643 11
            foreach ($h as $kk => $v) {
644 1
                if (preg_match('(\b'.preg_quote($k . '.'). ')i', $v[0])) {
645
                    $relations[$k] = [ $relation, $table ];
646 1
                    $h[$kk][0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $v[0]);
647
                }
648
            }
649 11
            if (isset($o[0]) && preg_match('(\b'.preg_quote($k . '.'). ')i', $o[0])) {
650
                $relations[$k] = [ $relation, $table ];
651
                $o[0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $o[0]);
652
            }
653 11
            if (isset($g[0]) && preg_match('(\b'.preg_quote($k . '.'). ')i', $g[0])) {
654
                $relations[$k] = [ $relation, $table ];
655
                $g[0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $g[0]);
656
            }
657 11
            foreach ($j as $kk => $v) {
658 1
                foreach ($v->keymap as $kkk => $vv) {
659 1
                    if (preg_match('(\b'.preg_quote($k . '.'). ')i', $vv)) {
660
                        $relations[$k] = [ $relation, $table ];
661 11
                        $j[$k]->keymap[$kkk] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $vv);
662
                    }
663
                }
664
            }
665
        }
666 11
        $select = [];
667 11
        foreach ($f as $k => $field) {
668 11
            $select[] = $field . (!is_numeric($k) ? ' ' . $k : '');
669
        }
670 11
        foreach ($this->withr as $name => $relation) {
671 4
            foreach ($relation[0]->table->getColumns() as $column) {
672 4
                $select[] = $getAlias($name) . '.' . $column . ' ' . $getAlias($name . static::SEP . $column);
673
            }
674
        }
675 11
        $sql = 'SELECT '.implode(', ', $select).' FROM '.$table.' ';
676 11
        $par = [];
677 11
        foreach ($j as $k => $v) {
678 1
            $sql .= ($v->many ? 'LEFT ' : '' ) . 'JOIN '.$v->table->getName().' '.$k.' ON ';
679 1
            $tmp = [];
680 1
            foreach ($v->keymap as $kk => $vv) {
681 1
                $tmp[] = $kk.' = '.$vv;
682
            }
683 1
            $sql .= implode(' AND ', $tmp) . ' ';
684
        }
685 11
        foreach ($relations as $relation => $v) {
686 4
            $table = $v[1] !== $this->definition->getName() ? $getAlias($v[1]) : $v[1];
687 4
            $v = $v[0];
688 4
            if ($v->pivot) {
689 3
                $alias = $getAlias($relation.'_pivot');
690 3
                $sql .= 'LEFT JOIN '.$v->pivot->getName().' '.$alias.' ON ';
691 3
                $tmp = [];
692 3
                foreach ($v->keymap as $kk => $vv) {
693 3
                    $tmp[] = $table.'.'.$kk.' = '.$alias.'.'.$vv.' ';
694
                }
695 3
                $sql .= implode(' AND ', $tmp) . ' ';
696 3
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$getAlias($relation).' ON ';
697 3
                $tmp = [];
698 3
                foreach ($v->pivot_keymap as $kk => $vv) {
699 3
                    $tmp[] = $getAlias($relation).'.'.$vv.' = '.$alias.'.'.$kk.' ';
700
                }
701 3
                $sql .= implode(' AND ', $tmp) . ' ';
702
            } else {
703 4
                $alias = $getAlias($relation);
704
705 4
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$alias.' ON ';
706 4
                $tmp = [];
707 4
                foreach ($v->keymap as $kk => $vv) {
708 4
                    $tmp[] = $table.'.'.$kk.' = '.$alias.'.'.$vv.' ';
709
                }
710 4
                if ($v->sql) {
711
                    $tmp[] = $v->sql . ' ';
712
                    $par = array_merge($par, $v->par ?? []);
713
                }
714 4
                $sql .= implode(' AND ', $tmp) . ' ';
715
            }
716
        }
717 11
        if (count($w)) {
718
            $sql .= 'WHERE ';
719
            $tmp = [];
720
            foreach ($w as $v) {
721
                $tmp[] = '(' . $v[0] . ')';
722
                $par = array_merge($par, $v[1]);
723
            }
724
            $sql .= implode(' AND ', $tmp).' ';
725
        }
726 11
        if (count($g)) {
727 1
            $sql .= 'GROUP BY ' . $g[0] . ' ';
728 1
            $par = array_merge($par, $g[1]);
729
        }
730 11
        if (count($h)) {
731 1
            $sql .= 'HAVING ';
732 1
            $tmp = [];
733 1
            foreach ($h as $v) {
734 1
                $tmp[] = '(' . $v[0] . ')';
735 1
                $par = array_merge($par, $v[1]);
736
            }
737 1
            $sql .= implode(' AND ', $tmp).' ';
738
        }
739 11
        if (count($o)) {
740 1
            $sql .= 'ORDER BY ' . $o[0] . ' ';
741 1
            $par = array_merge($par, $o[1]);
742
        }
743 11
        $porder = [];
744 11
        foreach ($this->definition->getPrimaryKey() as $field) {
745 11
            $porder[] = $this->getColumn($field)['name'];
746
        }
747 11
        if (count($porder)) {
748 11
            $sql .= (count($o) ? ', ' : 'ORDER BY ') . implode(', ', $porder) . ' ';
749
        }
750
751 11
        if ($this->li_of[0]) {
752 1
            if ($this->db->driverName() === 'oracle') {
753
                if ((int)$this->db->driverOption('version', 0) >= 12) {
754
                    $sql .= 'OFFSET ' . $this->li_of[1] . ' ROWS FETCH NEXT ' . $this->li_of[0] . ' ROWS ONLY';
755
                } else {
756
                    $f = array_map(function ($v) {
757
                        $v = explode(' ', trim($v), 2);
758
                        if (count($v) === 2) { return $v[1]; }
759
                        $v = explode('.', $v[0], 2);
760
                        return count($v) === 2 ? $v[1] : $v[0];
761
                    }, $select);
762
                    $sql = "SELECT " . implode(', ', $f) . " 
763
                            FROM (
764
                                SELECT tbl__.*, rownum rnum__ FROM (
765
                                    " . $sql . "
766
                                ) tbl__ 
767
                                WHERE rownum <= " . ($this->li_of[0] + $this->li_of[1]) . "
768
                            ) WHERE rnum__ > " . $this->li_of[1];
769
                }
770
            } else {
771 1
                $sql .= 'LIMIT ' . $this->li_of[0] . ' OFFSET ' . $this->li_of[1];
772
            }
773
        }
774 11
        return $this->qiterator = new TableQueryIterator(
775 11
            $this->db->get($sql, $par), 
776 11
            $this->pkey,
777 11
            $this->withr,
778 11
            $aliases
779
        );
780
    }
781
    /**
782
     * Perform the actual fetch
783
     * @param  array|null $fields optional array of columns to select (related columns can be used too)
784
     * @return array               the query result as an array
785
     */
786 1
    public function select(array $fields = null) : array
787
    {
788 1
        return iterator_to_array($this->iterator($fields));
789
    }
790
    /**
791
     * Insert a new row in the table
792
     * @param  array   $data   key value pairs, where each key is the column name and the value is the value to insert
793
     * @return array           the inserted ID where keys are column names and values are column values
794
     */
795 1
    public function insert(array $data) : array
796
    {
797 1
        $table = $this->definition->getName();
798 1
        $columns = $this->definition->getFullColumns();
799 1
        $insert = [];
800 1
        foreach ($data as $column => $value) {
801 1
            if (isset($columns[$column])) {
802 1
                $insert[$column] = $this->normalizeValue($columns[$column], $value);
803
            }
804
        }
805 1
        if (!count($insert)) {
806
            throw new DBException('No valid columns to insert');
807
        }
808 1
        $sql = 'INSERT INTO '.$table.' ('.implode(', ', array_keys($insert)).') VALUES (??)';
809 1
        $par = [$insert];
810 1
        $primary = $this->definition->getPrimaryKey();
811 1
        if (!count($primary)) {
812
            $this->db->query($sql, $par);
813
            return [];
814
        }
815 1
        if ($this->db->driverName() === 'oracle') {
816
            $ret = [];
817
            foreach ($primary as $k) {
818
                $ret[$k] = str_repeat(' ', 255);
819
                $par[] = &$ret[$k];
820
            }
821
            $sql .= ' RETURNING ' . implode(',', $primary) . ' INTO ' . implode(',', array_fill(0, count($primary), '?'));
822
            $this->db->query($sql, $par);
823
            return $ret;
824
        } else {
825 1
            $ret = [];
826 1
            $ins = $this->db->query($sql, $par)->insertID();
827 1
            foreach ($primary as $k) {
828 1
                $ret[$k] = $data[$k] ?? $ins;
829
            }
830 1
            return $ret;
831
        }
832
    }
833
    /**
834
     * Update the filtered rows with new data
835
     * @param  array  $data key value pairs, where each key is the column name and the value is the value to insert
836
     * @return int          the number of affected rows
837
     */
838 1
    public function update(array $data) : int
839
    {
840 1
        $table = $this->definition->getName();
841 1
        $columns = $this->definition->getFullColumns();
842 1
        $update = [];
843 1
        foreach ($data as $column => $value) {
844 1
            if (isset($columns[$column])) {
845 1
                $update[$column] = $this->normalizeValue($columns[$column], $value);
846
            }
847
        }
848 1
        if (!count($update)) {
849
            throw new DBException('No valid columns to update');
850
        }
851 1
        $sql = 'UPDATE '.$table.' SET ';
852 1
        $par = [];
853
        $sql .= implode(', ', array_map(function ($v) { return $v . ' = ?'; }, array_keys($update))) . ' ';
854 1
        $par = array_merge($par, array_values($update));
855 1
        if (count($this->where)) {
856 1
            $sql .= 'WHERE ';
857 1
            $tmp = [];
858 1
            foreach ($this->where as $v) {
859 1
                $tmp[] = $v[0];
860 1
                $par = array_merge($par, $v[1]);
861
            }
862 1
            $sql .= implode(' AND ', $tmp) . ' ';
863
        }
864 1
        if (count($this->order)) {
865
            $sql .= $this->order[0];
866
            $par = array_merge($par, $this->order[1]);
867
        }
868 1
        return $this->db->query($sql, $par)->affected();
869
    }
870
    /**
871
     * Delete the filtered rows from the DB
872
     * @return int the number of deleted rows
873
     */
874 1
    public function delete() : int
875
    {
876 1
        $table = $this->definition->getName();
877 1
        $sql = 'DELETE FROM '.$table.' ';
878 1
        $par = [];
879 1
        if (count($this->where)) {
880 1
            $sql .= 'WHERE ';
881 1
            $tmp = [];
882 1
            foreach ($this->where as $v) {
883 1
                $tmp[] = $v[0];
884 1
                $par = array_merge($par, $v[1]);
885
            }
886 1
            $sql .= implode(' AND ', $tmp) . ' ';
887
        }
888 1
        if (count($this->order)) {
889
            $sql .= $this->order[0];
890
            $par = array_merge($par, $this->order[1]);
891
        }
892 1
        return $this->db->query($sql, $par)->affected();
893
    }
894
    /**
895
     * Solve the n+1 queries problem by prefetching a relation by name
896
     * @param  string $relation the relation name to fetch along with the data
897
     * @return $this
898
     */
899 4
    public function with(string $relation) : TableQuery
900
    {
901 4
        $this->qiterator = null;
902 4
        $parts = explode('.', $relation);
903 4
        $table = $this->definition;
904 4
        array_reduce(
905 4
            $parts,
906 4
            function ($carry, $item) use (&$table) {
907 4
                $relation = $table->getRelation($item);
908 4
                if (!$relation) {
909
                    throw new DBException('Invalid relation name');
910
                }
911 4
                $name = $carry ? $carry . static::SEP . $item : $item;
912 4
                $this->withr[$name] = [ $relation, $carry ?? $table->getName() ];
913 4
                $table = $relation->table;
914 4
                return $name;
915 4
            }
916
        );
917 4
        return $this;
918
    }
919
920 1
    public function getIterator()
921
    {
922 1
        return $this->iterator();
923
    }
924
925 9
    public function offsetGet($offset)
926
    {
927 9
        return $this->iterator()->offsetGet($offset);
928
    }
929
    public function offsetExists($offset)
930
    {
931
        return $this->iterator()->offsetExists($offset);
932
    }
933
    public function offsetUnset($offset)
934
    {
935
        return $this->iterator()->offsetUnset($offset);
936
    }
937
    public function offsetSet($offset, $value)
938
    {
939
        return $this->iterator()->offsetSet($offset, $value);
940
    }
941
942
    public function collection(array $fields = null) : Collection
943
    {
944
        return new Collection($this->iterator($fields));
945
    }
946
}
947