Completed
Push — master ( 850e70...ce4a45 )
by Ivan
03:43
created

TableQuery::offsetSet()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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