Completed
Push — master ( ce4a45...4f05e7 )
by Ivan
04:08
created

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