Passed
Pull Request — master (#2)
by Maksim
01:43
created

QueryBuilder::hydrate()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 3
nop 1
dl 0
loc 9
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Tsukasa\QueryBuilder;
4
5
use Doctrine\DBAL\Driver\Connection;
6
use Tsukasa\QueryBuilder\Aggregation\Aggregation;
7
use Tsukasa\QueryBuilder\Exception\QBException;
8
use Tsukasa\QueryBuilder\Expression\Expression;
9
use Tsukasa\QueryBuilder\Interfaces\IAdapter;
10
use Tsukasa\QueryBuilder\Interfaces\ILookupBuilder;
11
use Tsukasa\QueryBuilder\Interfaces\ILookupCollection;
12
use Tsukasa\QueryBuilder\Interfaces\IToSql;
13
use Tsukasa\QueryBuilder\Interfaces\QueryBuilderInterface;
14
use Tsukasa\QueryBuilder\LookupBuilder\LookupBuilder;
15
use Tsukasa\QueryBuilder\Q\Q;
16
17
use Tsukasa\QueryBuilder\Database\Mysql\Adapter as MysqlAdapter;
18
use Tsukasa\QueryBuilder\Database\Sqlite\Adapter as SqliteAdapter;
19
use Tsukasa\QueryBuilder\Database\Pgsql\Adapter as PgsqlAdapter;
20
21
class QueryBuilder implements QueryBuilderInterface
22
{
23
    const TYPE_SELECT = 'SELECT';
24
    const TYPE_INSERT = 'INSERT';
25
    const TYPE_UPDATE = 'UPDATE';
26
    const TYPE_DELETE = 'DELETE';
27
28
    /**
29
     * @var array|Q|string
30
     */
31
    private $_whereAnd = [];
32
    /**
33
     * @var array|Q|string
34
     */
35
    private $_whereOr = [];
36
    /**
37
     * @var array|string
38
     */
39
    private $_join = [];
40
    /**
41
     * @var array|string
42
     */
43
    private $_order = [];
44
    /**
45
     * @var array
46
     */
47
    private $_group = [];
48
    /**
49
     * @var array|string|\Tsukasa\QueryBuilder\Aggregation\Aggregation
50
     */
51
    private $_select = [];
52
    /**
53
     * @var array|string|null
54
     */
55
    private $_from;
56
    /**
57
     * @var array
58
     */
59
    private $_union = [];
60
    /**
61
     * @var null|int
62
     */
63
    private $_limit;
64
    /**
65
     * @var null|int
66
     */
67
    private $_offset;
68
    /**
69
     * @var array
70
     */
71
    private $_having = [];
72
    /**
73
     * @var null|string
74
     */
75
    private $_alias;
76
    /**
77
     * @var null|string sql query type SELECT|UPDATE|DELETE
78
     */
79
    private $_type;
80
    /**
81
     * @var array
82
     */
83
    private $_update = [];
84
    /**
85
     * @var BaseAdapter
86
     */
87
    protected $adapter;
88
    /**
89
     * @var ILookupBuilder
90
     */
91
    protected $lookupBuilder;
92
    /**
93
     * @var null
94
     */
95
    protected $schema;
96
    /**
97
     * Counter of joined tables aliases
98
     * @var int
99
     */
100
    private $_aliasesCount = 0;
101
102
    private $_joinAlias = [];
103
104
    /**
105
     * Strings options query
106
     * @var string
107
     */
108
    private $_queryOptions = '';
109
    /**
110
     * @var Connection
111
     */
112
    protected $connection;
113
114
    public function getConnection()
115
    {
116
        return $this->connection;
117
    }
118
119
    /**
120
     * @param  $connection \Doctrine\DBAL\Connection
121
     */
122
    public function setConnection($connection)
123
    {
124
        $this->connection = $connection;
125
    }
126
127
    /**
128
     * @param Connection $connection
129
     * @param BaseAdapter|null $adapter
130
     * @param LookupBuilder|null $lookupBuilder
131
     * @return QueryBuilderInterface
132
     */
133
    public static function getInstance(Connection $connection, $adapter = null, $lookupBuilder = null)
134
    {
135
        if ($adapter === null) {
136
            $driver = $connection->getDriver();
0 ignored issues
show
Bug introduced by
The method getDriver() does not exist on Doctrine\DBAL\Driver\Connection. It seems like you code against a sub-type of Doctrine\DBAL\Driver\Connection such as Doctrine\DBAL\Connection. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

136
            /** @scrutinizer ignore-call */ 
137
            $driver = $connection->getDriver();
Loading history...
137
            switch ($driver->getName()) {
138
                case 'pdo_mysql':
139
                    $adapter = new MysqlAdapter($connection);
140
                    break;
141
                case 'pdo_sqlite':
142
                    $adapter = new SqliteAdapter($connection);
143
                    break;
144
                case 'pdo_pgsql':
145
                    $adapter = new PgsqlAdapter($connection);
146
                    break;
147
                default:
148
                    throw new QBException('Unknown driver');
149
            }
150
        }
151
152
        $lookupBuilder = $lookupBuilder ?: new LookupBuilder();
153
        $lookupBuilder->addLookupCollection($adapter->getLookupCollection());
0 ignored issues
show
Bug introduced by
It seems like $adapter->getLookupCollection() can also be of type array; however, parameter $lookupCollection of Tsukasa\QueryBuilder\Loo...::addLookupCollection() does only seem to accept Tsukasa\QueryBuilder\Interfaces\ILookupCollection, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

153
        $lookupBuilder->addLookupCollection(/** @scrutinizer ignore-type */ $adapter->getLookupCollection());
Loading history...
154
        return new static($connection, $adapter, $lookupBuilder);
155
    }
156
157
    /**
158
     * QueryBuilder constructor.
159
     * @param Connection $connection
160
     * @param BaseAdapter $adapter
161
     * @param ILookupBuilder $lookupBuilder
162
     */
163
    public function __construct(Connection $connection, BaseAdapter $adapter, ILookupBuilder $lookupBuilder)
164
    {
165
        $this->connection = $connection;
166
        $this->adapter = $adapter;
167
        $this->lookupBuilder = $lookupBuilder;
168
    }
169
170
    /**
171
     * @param ILookupCollection $lookupCollection
172
     * @return QueryBuilderInterface
173
     */
174
    public function addLookupCollection(ILookupCollection $lookupCollection)
175
    {
176
        $this->lookupBuilder->addLookupCollection($lookupCollection);
177
        return $this;
178
    }
179
180
    public function setType($type)
181
    {
182
        $types = [static::TYPE_INSERT, static::TYPE_UPDATE, static::TYPE_DELETE, static::TYPE_SELECT];
183
        if (in_array($type, $types, true)) {
184
            $this->_type = $type;
185
        } else {
186
            throw new QBException('Incorrect type');
187
        }
188
189
190
        return $this;
191
    }
192
193
    /**
194
     * @return QueryBuilderInterface
195
     */
196
    public function setTypeSelect()
197
    {
198
        $this->_type = self::TYPE_SELECT;
199
        return $this;
200
    }
201
202
    /**
203
     * @return QueryBuilderInterface
204
     */
205
    public function setTypeInsert()
206
    {
207
        $this->_type = self::TYPE_INSERT;
208
        return $this;
209
    }
210
211
    /**
212
     * @return QueryBuilderInterface
213
     */
214
    public function setTypeUpdate()
215
    {
216
        $this->_type = self::TYPE_UPDATE;
217
        return $this;
218
    }
219
220
    /**
221
     * @return QueryBuilderInterface
222
     */
223
    public function setTypeDelete()
224
    {
225
        $this->_type = self::TYPE_DELETE;
226
        return $this;
227
    }
228
229
    /**
230
     * If type is null return TYPE_SELECT
231
     * @return string
232
     */
233
    public function getType()
234
    {
235
        return $this->_type === null
236
            ? self::TYPE_SELECT
237
            : $this->_type;
238
    }
239
240
    public function setOptions($options = '')
241
    {
242
        $this->_queryOptions = $options;
243
        return $this;
244
    }
245
246
    /**
247
     * @param Aggregation $aggregation
248
     * @return string
249
     */
250
    protected function buildSelectFromAggregation(Aggregation $aggregation)
251
    {
252
        $tableAlias = $this->getAlias();
253
        $rawColumn = $aggregation->getField();
254
        $newSelect = $this->getLookupBuilder()->buildJoin($this, $rawColumn);
0 ignored issues
show
Bug introduced by
The method buildJoin() does not exist on Tsukasa\QueryBuilder\Interfaces\ILookupBuilder. It seems like you code against a sub-type of Tsukasa\QueryBuilder\Interfaces\ILookupBuilder such as Tsukasa\QueryBuilder\LookupBuilder\LookupBuilder. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

254
        $newSelect = $this->getLookupBuilder()->/** @scrutinizer ignore-call */ buildJoin($this, $rawColumn);
Loading history...
255
        if ($newSelect === false) {
256
            if ($tableAlias === null || $rawColumn === '*') {
257
                $columns = $rawColumn;
258
            } elseif (strpos($rawColumn, '.') !== false) {
259
                $columns = $rawColumn;
260
            } else {
261
                $columns = $tableAlias . '.' . $rawColumn;
262
            }
263
        } else {
264
            list($alias, $joinColumn) = $newSelect;
265
            $columns = $alias . '.' . $joinColumn;
266
        }
267
        $fieldsSql = $this->getAdapter()->buildColumns($columns);
268
        $aggregation->setFieldSql($fieldsSql);
269
270
        return $aggregation->setQB($this)->toSQL();
271
    }
272
273
    /**
274
     * @return string
275
     * @throws \Exception
276
     */
277
    protected function buildSelect()
278
    {
279
        if (empty($this->_select)) {
280
            $this->_select = ['*'];
281
        }
282
283
        $select = [];
284
        $builder = $this->getLookupBuilder();
285
        if (is_array($this->_select)) {
286
            foreach ($this->_select as $alias => $column) {
287
                if ($column instanceof Aggregation) {
288
                    $select[$alias] = $this->buildSelectFromAggregation($column);
289
                } else if (is_string($column)) {
290
                    if (strpos($column, 'SELECT') !== false) {
291
                        $select[$alias] = $column;
292
                    } else {
293
                        $select[$alias] = $this->addColumnAlias($builder->fetchColumnName($column));
0 ignored issues
show
Bug introduced by
The method fetchColumnName() does not exist on Tsukasa\QueryBuilder\Interfaces\ILookupBuilder. Since it exists in all sub-types, consider adding an abstract or default implementation to Tsukasa\QueryBuilder\Interfaces\ILookupBuilder. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

293
                        $select[$alias] = $this->addColumnAlias($builder->/** @scrutinizer ignore-call */ fetchColumnName($column));
Loading history...
294
                    }
295
                } else {
296
                    $select[$alias] = $column;
297
                }
298
            }
299
        } else if (is_string($this->_select)) {
300
            $select = $this->addColumnAlias($this->_select);
301
        }
302
        return $this->getAdapter()->sqlSelect($select, $this->_queryOptions);
303
    }
304
305
    protected function pushToSelect($select, $alias = null)
306
    {
307
        $isValid = is_string($select)
308
            || (is_numeric($select) && is_finite($select))
309
            || is_a($select, Expression::class)
310
            || is_a($select, Aggregation::class)
311
        ;
312
313
        if (!$isValid) {
314
            throw new QBException('Incorrect select type');
315
        }
316
317
        if ($alias) {
318
            $this->_select[$alias] = $select;
319
        } else {
320
            $this->_select[] = $select;
321
        }
322
323
        return $this;
324
    }
325
326
    /**
327
     * @param string|IToSql $select
328
     * @param null $alias
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $alias is correct as it would always require null to be passed?
Loading history...
329
     * @return QueryBuilderInterface
330
     */
331
    public function addSelect($select, $alias = null)
332
    {
333
        if (is_string($select) && $newSelect = $this->getLookupBuilder()->buildJoin($this, $select)) {
334
            list($t_alias, $column) = $newSelect;
335
            $this->pushToSelect($t_alias . '.' . $column, $alias);
336
        } else {
337
            $this->pushToSelect(
338
                $this->hydrate($select),
339
                $alias
340
            );
341
        }
342
343
        return $this;
344
    }
345
346
    /**
347
     * @param array|string $select
348
     * @return QueryBuilderInterface
349
     */
350
    public function setSelect($select)
351
    {
352
        $this->_select = [];
353
354
        if (empty($select)) {
355
            return $this;
356
        }
357
358
        if (is_array($select)) {
359
            foreach ($select as $key => $part) {
360
                $this->addSelect($part, $key);
361
            }
362
        } else {
363
            $this->addSelect($select);
364
        }
365
366
        return $this;
367
    }
368
369
    /**
370
     * @param array|string $tableName
371
     * @param null|string $alias
372
     * @return QueryBuilderInterface
373
     */
374
    public function setFrom($tableName, $alias = null)
375
    {
376
        if ($alias && is_string($alias)) {
377
            if (is_array($tableName)) {
378
                $tableName = current($tableName);
379
            }
380
381
            $tableName = [$alias => $tableName];
382
        }
383
384
        $this->_from = $tableName;
385
        return $this;
386
    }
387
388
    /**
389
     * @param $alias string join alias
390
     * @return bool
391
     */
392
    public function hasJoin($alias)
393
    {
394
        return array_key_exists($alias, $this->_join);
0 ignored issues
show
Bug introduced by
It seems like $this->_join can also be of type string; however, parameter $search of array_key_exists() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

394
        return array_key_exists($alias, /** @scrutinizer ignore-type */ $this->_join);
Loading history...
395
    }
396
397
    /**
398
     * @param int $page
399
     * @param int $pageSize
400
     * @return QueryBuilderInterface
401
     */
402
    public function paginate($page = 1, $pageSize = 10)
403
    {
404
        $page = (int)$page;
405
        $pageSize = (int)$pageSize;
406
407
        $this->setLimit($pageSize);
408
        $this->setOffset($page > 1 ? $pageSize * ($page - 1) : 0);
409
        return $this;
410
    }
411
412
    /**
413
     * @param string|number $limit
414
     * @return QueryBuilderInterface
415
     */
416
    public function setLimit($limit)
417
    {
418
        $this->_limit = (int)$limit;
419
        return $this;
420
    }
421
422
    /**
423
     * @return int
424
     */
425
    public function getLimit()
426
    {
427
        return $this->_limit;
428
    }
429
430
    /**
431
     * @param string|number $offset
432
     * @return QueryBuilderInterface
433
     */
434
    public function setOffset($offset)
435
    {
436
        $this->_offset = (int)$offset;
437
        return $this;
438
    }
439
440
    /**
441
     * @return int|string|null
442
     */
443
    public function getOffset()
444
    {
445
        return $this->_offset;
446
    }
447
448
    /**
449
     * @return ILookupBuilder|\Tsukasa\QueryBuilder\LookupBuilder\LookupBuilder
450
     */
451
    public function getLookupBuilder()
452
    {
453
        return $this->lookupBuilder;
454
    }
455
456
    /**
457
     * @return BaseAdapter|IAdapter
458
     */
459
    public function getAdapter()
460
    {
461
        return $this->adapter;
462
    }
463
464
    /**
465
     * @param string $joinType LEFT JOIN, RIGHT JOIN, etc...
466
     * @param string|QueryBuilderInterface $tableName
467
     * @param array $on link columns
468
     * @param string|null $alias string
469
     * @param string|null $index
470
     * @return QueryBuilderInterface
471
     */
472
    public function join($joinType, $tableName, array $on = [], $alias = null, $index = null)
473
    {
474
        if ($tableName instanceof QueryBuilderInterface) {
475
            $this->_join[] = $this->getAdapter()->sqlJoin($joinType, $tableName, $on, $alias, $index);
476
        } else {
477
            if ($joinType === 'RAW' && !empty($tableName)) {
478
                $join = $this->getAdapter()->quoteSql($tableName);
479
            } else {
480
                $join = $this->getAdapter()->sqlJoin($joinType, $tableName, $on, $alias);
481
            }
482
483
            if (!$alias) {
484
                $alias = count($this->_join);
0 ignored issues
show
Bug introduced by
It seems like $this->_join can also be of type string; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

484
                $alias = count(/** @scrutinizer ignore-type */ $this->_join);
Loading history...
485
            }
486
            $this->_join[$alias] = $join;
487
            $this->_joinAlias[$tableName][] = $alias;
488
        }
489
        return $this;
490
    }
491
492
    /**
493
     * @param $sql
494
     * @param string $alias
495
     * @return QueryBuilderInterface
496
     */
497
    public function joinRaw($sql, $alias = null)
498
    {
499
        return $this->join('RAW', $sql, [], $alias);
500
    }
501
502
    /**
503
     * @param array|string $columns columns
504
     * @return QueryBuilderInterface
505
     */
506
    public function setGroup($columns)
507
    {
508
        if ($columns && is_string($columns)) {
509
            $columns = array_map('trim', explode(',', $columns));
510
        }
511
512
        $this->_group = $columns;
0 ignored issues
show
Documentation Bug introduced by
It seems like $columns can also be of type string. However, the property $_group is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
513
        return $this;
514
    }
515
516
    /**
517
     * @param array|string $columns columns
518
     * @return QueryBuilderInterface
519
     */
520
    public function addGroup($columns)
521
    {
522
        if ($columns && is_string($columns)) {
523
            $columns = array_map('trim', explode(',', $columns));
524
        }
525
        $this->_group = array_merge($this->_group, $columns);
0 ignored issues
show
Bug introduced by
It seems like $columns can also be of type string; however, parameter $array2 of array_merge() does only seem to accept array|null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

525
        $this->_group = array_merge($this->_group, /** @scrutinizer ignore-type */ $columns);
Loading history...
526
        return $this;
527
    }
528
529
    protected function pushOrder($column)
530
    {
531
532
    }
533
534
    /**
535
     * @param array|string|null $columns columns
536
     * @return QueryBuilderInterface
537
     */
538
    public function setOrder($columns)
539
    {
540
541
        $this->_order = [];
542
543
        if (empty($columns)) {
544
            return $this;
545
        }
546
547
        if (is_array($columns)) {
548
            foreach ($columns as $column) {
549
                $this->addOrder($column);
550
            }
551
        } else {
552
            $this->addOrder($columns);
553
        }
554
555
        return $this;
556
    }
557
558
    /**
559
     * @param string|Expression $column
560
     * @return QueryBuilderInterface
561
     */
562
    public function addOrder($column)
563
    {
564
        $isValid = is_string($column)
565
            || is_a($column, Expression::class)
566
        ;
567
568
        if (!$isValid) {
569
            throw new QBException('Incorrect order type');
570
        }
571
572
        if (is_string($column) && strpos($column, ',') !== false) {
573
            $columns = preg_split('/\s*,\s*/', $column, -1, PREG_SPLIT_NO_EMPTY);
574
575
            foreach ($columns as $_column) {
576
                $temp = explode(' ', $_column);
577
                if (count($temp) === 2) {
578
579
                    $_column = $temp[0];
580
                    if (strtoupper(trim($temp[1])) === 'DESC') {
581
                        $_column = '-' . $_column;
582
                    }
583
584
                    $this->_order[] = $_column;
585
                } else {
586
                    $this->_order[] = current($temp);
587
                }
588
            }
589
        } else {
590
            $this->_order[] = $column;
591
        }
592
593
        return $this;
594
    }
595
596
    /**
597
     * @param $tableName
598
     * @param array $rows
599
     * @return QueryBuilderInterface
600
     */
601
    public function insert($tableName, $rows)
602
    {
603
        $this->setTypeInsert();
604
        $this->_update = [$tableName, $rows];
605
        return $this;
606
    }
607
608
    /**
609
     * @param $tableName string
610
     * @param array $values columns [name => value...]
611
     * @return QueryBuilderInterface
612
     */
613
    public function update($tableName, array $values)
614
    {
615
        $this->setTypeUpdate();
616
        $this->_update = [$tableName, $values];
617
        return $this;
618
    }
619
620
    public function getAlias()
621
    {
622
        return $this->_alias;
623
    }
624
625
    public function setAlias($alias = null)
626
    {
627
        if (empty($alias)) {
628
            $alias = null;
629
        }
630
631
        $this->_alias = $alias;
632
        return $this;
633
    }
634
635
    protected function buildCondition($condition, &$params = [])
636
    {
637
        if (!is_array($condition)) {
638
            return (string)$condition;
639
        }
640
641
        if (empty($condition)) {
642
            return '';
643
        }
644
645
        if (isset($condition[0]) && is_string($condition[0])) {
646
            $operatorRaw = array_shift($condition);
647
            $operator = strtoupper($operatorRaw);
648
            return $this->buildAndCondition($operator, $condition, $params);
649
        }
650
651
        return $this->parseCondition($condition);
652
    }
653
654
    public function getJoinAlias($tableName)
655
    {
656
        return $this->_joinAlias[$tableName];
657
    }
658
659
    public function getJoins()
660
    {
661
        return $this->_join;
662
    }
663
664
    /**
665
     * @param $condition
666
     * @param string $operator
667
     * @return string
668
     */
669
    public function parseCondition($condition, $operator = 'AND')
670
    {
671
        $parts = [];
672
673
        if (is_array($condition)) {
674
            foreach ($condition as $key => $value) {
675
                if (is_numeric($key)) {
676
                    if ($value instanceof IToSql) {
677
                        $parts[] = $this->parseCondition($value, $operator);
678
                    } elseif ($value instanceof QueryBuilder) {
679
                        $parts[] = $this->parseCondition($value, $operator);
680
                    } else if (is_array($value)) {
681
                        $parts[] = $this->parseCondition($value, $operator);
682
                    } else if (is_string($value)) {
683
                        $parts[] = $value;
684
                    }
685
                } else {
686
                    $tableAlias = $this->getAlias();
687
                    $value = $this->getAdapter()->prepareValue($value);
688
689
                    list($lookup, $column, $lookupValue) = $this->lookupBuilder->parseLookup($this, $key, $value);
690
                    $column = $this->getLookupBuilder()->fetchColumnName($column);
691
                    if ($tableAlias !== null && strpos($column, '.') === false) {
692
                        $column = $tableAlias . '.' . $column;
693
                    }
694
                    $parts[] = $this->lookupBuilder->runLookup($this->getAdapter(), $lookup, $column, $lookupValue);
695
                }
696
            }
697
698
            if ($parts) {
699
                if (count($parts) === 1) {
700
                    return $parts[0];
701
                }
702
703
                return '(' . implode(') ' . $operator . ' (', $parts) . ')';
704
            }
705
706
        } else if ($condition instanceof IToSql) {
707
            return $condition
708
                ->setQb($this)
709
                ->toSql();
710
        } else if ($condition instanceof QueryBuilder) {
711
            return $condition->toSQL();
712
        } else if (is_string($condition)) {
713
            return $condition;
714
        }
715
716
        return '';
717
    }
718
719
    protected function buildAndCondition($operator, $operands, &$params)
720
    {
721
        $parts = [];
722
        foreach ($operands as $operand) {
723
            if (is_array($operand)) {
724
                $operand = $this->buildCondition($operand, $params);
725
            } else {
726
                $operand = $this->parseCondition($operand);
727
            }
728
            if ($operand !== '') {
729
                $parts[] = $this->getAdapter()->quoteSql($operand);
730
            }
731
        }
732
        if (!empty($parts)) {
733
            return '(' . implode(') ' . $operator . ' (', $parts) . ')';
734
        }
735
736
        return '';
737
    }
738
739
    /**
740
     * @param $condition
741
     * @return QueryBuilderInterface
742
     */
743
    public function addWhere($condition)
744
    {
745
        if (!empty($condition)) {
746
            $this->_whereAnd[] = $this->hydrate($condition);
747
        }
748
        return $this;
749
    }
750
751
    public function setWhere($condition)
752
    {
753
        $this->_whereAnd = [];
754
755
        return $this->addWhere($condition);
756
    }
757
758
    /**
759
     * @param $condition
760
     * @return QueryBuilderInterface
761
     */
762
    public function addOrWhere($condition)
763
    {
764
        if (!empty($condition)) {
765
            $this->_whereOr[] = $this->hydrate($condition);
766
        }
767
        return $this;
768
    }
769
770
    public function setOrWhere($condition)
771
    {
772
        $this->_whereOr = [];
773
774
        return $this->addWhere($condition);
775
    }
776
777
    /**
778
     * @return array
779
     */
780
    protected function buildWhereTree()
781
    {
782
        $where = [];
783
        foreach ($this->_whereAnd as $condition) {
784
            if (empty($where)) {
785
                $where = ['and', $condition];
786
            } else {
787
                $where = ['and', $where, ['and', $condition]];
788
            }
789
        }
790
791
        foreach ($this->_whereOr as $condition) {
792
            if (empty($where)) {
793
                $where = ['or', $condition];
794
            } else {
795
                $where = ['or', $where, ['and', $condition]];
796
            }
797
        }
798
        return $where;
799
    }
800
801
    public function getSelect()
802
    {
803
        return $this->_select;
804
    }
805
806
    protected function buildWhere()
807
    {
808
        $params = [];
809
        $sql = $this->buildCondition($this->buildWhereTree(), $params);
810
        return empty($sql) ? '' : ' WHERE ' . $sql;
811
    }
812
813
814
    protected function generateSelectSql()
815
    {
816
        // Fetch where conditions before pass it to adapter.
817
        // Reason: Dynamic sql build in callbacks
818
819
        // $this->prepareJoin();
820
821
        $where = $this->buildWhere();
822
        $order = $this->buildOrder();
823
        $union = $this->buildUnion();
824
825
        $select = $this->buildSelect();
826
        $from = $this->buildFrom();
827
        $join = $this->buildJoin();
828
        $group = $this->buildGroup();
829
        $having = $this->buildHaving();
830
        $limitOffset = $this->buildLimitOffset();
831
        return strtr('{select}{from}{join}{where}{group}{having}{order}{limit_offset}{union}', [
832
            '{select}' => $select,
833
            '{from}' => $from,
834
            '{where}' => $where,
835
            '{group}' => $group,
836
            '{order}' => empty($union) ? $order : '',
837
            '{having}' => $having,
838
            '{join}' => $join,
839
            '{limit_offset}' => $limitOffset,
840
            '{union}' => empty($union) ? '' : $union . $order
841
        ]);
842
    }
843
844
    public function generateDeleteSql()
845
    {
846
        $options = $this->_queryOptions ;
847
        if ($options) {
848
            $options = " {$options} ";
849
        }
850
851
        $limitOffset = $this->buildLimitOffset();
852
        return strtr('{delete}{options}{from}{where}{limit_offset}', [
853
            '{delete}' => 'DELETE',
854
            '{options}' => $options,
855
            '{from}' => $this->buildFrom(),
856
            '{where}' => $this->buildWhere(),
857
            '{limit_offset}' => $limitOffset,
858
        ]);
859
    }
860
861
    public function generateInsertSql()
862
    {
863
        list($tableName, $values) = $this->_update;
864
        $this->setAlias();
865
        return $this->getAdapter()->sqlInsert($tableName, $values, $this->_queryOptions);
866
    }
867
868
    public function generateUpdateSql()
869
    {
870
        list($tableName, $values) = $this->_update;
871
        $this->setAlias();
872
        return strtr('{update}{where}', [
873
            '{update}' => $this->getAdapter()->sqlUpdate($tableName, $values, $this->_queryOptions),
874
            '{where}' => $this->buildWhere(),
875
        ]);
876
    }
877
878
    /**
879
     * @return string
880
     * @throws QBException
881
     */
882
    public function toSQL()
883
    {
884
        switch ($this->getType())
885
        {
886
            case self::TYPE_SELECT:
887
                return $this->generateSelectSql();
888
889
            case self::TYPE_INSERT:
890
                return $this->generateInsertSql();
891
892
            case self::TYPE_UPDATE:
893
                return $this->generateUpdateSql();
894
895
            case self::TYPE_DELETE:
896
                return $this->generateDeleteSql();
897
        }
898
899
        throw new QBException('Unknown query type');
900
    }
901
902
    protected function buildHaving()
903
    {
904
        return $this->getAdapter()->sqlHaving(
905
            $this->parseCondition($this->_having),
906
            $this
0 ignored issues
show
Unused Code introduced by
The call to Tsukasa\QueryBuilder\BaseAdapter::sqlHaving() has too many arguments starting with $this. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

906
        return $this->getAdapter()->/** @scrutinizer ignore-call */ sqlHaving(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
907
        );
908
    }
909
910
    protected function buildLimitOffset()
911
    {
912
        return $this->getAdapter()->sqlLimitOffset(
913
            $this->_limit,
914
            $this->_offset
915
        );
916
    }
917
918
    protected function buildUnion()
919
    {
920
        $sql = '';
921
        foreach ($this->_union as  list($union, $all)) {
922
            $sql .= ' ' . $this->getAdapter()->sqlUnion($union, $all);
923
        }
924
925
        return empty($sql) ? '' : $sql;
926
    }
927
928
    public function getSchema()
929
    {
930
        return $this->schema;
931
    }
932
933
    /**
934
     * @param $tableName
935
     * @param $columns
936
     * @param null $options
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $options is correct as it would always require null to be passed?
Loading history...
937
     * @param bool $ifNotExists
938
     * @return string
939
     */
940
    public function createTable($tableName, $columns, $options = null, $ifNotExists = false)
941
    {
942
        return $this->getAdapter()->sqlCreateTable(
943
            $tableName,
944
            $columns,
945
            $options,
946
            $ifNotExists
947
        );
948
    }
949
950
    /**
951
     * @param array|string|Q $having lookups
952
     * @return QueryBuilderInterface
953
     */
954
    public function setHaving($having)
955
    {
956
        $this->_having = [];
957
958
        return $this->addHaving($having);
959
    }
960
961
    public function addHaving($having)
962
    {
963
        if (!empty($having)) {
964
            $this->_having[] = $this->hydrate($having);
965
        }
966
967
        return $this;
968
    }
969
970
    public function addUnion($union, $all = false)
971
    {
972
        $this->_union[] = [$union, $all];
973
        return $this;
974
    }
975
976
    /**
977
     * @param $tableName
978
     * @param $name
979
     * @param $columns
980
     * @return string
981
     */
982
    public function addPrimaryKey($tableName, $name, $columns)
983
    {
984
        return $this->getAdapter()->sqlAddPrimaryKey($tableName, $name, $columns);
985
    }
986
987
    /**
988
     * @param $tableName
989
     * @param $column
990
     * @param $type
991
     * @return string
992
     */
993
    public function alterColumn($tableName, $column, $type)
994
    {
995
        return $this->getAdapter()->sqlAlterColumn($tableName, $column, $type);
996
    }
997
998
    /**
999
     * Makes alias for joined table
1000
     * @param $table
1001
     * @param bool $increment
1002
     * @return string
1003
     */
1004
    public function makeAliasKey($table, $increment = false)
1005
    {
1006
        if ($increment) {
1007
            ++$this->_aliasesCount;
1008
        }
1009
1010
        $tableName = $this->getAdapter()->getRawTableName($table);
1011
1012
        if (strpos($tableName, '.') !== false) {
1013
            $tableName = substr($tableName, strpos($tableName, '.')+1);
1014
        }
1015
1016
        return strtr('{table}_{count}', [
1017
            '{table}' => $tableName,
1018
            '{count}' => $this->_aliasesCount + 1
1019
        ]);
1020
    }
1021
1022
    /**
1023
     * @param string $table
1024
     * @param string $code
1025
     * @param string $topAlias
1026
     *
1027
     * @return string
1028
     */
1029
    public function makeMappedAliasKey($table, $code, $topAlias = null)
1030
    {
1031
        $key = $topAlias . '_' . $code;
1032
1033
        if (empty($this->_joinAlias[$table])) {
1034
            $this->_joinAlias[$table]['__alias_count__'] = 1;
1035
        }
1036
1037
        if (!empty($this->_joinAlias[$table][$key])) {
1038
            return $this->_joinAlias[$table][$key];
1039
        }
1040
1041
        $this->_joinAlias[$table][$key] = strtr('{table}_{count}', [
1042
            '{table}' => $this->getAdapter()->getRawTableName($table),
1043
            '{count}' => $this->_joinAlias[$table]['__alias_count__'] += 1
1044
        ]);
1045
1046
        return $this->_joinAlias[$table][$key];
1047
    }
1048
1049
    public function getJoin($tableName)
1050
    {
1051
        return $this->_join[$tableName];
1052
    }
1053
1054
    /**
1055
     * @param $column
1056
     * @return string
1057
     */
1058
    protected function addColumnAlias($column)
1059
    {
1060
        $tableAlias = $this->getAlias();
1061
        if ($tableAlias === null) {
1062
            return $column;
1063
        }
1064
1065
        if (strpos($column, '.') === false &&
1066
            strpos($column, '(') === false &&
1067
            strpos($column, 'SELECT') === false
1068
        ) {
1069
            return $tableAlias . '.' . $column;
1070
        }
1071
1072
        return $column;
1073
    }
1074
1075
    protected function hasAliasedField($column)
1076
    {
1077
        foreach ($this->_select as $alias => $item)
1078
        {
1079
            if (!is_numeric($alias) && $column === $alias) {
1080
                return true;
1081
            }
1082
        }
1083
1084
        return false;
1085
    }
1086
1087
    protected function applyTableAlias($column)
1088
    {
1089
        // If column already has alias - skip
1090
        if ((strpos($column, '.') === false) && !$this->hasAliasedField($column)) {
1091
            $tableAlias = $this->getAlias();
1092
            return $tableAlias === null ? $column : $tableAlias . '.' . $column;
1093
        }
1094
1095
        return $column;
1096
    }
1097
1098
    protected function buildJoin()
1099
    {
1100
        if (empty($this->_join)) {
1101
            return '';
1102
        }
1103
        $join = [];
1104
        foreach ($this->_join as $part) {
1105
            $join[] = $part;
1106
        }
1107
        return ' ' . implode(' ', $join);
1108
    }
1109
1110
1111
    /**
1112
     * @param $order
1113
     * @return array
1114
     */
1115
    protected function buildOrderJoin($order)
1116
    {
1117
        if (strpos($order, '-') === false) {
1118
            $direction = 'ASC';
1119
        } else {
1120
            $direction = 'DESC';
1121
            $order = substr($order, 1);
1122
        }
1123
        $order = $this->getLookupBuilder()->fetchColumnName($order);
1124
        $newOrder = $this->getLookupBuilder()->buildJoin($this, $order);
1125
        if ($newOrder === false) {
1126
            return [$order, $direction];
1127
        }
1128
1129
        list($alias, $column) = $newOrder;
1130
        return [$alias . '.' . $column, $direction];
1131
    }
1132
1133
    public function getOrder()
1134
    {
1135
        return $this->_order;
1136
    }
1137
1138
    protected function buildOrder()
1139
    {
1140
        /**
1141
         * не делать проверку по empty(), проваливается половина тестов с ORDER BY
1142
         * и проваливается тест с построением JOIN по lookup
1143
         */
1144
        if ($this->_order === null) {
1145
            return '';
1146
        }
1147
1148
        $order = [];
1149
        if (is_array($this->_order)) {
1150
            foreach ($this->_order as $column) {
1151
                if ($column instanceof Expression) {
1152
                    $order[$column->toSQL()] = '';
1153
                }
1154
                else if ($column === '?') {
1155
                    $order[] = $this->getAdapter()->getRandomOrder();
1156
                } else {
1157
                    list($newColumn, $direction) = $this->buildOrderJoin($column);
1158
                    $order[$this->applyTableAlias($newColumn)] = $direction;
1159
                }
1160
            }
1161
        } else {
1162
            $order[] = $this->buildOrderJoin($this->_order);
1163
        }
1164
1165
        $sql = $this->getAdapter()->sqlOrderBy($order);
1166
        return empty($sql) ? '' : ' ORDER BY ' . $sql;
1167
    }
1168
1169
    /**
1170
     * @param $group
1171
     * @return string
1172
     */
1173
    protected function buildGroupJoin($group)
1174
    {
1175
        if (strpos($group, '.') === false) {
1176
            $newGroup = $this->getLookupBuilder()->fetchColumnName($group);
1177
            $newGroup = $this->getLookupBuilder()->buildJoin($this, $newGroup);
1178
1179
            if ($newGroup === false) {
1180
                return $group;
1181
            }
1182
1183
            list($alias, $column) = $newGroup;
1184
            return $alias . '.' . $column;
1185
        }
1186
1187
        return $group;
1188
    }
1189
1190
    protected function buildGroup()
1191
    {
1192
        $group = [];
1193
        if ($this->_group) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->_group of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1194
            foreach ($this->_group as $key => $column) {
1195
                $newColumn = $this->buildGroupJoin($column);
1196
                $group[] = $this->applyTableAlias($newColumn);
1197
            }
1198
        }
1199
1200
        $sql = $this->getAdapter()->sqlGroupBy($group);
1201
        return empty($sql) ? '' : ' GROUP BY ' . $sql;
1202
    }
1203
1204
    protected function buildFrom()
1205
    {
1206
        if ($this->_alias !== null && !is_array($this->_from)) {
1207
            $from = [$this->_alias => $this->_from];
1208
        } else {
1209
            $from = $this->_from;
1210
        }
1211
        $sql = $this->getAdapter()->sqlFrom($from);
1212
        return empty($sql) ? '' : ' FROM ' . $sql;
1213
    }
1214
1215
    protected function hydrate($val)
1216
    {
1217
        if (is_object($val)) {
1218
            if ($val instanceof IToSql) {
1219
                $val->setQb($this);
1220
            }
1221
        }
1222
1223
        return $val;
1224
    }
1225
}
1226