Completed
Push — master ( a6d599...c90248 )
by Ivan
04:03
created

TableQuery::iterator()   F

Complexity

Conditions 36
Paths > 20000

Size

Total Lines 142
Code Lines 102

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 76
CRAP Score 49.1516

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 142
ccs 76
cts 97
cp 0.7835
rs 2
cc 36
eloc 102
nc 3939841
nop 1
crap 49.1516

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
    /**
14
     * @var DBInterface
15
     */
16
    protected $db;
17
    /**
18
     * @var Table
19
     */
20
    protected $definition;
21
    /**
22
     * @var TableQueryIterator|null
23
     */
24
    protected $qiterator;
25
26
    /**
27
     * @var array
28
     */
29
    protected $where = [];
30
    /**
31
     * @var array
32
     */
33
    protected $order = [];
34
    /**
35
     * @var array
36
     */
37
    protected $group = [];
38
    /**
39
     * @var array
40
     */
41
    protected $having = [];
42
    /**
43
     * @var int[]
44
     */
45
    protected $li_of = [0,0];
46
    protected $fields = [];
47
    /**
48
     * @var array
49
     */
50
    protected $withr = [];
51
    /**
52
     * @var array
53
     */
54
    protected $joins = [];
55
56
    /**
57
     * Create an instance
58
     * @param  DBInterface        $db         the database connection
59
     * @param  Table|string  $definition     the name or definition of the main table in the query
0 ignored issues
show
Bug introduced by
There is no parameter named $definition. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
60
     */
61 12
    public function __construct(DBInterface $db, $table)
62
    {
63 12
        $this->db = $db;
64 12
        $this->definition = $table instanceof Table ? $table : $this->db->definition((string)$table);
65 12
        $this->columns($this->definition->getColumns());
66 12
    }
67
    public function __clone()
68
    {
69
        $this->reset();
70
    }
71
    /**
72
     * Get the table definition of the queried table
73
     * @return Table        the definition
74
     */
75
    public function getDefinition() : Table
76
    {
77
        return $this->definition;
78
    }
79
80 12
    protected function getColumn($column)
81
    {
82 12
        $column = explode('.', $column, 2);
83 12
        if (count($column) === 1) {
84 12
            $column = [ $this->definition->getName(), $column[0] ];
85
        }
86 12
        if ($column[0] === $this->definition->getName()) {
87 12
            $col = $this->definition->getColumn($column[1]);
88 12
            if (!$col) {
89 12
                throw new DBException('Invalid column name in own table');
90
            }
91
        } else {
92 2
            if ($this->definition->hasRelation($column[0])) {
93 1
                $col = $this->definition->getRelation($column[0])->table->getColumn($column[1]);
94 1
                if (!$col) {
95 1
                    throw new DBException('Invalid column name in related table');
96
                }
97 1
            } else if (isset($this->joins[$column[0]])) {
98 1
                $col = $this->joins[$column[0]]->table->getColumn($column[1]);
99 1
                if (!$col) {
100 1
                    throw new DBException('Invalid column name in related table');
101
                }
102
            } else {
103 1
                throw new DBException('Invalid foreign table name: ' . implode(',', $column));
104
            }
105
        }
106 12
        return [ 'name' => implode('.', $column), 'data' => $col ];
107
    }
108 4
    protected function normalizeValue(TableColumn $col, $value)
109
    {
110 4
        if ($value === null && $col->isNullable()) {
111
            return null;
112
        }
113 4
        switch ($col->getBasicType()) {
114 4
            case 'date':
115
                if (is_string($value)) {
116
                    return date('Y-m-d', strtotime($value));
117
                }
118
                if (is_int($value)) {
119
                    return date('Y-m-d', $value);
120
                }
121
                if ($value instanceof \DateTime) {
122
                    return $value->format('Y-m-d');
123
                }
124
                return $value;
125 4
            case 'datetime':
126
                if (is_string($value)) {
127
                    return date('Y-m-d H:i:s', strtotime($value));
128
                }
129
                if (is_int($value)) {
130
                    return date('Y-m-d H:i:s', $value);
131
                }
132
                if ($value instanceof \DateTime) {
133
                    return $value->format('Y-m-d H:i:s');
134
                }
135
                return $value;
136 4
            case 'enum':
137
                if (is_int($value)) {
138
                    return $value;
139
                }
140
                if (!in_array($value, $col->getValues())) {
141
                    return 0;
142
                }
143
                return $value;
144 4
            case 'int':
145 1
                return (int)$value;
146
            default:
147 3
                return $value;
148
        }
149
    }
150
    /**
151
     * Filter the results by a column and a value
152
     * @param  string $column  the column name to filter by (related columns can be used - for example: author.name)
153
     * @param  mixed  $value   a required value, array of values or range of values (range example: ['beg'=>1,'end'=>3])
154
     * @param  bool   $negate  optional boolean indicating that the filter should be negated
155
     * @return $this
156
     */
157 2
    public function filter(string $column, $value, bool $negate = false) : TableQuery
158
    {
159 2
        list($name, $column) = array_values($this->getColumn($column));
160 2
        if (is_null($value)) {
161
            return $negate ?
162
                $this->where($name . ' IS NOT NULL') :
163
                $this->where($name . ' IS NULL');
164
        }
165 2
        if (!is_array($value)) {
166 2
            return $negate ?
167
                $this->where(
168
                    $name . ' <> ?',
169
                    [ $this->normalizeValue($column, $value) ]
170
                ) :
171 2
                $this->where(
172 2
                    $name . ' = ?',
173 2
                    [ $this->normalizeValue($column, $value) ]
174
                );
175
        }
176
        if (isset($value['beg']) && isset($value['end'])) {
177
            return $negate ?
178
                $this->where(
179
                    $name.' NOT BETWEEN ? AND ?',
180
                    [
181
                        $this->normalizeValue($column, $value['beg']),
182
                        $this->normalizeValue($column, $value['end'])
183
                    ]
184
                ) :
185
                $this->where(
186
                    $name.' BETWEEN ? AND ?',
187
                    [
188
                        $this->normalizeValue($column, $value['beg']),
189
                        $this->normalizeValue($column, $value['end'])
190
                    ]
191
                );
192
        }
193
        return $negate ?
194
            $this->where(
195
                $name . ' NOT IN (??)',
196
                [ array_map(function ($v) use ($column) { return $this->normalizeValue($column, $v); }, $value) ]
197
            ) :
198
            $this->where(
199
                $name . ' IN (??)',
200
                [ array_map(function ($v) use ($column) { return $this->normalizeValue($column, $v); }, $value) ]
201
            );
202
    }
203
    /**
204
     * Sort by a column
205
     * @param  string       $column the column name to sort by (related columns can be used - for example: author.name)
206
     * @param  bool|boolean $desc   should the sorting be in descending order, defaults to `false`
207
     * @return $this
208
     */
209
    public function sort(string $column, bool $desc = false) : TableQuery
210
    {
211
        return $this->order($this->getColumn($column)['name'] . ' ' . ($desc ? 'DESC' : 'ASC'));
212
    }
213
    /**
214
     * Group by a column (or columns)
215
     * @param  string|array        $column the column name (or names) to group by
216
     * @return $this
217
     */
218 1
    public function group($column) : TableQuery
219
    {
220 1
        if (!is_array($column)) {
221 1
            $column = [ $column ];
222
        }
223 1
        foreach ($column as $k => $v) {
224 1
            $column[$k] = $this->getColumn($v)['name'];
225
        }
226 1
        return $this->groupBy(implode(', ', $column), []);
227
    }
228
    /**
229
     * Get a part of the data
230
     * @param  int|integer $page    the page number to get (1-based), defaults to 1
231
     * @param  int|integer $perPage the number of records per page - defaults to 25
232
     * @return $this
233
     */
234
    public function paginate(int $page = 1, int $perPage = 25) : TableQuery
235
    {
236
        return $this->limit($perPage, ($page - 1) * $perPage);
237
    }
238 1
    public function __call($name, $data)
239
    {
240 1
        if (strpos($name, 'filterBy') === 0) {
241
            return $this->filter(strtolower(substr($name, 8)), $data[0]);
242
        }
243 1
        if (strpos($name, 'sortBy') === 0) {
244
            return $this->sort(strtolower(substr($name, 6)), $data[0]);
245
        }
246 1
        if (strpos($name, 'groupBy') === 0) {
247 1
            return $this->group(strtolower(substr($name, 7)));
248
        }
249
    }
250
    /**
251
     * Remove all filters, sorting, etc
252
     * @return $this
253
     */
254
    public function reset() : TableQuery
255
    {
256
        $this->where = [];
257
        $this->joins = [];
258
        $this->group = [];
259
        $this->withr = [];
260
        $this->order = [];
261
        $this->having = [];
262
        $this->li_of = [0,0];
263
        $this->qiterator = null;
264
        return $this;
265
    }
266
    /**
267
     * Apply advanced grouping
268
     * @param  string $sql    SQL statement to use in the GROUP BY clause
269
     * @param  array  $params optional params for the statement (defaults to an empty array)
270
     * @return $this
271
     */
272 1
    public function groupBy(string $sql, array $params = []) : TableQuery
273
    {
274 1
        $this->qiterator = null;
275 1
        $this->group = [ $sql, $params ];
276 1
        return $this;
277
    }
278
    /**
279
     * Join a table to the query (no need to do this for relations defined with foreign keys)
280
     * @param  Table|string $table     the table to join
281
     * @param  array        $fields    what to join on (joined_table_field => other_field) 
282
     * @param  string|null  $name      alias for the join, defaults to the table name 
283
     * @param  bool         $multiple  are multiple rows joined (results in a LEFT JOIN), default to true 
284
     * @return $this
285
     */
286 1
    public function join($table, array $fields, string $name = null, bool $multiple = true)
287
    {
288 1
        $table = $table instanceof Table ? $table : $this->db->definition((string)$table);
289 1
        $name = $name ?? $table->getName();
290 1
        if (isset($this->joins[$name]) || $this->definition->hasRelation($name)) {
291
            throw new DBException('Alias / table name already in use');
292
        }
293 1
        $this->joins[$name] = new TableRelation($name, $table, [], $multiple);
294 1
        foreach ($fields as $k => $v) {
295 1
            $k = explode('.', $k, 2);
296 1
            $k = count($k) == 2 ? $k[1] : $k[0];
297 1
            $this->joins[$name]->keymap[$this->getColumn($name . '.' . $k)['name']] = $this->getColumn($v)['name'];
298
        }
299 1
        return $this;
300
    }
301
    /**
302
     * Apply an advanced filter (can be called multiple times)
303
     * @param  string $sql    SQL statement to be used in the where clause
304
     * @param  array  $params parameters for the SQL statement (defaults to an empty array)
305
     * @return $this
306
     */
307 3
    public function where(string $sql, array $params = []) : TableQuery
308
    {
309 3
        $this->qiterator = null;
310 3
        $this->where[] = [ $sql, $params ];
311 3
        return $this;
312
    }
313
    /**
314
     * Apply an advanced HAVING filter (can be called multiple times)
315
     * @param  string $sql    SQL statement to be used in the HAING clause
316
     * @param  array  $params parameters for the SQL statement (defaults to an empty array)
317
     * @return $this
318
     */
319 1
    public function having(string $sql, array $params = []) : TableQuery
320
    {
321 1
        $this->qiterator = null;
322 1
        $this->having[] = [ $sql, $params ];
323 1
        return $this;
324
    }
325
    /**
326
     * Apply advanced sorting
327
     * @param  string $sql    SQL statement to use in the ORDER clause
328
     * @param  array  $params optional params for the statement (defaults to an empty array)
329
     * @return $this
330
     */
331 1
    public function order(string $sql, array $params = []) : TableQuery
332
    {
333 1
        $this->qiterator = null;
334 1
        $this->order = [ $sql, $params ];
335 1
        return $this;
336
    }
337
    /**
338
     * Apply an advanced limit
339
     * @param  int         $limit  number of rows to return
340
     * @param  int         $offset number of rows to skip from the beginning (defaults to 0)
341
     * @return $this
342
     */
343 1
    public function limit(int $limit, int $offset = 0) : TableQuery
344
    {
345 1
        $this->qiterator = null;
346 1
        $this->li_of = [ $limit, $offset ];
347 1
        return $this;
348
    }
349
    /**
350
     * Get the number of records
351
     * @return int the total number of records (does not respect pagination)
352
     */
353 3
    public function count() : int
354
    {
355 3
        $table = $this->definition->getName();
356 3
        $primary = $this->definition->getPrimaryKey();
357 3
        $sql = 'SELECT COUNT(DISTINCT '.$table.'.'.implode(', '.$table.'.', $primary).') FROM '.$table.' ';
358 3
        $par = [];
359
        
360 3
        $relations = $this->withr;
361 3
        foreach ($this->definition->getRelations() as $k => $v) {
362 3
            foreach ($this->where as $vv) {
363 1
                if (strpos($vv[0], $k . '.') !== false) {
364 1
                    $relations[] = $k;
365
                }
366
            }
367 3
            if (isset($this->order[0]) && strpos($this->order[0], $k . '.') !== false) {
368 3
                $relations[] = $k;
369
            }
370
        }
371
372 3
        foreach ($this->joins as $k => $v) {
373
            $sql .= ($v->many ? 'LEFT ' : '' ) . 'JOIN '.$v->table->getName().' '.$k.' ON ';
374
            $tmp = [];
375
            foreach ($v->keymap as $kk => $vv) {
376
                $tmp[] = $kk.' = '.$vv;
377
            }
378
            $sql .= implode(' AND ', $tmp) . ' ';
379
        }
380 3
        foreach (array_unique($relations) as $k) {
381 1
            $v = $this->definition->getRelation($k);
382 1
            if ($v->pivot) {
383 1
                $sql .= 'LEFT JOIN '.$v->pivot->getName().' '.$k.'_pivot ON ';
384 1
                $tmp = [];
385 1
                foreach ($v->keymap as $kk => $vv) {
386 1
                    $tmp[] = $table.'.'.$kk.' = '.$k.'_pivot.'.$vv.' ';
387
                }
388 1
                $sql .= implode(' AND ', $tmp) . ' ';
389 1
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$k.' ON ';
390 1
                $tmp = [];
391 1
                foreach ($v->pivot_keymap as $kk => $vv) {
392 1
                    $tmp[] = $k.'.'.$vv.' = '.$k.'_pivot.'.$kk.' ';
393
                }
394 1
                $sql .= implode(' AND ', $tmp) . ' ';
395
            } else {
396 1
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$k.' ON ';
397 1
                $tmp = [];
398 1
                foreach ($v->keymap as $kk => $vv) {
399 1
                    $tmp[] = $table.'.'.$kk.' = '.$k.'.'.$vv.' ';
400
                }
401 1
                if ($v->sql) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $v->sql of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
402
                    $tmp[] = $v->sql . ' ';
403
                    $par = array_merge($par, $v->par ?? []);
404
                }
405 1
                $sql .= implode(' AND ', $tmp) . ' ';
406
            }
407
        }
408 3
        if (count($this->where)) {
409 1
            $sql .= 'WHERE ';
410 1
            $tmp = [];
411 1
            foreach ($this->where as $v) {
412 1
                $tmp[] = '(' . $v[0] . ')';
413 1
                $par = array_merge($par, $v[1]);
414
            }
415 1
            $sql .= implode(' AND ', $tmp).' ';
416
        }
417 3
        if (count($this->group)) {
418
            $sql .= 'GROUP BY ' . $this->group[0] . ' ';
419
            $par = array_merge($par, $this->group[1]);
420
        }
421 3
        if (count($this->having)) {
422
            $sql .= 'HAVING ';
423
            $tmp = [];
424
            foreach ($this->having as $v) {
425
                $tmp[] = '(' . $v[0] . ')';
426
                $par = array_merge($par, $v[1]);
427
            }
428
            $sql .= implode(' AND ', $tmp).' ';
429
        }
430 3
        return $this->db->one($sql, $par);
431
    }
432
    /**
433
     * Specify which columns to fetch (be default all table columns are fetched)
434
     * @param  array $fields optional array of columns to select (related columns can be used too)
435
     * @return $this
436
     */
437 12
    public function columns(array $fields) : TableQuery
438
    {
439 12
        foreach ($fields as $k => $v) {
440 12
            if (strpos($v, '*') !== false) {
441
                $temp = explode('.', $v);
442
                if (count($temp) == 1) {
443
                    $table = $this->definition->getName();
444
                } else {
445
                    $table = $temp[0];
446
                }
447
                $cols = [];
0 ignored issues
show
Unused Code introduced by
$cols 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...
448
                if ($this->definition->hasRelation($table)) {
449
                    $cols = $this->definition->getRelation($table)->table->getColumns();
450
                } else if (isset($this->joins[$table])) {
451
                    $cols = $this->joins[$table]->table->getColumns();
452
                } else {
453
                    throw new DBException('Invalid foreign table name');
454
                }
455
                foreach ($cols as $col) {
456
                    $fields[] = $table . '.' . $col;
457
                }
458 12
                unset($fields[$k]);
459
            }
460
        }
461 12
        $primary = $this->definition->getPrimaryKey();
462 12
        foreach ($fields as $k => $v) {
463
            try {
464 12
                $fields[$k] = $this->getColumn($v)['name'];
465 1
            } catch (DBException $e) {
466 12
                $fields[$k] = $v;
467
            }
468
        }
469 12
        foreach ($primary as $field) {
470 12
            $field = $this->getColumn($field)['name'];
471 12
            if (!in_array($field, $fields)) {
472 12
                $fields[] = $field;
473
            }
474
        }
475 12
        $this->fields = $fields;
476 12
        return $this;
477
    }
478
    /**
479
     * Perform the actual fetch
480
     * @param  array|null $fields optional array of columns to select (related columns can be used too)
481
     * @return TableQueryIterator               the query result as an iterator
482
     */
483 10
    public function iterator(array $fields = null) : TableQueryIterator
484
    {
485 10
        if ($this->qiterator) {
486 4
            return $this->qiterator;
487
        }
488 10
        $table = $this->definition->getName();
489 10
        $primary = $this->definition->getPrimaryKey();
490 10
        if ($fields !== null) {
491 1
            $this->columns($fields);
492
        }
493 10
        $relations = $this->withr;
494 10
        foreach ($this->definition->getRelations() as $k => $v) {
495 10
            foreach ($this->fields as $field) {
496 10
                if (strpos($field, $k . '.') === 0) {
497 10
                    $relations[] = $k;
498
                }
499
            }
500 10
            foreach ($this->where as $v) {
501
                if (strpos($v[0], $k . '.') !== false) {
502
                    $relations[] = $k;
503
                }
504
            }
505 10
            if (isset($this->order[0]) && strpos($this->order[0], $k . '.') !== false) {
506 10
                $relations[] = $k;
507
            }
508
        }
509 10
        $select = [];
510 10
        foreach ($this->fields as $k => $field) {
511 10
            $select[] = $field . (!is_numeric($k) ? ' ' . $k : '');
512
        }
513 10
        foreach ($this->withr as $relation) {
514 3
            foreach ($this->definition->getRelation($relation)->table->getColumns() as $column) {
515 3
                $select[] = $relation . '.' . $column . ' ' . $relation . '___' . $column;
516
            }
517
        }
518 10
        $sql = 'SELECT '.implode(', ', $select).' FROM '.$table.' ';
519 10
        $par = [];
520 10
        foreach ($this->joins as $k => $v) {
521 1
            $sql .= ($v->many ? 'LEFT ' : '' ) . 'JOIN '.$v->table->getName().' '.$k.' ON ';
522 1
            $tmp = [];
523 1
            foreach ($v->keymap as $kk => $vv) {
524 1
                $tmp[] = $kk.' = '.$vv;
525
            }
526 1
            $sql .= implode(' AND ', $tmp) . ' ';
527
        }
528 10
        foreach (array_unique($relations) as $relation) {
529 3
            $v = $this->definition->getRelation($relation);
530 3
            if ($v->pivot) {
531 2
                $sql .= 'LEFT JOIN '.$v->pivot->getName().' '.$relation.'_pivot ON ';
532 2
                $tmp = [];
533 2
                foreach ($v->keymap as $kk => $vv) {
534 2
                    $tmp[] = $table.'.'.$kk.' = '.$relation.'_pivot.'.$vv.' ';
535
                }
536 2
                $sql .= implode(' AND ', $tmp) . ' ';
537 2
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$relation.' ON ';
538 2
                $tmp = [];
539 2
                foreach ($v->pivot_keymap as $kk => $vv) {
540 2
                    $tmp[] = $relation.'.'.$vv.' = '.$relation.'_pivot.'.$kk.' ';
541
                }
542 2
                $sql .= implode(' AND ', $tmp) . ' ';
543
            } else {
544 3
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$relation.' ON ';
545 3
                $tmp = [];
546 3
                foreach ($v->keymap as $kk => $vv) {
547 3
                    $tmp[] = $table.'.'.$kk.' = '.$relation.'.'.$vv.' ';
548
                }
549 3
                if ($v->sql) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $v->sql of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
550
                    $tmp[] = $v->sql . ' ';
551
                    $par = array_merge($par, $v->par ?? []);
552
                }
553 3
                $sql .= implode(' AND ', $tmp) . ' ';
554
            }
555
        }
556 10
        if (count($this->where)) {
557
            $sql .= 'WHERE ';
558
            $tmp = [];
559
            foreach ($this->where as $v) {
560
                $tmp[] = '(' . $v[0] . ')';
561
                $par = array_merge($par, $v[1]);
562
            }
563
            $sql .= implode(' AND ', $tmp).' ';
564
        }
565 10
        if (count($this->group)) {
566 1
            $sql .= 'GROUP BY ' . $this->group[0] . ' ';
567 1
            $par = array_merge($par, $this->group[1]);
568
        }
569 10
        if (count($this->having)) {
570 1
            $sql .= 'HAVING ';
571 1
            $tmp = [];
572 1
            foreach ($this->having as $v) {
573 1
                $tmp[] = '(' . $v[0] . ')';
574 1
                $par = array_merge($par, $v[1]);
575
            }
576 1
            $sql .= implode(' AND ', $tmp).' ';
577
        }
578
        //if ($this->definition->hasRelations()) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% 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...
579
        //    $sql .= 'GROUP BY '.$table.'.'.implode(', '.$table.'.', $primary).' ';
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% 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...
580
        //}
581 10
        if (count($this->order)) {
582 1
            $sql .= 'ORDER BY ' . $this->order[0] . ' ';
583 1
            $par = array_merge($par, $this->order[1]);
584
        }
585 10
        $porder = [];
586 10
        foreach ($primary as $field) {
587 10
            $porder[] = $this->getColumn($field)['name'];
588
        }
589 10
        $sql .= (count($this->order) ? ', ' : 'ORDER BY ') . implode(', ', $porder) . ' ';
590
591 10
        if ($this->li_of[0]) {
592 1
            if ($this->db->driver() === 'oracle') {
593
                if ((int)($this->db->settings()->options['version'] ?? 0) >= 12) {
0 ignored issues
show
Bug introduced by
The method settings() does not seem to exist on object<vakata\database\DBInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
594
                    $sql .= 'OFFSET ' . $this->li_of[1] . ' ROWS FETCH NEXT ' . $this->li_of[0] . ' ROWS ONLY';
595
                } else {
596
                    $f = array_map(function ($v) {
597
                        $v = explode(' ', trim($v), 2);
598
                        if (count($v) === 2) { return $v[1]; }
599
                        $v = explode('.', $v[0], 2);
600
                        return count($v) === 2 ? $v[1] : $v[0];
601
                    }, $select);
602
                    $sql = "SELECT " . implode(', ', $f) . " 
603
                            FROM (
604
                                SELECT tbl__.*, rownum rnum__ FROM (
605
                                    " . $sql . "
606
                                ) tbl__ 
607
                                WHERE rownum <= " . ($this->li_of[0] + $this->li_of[1]) . "
608
                            ) WHERE rnum__ > " . $this->li_of[1];
609
                }
610
            } else {
611 1
                $sql .= 'LIMIT ' . $this->li_of[0] . ' OFFSET ' . $this->li_of[1];
612
            }
613
        }
614 10
        return $this->qiterator = new TableQueryIterator(
615 10
            $this->db->get($sql, $par), 
616 10
            $this->definition->getPrimaryKey(),
617 10
            array_combine(
618 10
                $this->withr,
619
                array_map(function ($relation) {
620 3
                    return $this->definition->getRelation($relation);
621 10
                }, $this->withr)
622
            )
623
        );
624
    }
625
    /**
626
     * Perform the actual fetch
627
     * @param  array|null $fields optional array of columns to select (related columns can be used too)
628
     * @return array               the query result as an array
629
     */
630 1
    public function select(array $fields = null) : array
631
    {
632 1
        return iterator_to_array($this->iterator($fields));
633
    }
634
    /**
635
     * Insert a new row in the table
636
     * @param  array   $data   key value pairs, where each key is the column name and the value is the value to insert
637
     * @return array           the inserted ID where keys are column names and values are column values
638
     */
639 1
    public function insert(array $data) : array
640
    {
641 1
        $table = $this->definition->getName();
642 1
        $columns = $this->definition->getFullColumns();
643 1
        $insert = [];
644 1
        foreach ($data as $column => $value) {
645 1
            if (isset($columns[$column])) {
646 1
                $insert[$column] = $this->normalizeValue($columns[$column], $value);
647
            }
648
        }
649 1
        if (!count($insert)) {
650
            throw new DBException('No valid columns to insert');
651
        }
652 1
        $sql = 'INSERT INTO '.$table.' ('.implode(', ', array_keys($insert)).') VALUES (??)';
653 1
        $par = [$insert];
654
        //if ($update) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
72% 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...
655
        //    $sql .= 'ON DUPLICATE KEY UPDATE ';
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% 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...
656
        //    $sql .= implode(', ', array_map(function ($v) { return $v . ' = ?'; }, array_keys($insert))) . ' ';
0 ignored issues
show
Unused Code Comprehensibility introduced by
53% 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...
657
        //    $par  = array_merge($par, $insert);
0 ignored issues
show
Unused Code Comprehensibility introduced by
54% 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...
658
        //}
659 1
        if ($this->db->driver() === 'oracle') {
660
            $primary = $this->definition->getPrimaryKey();
661
            $ret = [];
662
            foreach ($primary as $k) {
663
                $ret[$k] = str_repeat(' ', 255);
664
                $par[] = &$ret[$k];
665
            }
666
            $sql .= ' RETURNING ' . implode(',', $primary) . ' INTO ' . implode(',', array_fill(0, count($primary), '?'));
667
            $this->db->query($sql, $par);
668
            return $ret;
669
        } else {
670 1
            $ret = [];
671 1
            $ins = $this->db->query($sql, $par)->insertID();
672 1
            foreach ($this->definition->getPrimaryKey() as $k) {
673 1
                $ret[$k] = $data[$k] ?? $ins;
674
            }
675 1
            return $ret;
676
        }
677
    }
678
    /**
679
     * Update the filtered rows with new data
680
     * @param  array  $data key value pairs, where each key is the column name and the value is the value to insert
681
     * @return int          the number of affected rows
682
     */
683 1
    public function update(array $data) : int
684
    {
685 1
        $table = $this->definition->getName();
686 1
        $columns = $this->definition->getFullColumns();
687 1
        $update = [];
688 1
        foreach ($data as $column => $value) {
689 1
            if (isset($columns[$column])) {
690 1
                $update[$column] = $this->normalizeValue($columns[$column], $value);
691
            }
692
        }
693 1
        if (!count($update)) {
694
            throw new DBException('No valid columns to update');
695
        }
696 1
        $sql = 'UPDATE '.$table.' SET ';
697 1
        $par = [];
698
        $sql .= implode(', ', array_map(function ($v) { return $v . ' = ?'; }, array_keys($update))) . ' ';
699 1
        $par = array_merge($par, array_values($update));
700 1
        if (count($this->where)) {
701 1
            $sql .= 'WHERE ';
702 1
            $tmp = [];
703 1
            foreach ($this->where as $v) {
704 1
                $tmp[] = $v[0];
705 1
                $par = array_merge($par, $v[1]);
706
            }
707 1
            $sql .= implode(' AND ', $tmp) . ' ';
708
        }
709 1
        if (count($this->order)) {
710
            $sql .= $this->order[0];
711
            $par = array_merge($par, $this->order[1]);
712
        }
713 1
        return $this->db->query($sql, $par)->affected();
714
    }
715
    /**
716
     * Delete the filtered rows from the DB
717
     * @return int the number of deleted rows
718
     */
719 1
    public function delete() : int
720
    {
721 1
        $table = $this->definition->getName();
722 1
        $sql = 'DELETE FROM '.$table.' ';
723 1
        $par = [];
724 1
        if (count($this->where)) {
725 1
            $sql .= 'WHERE ';
726 1
            $tmp = [];
727 1
            foreach ($this->where as $v) {
728 1
                $tmp[] = $v[0];
729 1
                $par = array_merge($par, $v[1]);
730
            }
731 1
            $sql .= implode(' AND ', $tmp) . ' ';
732
        }
733 1
        if (count($this->order)) {
734
            $sql .= $this->order[0];
735
            $par = array_merge($par, $this->order[1]);
736
        }
737 1
        return $this->db->query($sql, $par)->affected();
738
    }
739
    /**
740
     * Solve the n+1 queries problem by prefetching a relation by name
741
     * @param  string $relation the relation name to fetch along with the data
742
     * @return $this
743
     */
744 3
    public function with(string $relation) : TableQuery
745
    {
746 3
        if (!$this->definition->hasRelation($relation)) {
747
            throw new DBException('Invalid relation name');
748
        }
749 3
        $this->qiterator = null;
750 3
        $this->withr[$relation] = $relation;
751 3
        return $this;
752
    }
753
754 1
    public function getIterator()
755
    {
756 1
        return $this->iterator();
757
    }
758
759 8
    public function offsetGet($offset)
760
    {
761 8
        return $this->iterator()->offsetGet($offset);
762
    }
763
    public function offsetExists($offset)
764
    {
765
        return $this->iterator()->offsetExists($offset);
766
    }
767
    public function offsetUnset($offset)
768
    {
769
        return $this->iterator()->offsetUnset($offset);
770
    }
771
    public function offsetSet($offset, $value)
772
    {
773
        return $this->iterator()->offsetSet($offset, $value);
774
    }
775
776
    public function collection(array $fields = null) : Collection
777
    {
778
        return new Collection($this->iterator($fields));
779
    }
780
}
781