Completed
Push — master ( 0c3707...a98725 )
by Ivan
02:20
created

TableQuery   D

Complexity

Total Complexity 173

Size/Duplication

Total Lines 850
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 64.9%

Importance

Changes 0
Metric Value
wmc 173
lcom 1
cbo 6
dl 0
loc 850
ccs 307
cts 473
cp 0.649
rs 4.4444
c 0
b 0
f 0

31 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 3
A __clone() 0 4 1
A getDefinition() 0 4 1
D getColumn() 0 46 10
C normalizeValue() 0 50 17
C filter() 0 73 21
A sort() 0 4 2
A group() 0 10 3
A paginate() 0 4 1
A __call() 0 12 4
A reset() 0 12 1
A groupBy() 0 6 1
B join() 0 15 6
A where() 0 6 1
A having() 0 6 1
A order() 0 6 1
A limit() 0 6 1
F count() 0 79 20
C columns() 0 52 12
F iterator() 0 136 37
A select() 0 4 1
C insert() 0 38 8
C update() 0 32 7
A delete() 0 20 4
A with() 0 20 3
A getIterator() 0 4 1
A offsetGet() 0 4 1
A offsetExists() 0 4 1
A offsetUnset() 0 4 1
A offsetSet() 0 4 1
A collection() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like TableQuery often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TableQuery, and based on these observations, apply Extract Interface, too.

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