Completed
Push — master ( 04b8fa...5e586a )
by Maksim
16s queued 11s
created

QueryBuilder::setUnions()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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

137
            /** @scrutinizer ignore-call */ 
138
            $driver = $connection->getDriver();
Loading history...
138
            switch ($driver->getName()) {
139
                case 'pdo_mysql':
140
                    $adapter = new MysqlAdapter($connection);
141
                    break;
142
                case 'pdo_sqlite':
143
                    $adapter = new SqliteAdapter($connection);
144
                    break;
145
                case 'pdo_pgsql':
146
                    $adapter = new PgsqlAdapter($connection);
147
                    break;
148
                default:
149
                    throw new QBException('Unknown driver');
150
            }
151
        }
152
153
        $lookupBuilder = $lookupBuilder ?: new LookupBuilder();
154
        $lookupBuilder->addLookupCollection($adapter->getLookupCollection());
155
        return new static($connection, $adapter, $lookupBuilder);
156
    }
157
158
    /**
159
     * QueryBuilder constructor.
160
     * @param Connection $connection
161
     * @param ISQLGenerator $adapter
162
     * @param ILookupBuilder $lookupBuilder
163
     */
164
    public function __construct(Connection $connection, ISQLGenerator $adapter, ILookupBuilder $lookupBuilder)
165
    {
166
        $this->connection = $connection;
167
        $this->adapter = $adapter;
168
        $this->lookupBuilder = $lookupBuilder;
169
    }
170
171
    /**
172
     * @param ILookupCollection $lookupCollection
173
     * @return QueryBuilderInterface
174
     */
175
    public function addLookupCollection(ILookupCollection $lookupCollection)
176
    {
177
        $this->lookupBuilder->addLookupCollection($lookupCollection);
178
        return $this;
179
    }
180
181
    public function setType($type)
182
    {
183
        $types = [static::TYPE_INSERT, static::TYPE_UPDATE, static::TYPE_DELETE, static::TYPE_SELECT];
184
        if (in_array($type, $types, true)) {
185
            $this->_type = $type;
186
        }
187
        else {
188
            throw new QBException('Incorrect type');
189
        }
190
191
192
        return $this;
193
    }
194
195
    /**
196
     * @return QueryBuilderInterface
197
     */
198
    public function setTypeSelect()
199
    {
200
        $this->_type = self::TYPE_SELECT;
201
        return $this;
202
    }
203
204
    /**
205
     * @return QueryBuilderInterface
206
     */
207
    public function setTypeInsert()
208
    {
209
        $this->_type = self::TYPE_INSERT;
210
        return $this;
211
    }
212
213
    /**
214
     * @return QueryBuilderInterface
215
     */
216
    public function setTypeUpdate()
217
    {
218
        $this->_type = self::TYPE_UPDATE;
219
        return $this;
220
    }
221
222
    /**
223
     * @return QueryBuilderInterface
224
     */
225
    public function setTypeDelete()
226
    {
227
        $this->_type = self::TYPE_DELETE;
228
        return $this;
229
    }
230
231
    /**
232
     * If type is null return TYPE_SELECT
233
     * @return string
234
     */
235
    public function getType()
236
    {
237
        return $this->_type === null
238
            ? self::TYPE_SELECT
239
            : $this->_type;
240
    }
241
242
    public function setOptions($options = '')
243
    {
244
        $this->_queryOptions = $options;
245
        return $this;
246
    }
247
248
    /**
249
     * @param Aggregation $aggregation
250
     * @return string
251
     */
252
    protected function buildSelectFromAggregation(Aggregation $aggregation)
253
    {
254
        $tableAlias = $this->getAlias();
255
        $rawColumn = $aggregation->getField();
256
        $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

256
        $newSelect = $this->getLookupBuilder()->/** @scrutinizer ignore-call */ buildJoin($this, $rawColumn);
Loading history...
257
        if ($newSelect === false) {
258
            if ($tableAlias === null || $rawColumn === '*') {
259
                $columns = $rawColumn;
260
            }
261
            elseif (strpos($rawColumn, '.') !== false) {
262
                $columns = $rawColumn;
263
            }
264
            else {
265
                $columns = $tableAlias . '.' . $rawColumn;
266
            }
267
        }
268
        else {
269
            list($alias, $joinColumn) = $newSelect;
270
            $columns = $alias . '.' . $joinColumn;
271
        }
272
        $fieldsSql = $this->getAdapter()->buildColumns($columns);
273
        $aggregation->setFieldSql($fieldsSql);
274
275
        return $aggregation->setQB($this)->toSQL();
276
    }
277
278
    /**
279
     * @return string
280
     * @throws \Exception
281
     */
282
    protected function buildSelect()
283
    {
284
        if (empty($this->_select)) {
285
            $this->_select = ['*'];
286
        }
287
288
        $select = [];
289
        $builder = $this->getLookupBuilder();
290
        if (is_array($this->_select)) {
291
            foreach ($this->_select as $alias => $column) {
292
                if ($column instanceof Aggregation) {
293
                    $select[$alias] = $this->buildSelectFromAggregation($column);
294
                }
295
                else if (is_string($column)) {
296
                    if (strpos($column, 'SELECT') !== false) {
297
                        $select[$alias] = $column;
298
                    }
299
                    else {
300
                        $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

300
                        $select[$alias] = $this->addColumnAlias($builder->/** @scrutinizer ignore-call */ fetchColumnName($column));
Loading history...
301
                    }
302
                }
303
                else {
304
                    $select[$alias] = $column;
305
                }
306
            }
307
        }
308
        else if (is_string($this->_select)) {
309
            $select = $this->addColumnAlias($this->_select);
310
        }
311
        return $this->getAdapter()->sqlSelect($select, $this->_queryOptions);
312
    }
313
314
    protected function pushToSelect($select, $alias = null)
315
    {
316
        $isValid = is_string($select)
317
            || (is_numeric($select) && is_finite($select))
318
            || is_a($select, Expression::class)
319
            || is_a($select, Aggregation::class)
320
        ;
321
322
        if (!$isValid) {
323
            throw new QBException('Incorrect select type');
324
        }
325
326
        if ($alias) {
327
            $this->_select[$alias] = $select;
328
        }
329
        else {
330
            $this->_select[] = $select;
331
        }
332
333
        return $this;
334
    }
335
336
    /**
337
     * @param string|IToSql $select
338
     * @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...
339
     * @return QueryBuilderInterface
340
     */
341
    public function addSelect($select, $alias = null)
342
    {
343
        if (is_string($select) && $newSelect = $this->getLookupBuilder()->buildJoin($this, $select)) {
344
            list($t_alias, $column) = $newSelect;
345
            $this->pushToSelect($t_alias . '.' . $column, $alias);
346
        }
347
        else {
348
            $this->pushToSelect(
349
                $this->hydrate($select),
350
                $alias
351
            );
352
        }
353
354
        return $this;
355
    }
356
357
    /**
358
     * @param array|string $select
359
     * @return QueryBuilderInterface
360
     */
361
    public function setSelect($select)
362
    {
363
        $this->_select = [];
364
365
        if (empty($select)) {
366
            return $this;
367
        }
368
369
        if (is_array($select)) {
370
            foreach ($select as $key => $part) {
371
                $this->addSelect($part, $key);
372
            }
373
        }
374
        else {
375
            $this->addSelect($select);
376
        }
377
378
        return $this;
379
    }
380
381
    /**
382
     * @param array|string $tableName
383
     * @param null|string $alias
384
     * @return QueryBuilderInterface
385
     */
386
    public function setFrom($tableName, $alias = null)
387
    {
388
        if ($alias && is_string($alias)) {
389
            if (is_array($tableName)) {
390
                $tableName = current($tableName);
391
            }
392
393
            $tableName = [$alias => $tableName];
394
        }
395
396
        $this->_from = $tableName;
397
        return $this;
398
    }
399
400
    /**
401
     * @param $alias string join alias
402
     * @return bool
403
     */
404
    public function hasJoin($alias)
405
    {
406
        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

406
        return array_key_exists($alias, /** @scrutinizer ignore-type */ $this->_join);
Loading history...
407
    }
408
409
    /**
410
     * @param int $page
411
     * @param int $pageSize
412
     * @return QueryBuilderInterface
413
     */
414
    public function paginate($page = 1, $pageSize = 10)
415
    {
416
        $page = (int)$page;
417
        $pageSize = (int)$pageSize;
418
419
        $this->setLimit($pageSize);
420
        $this->setOffset($page > 1 ? $pageSize * ($page - 1) : 0);
421
        return $this;
422
    }
423
424
    /**
425
     * @param string|number $limit
426
     * @return QueryBuilderInterface
427
     */
428
    public function setLimit($limit)
429
    {
430
        $this->_limit = (int)$limit;
431
        return $this;
432
    }
433
434
    /**
435
     * @return int
436
     */
437
    public function getLimit()
438
    {
439
        return $this->_limit;
440
    }
441
442
    /**
443
     * @param string|number $offset
444
     * @return QueryBuilderInterface
445
     */
446
    public function setOffset($offset)
447
    {
448
        $this->_offset = (int)$offset;
449
        return $this;
450
    }
451
452
    /**
453
     * @return int|string|null
454
     */
455
    public function getOffset()
456
    {
457
        return $this->_offset;
458
    }
459
460
    /**
461
     * @return ILookupBuilder|\Tsukasa\QueryBuilder\LookupBuilder\LookupBuilder
462
     */
463
    public function getLookupBuilder()
464
    {
465
        return $this->lookupBuilder;
466
    }
467
468
    /**
469
     * @return ISQLGenerator
470
     */
471
    public function getAdapter()
472
    {
473
        return $this->adapter;
474
    }
475
476
    /**
477
     * @param string $joinType LEFT JOIN, RIGHT JOIN, etc...
478
     * @param string|QueryBuilderInterface $tableName
479
     * @param array $on link columns
480
     * @param string|null $alias string
481
     * @param string|null $index
482
     * @return QueryBuilderInterface
483
     */
484
    public function join($joinType, $tableName, array $on = [], $alias = null, $index = null)
485
    {
486
        if ($tableName instanceof QueryBuilderInterface) {
487
            $this->_join[] = $this->getAdapter()->sqlJoin($joinType, $tableName, $on, $alias, $index);
488
        }
489
        else {
490
            if ($joinType === 'RAW' && !empty($tableName)) {
491
                $join = $this->getAdapter()->quoteSql($tableName);
492
            }
493
            else {
494
                $join = $this->getAdapter()->sqlJoin($joinType, $tableName, $on, $alias);
495
            }
496
497
            if (!$alias) {
498
                $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

498
                $alias = count(/** @scrutinizer ignore-type */ $this->_join);
Loading history...
499
            }
500
            $this->_join[$alias] = $join;
501
            $this->_joinAlias[$tableName][] = $alias;
502
        }
503
        return $this;
504
    }
505
506
    /**
507
     * @param $sql
508
     * @param string $alias
509
     * @return QueryBuilderInterface
510
     */
511
    public function joinRaw($sql, $alias = null)
512
    {
513
        return $this->join('RAW', $sql, [], $alias);
514
    }
515
516
    /**
517
     * @param array|string $columns columns
518
     * @return QueryBuilderInterface
519
     */
520
    public function setGroup($columns)
521
    {
522
        if ($columns && is_string($columns)) {
523
            $columns = array_map('trim', explode(',', $columns));
524
        }
525
526
        $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...
527
        return $this;
528
    }
529
530
    /**
531
     * @param array|string $columns columns
532
     * @return QueryBuilderInterface
533
     */
534
    public function addGroup($columns)
535
    {
536
        if ($columns && is_string($columns)) {
537
            $columns = array_map('trim', explode(',', $columns));
538
        }
539
        $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

539
        $this->_group = array_merge($this->_group, /** @scrutinizer ignore-type */ $columns);
Loading history...
540
        return $this;
541
    }
542
543
    protected function pushOrder($column)
544
    {
545
546
    }
547
548
    /**
549
     * @param array|string|null $columns columns
550
     * @return QueryBuilderInterface
551
     */
552
    public function setOrder($columns)
553
    {
554
555
        $this->_order = [];
556
557
        if (empty($columns)) {
558
            return $this;
559
        }
560
561
        if (is_array($columns)) {
562
            foreach ($columns as $column) {
563
                $this->addOrder($column);
564
            }
565
        }
566
        else {
567
            $this->addOrder($columns);
568
        }
569
570
        return $this;
571
    }
572
573
    /**
574
     * @param string|Expression $column
575
     * @return QueryBuilderInterface
576
     */
577
    public function addOrder($column)
578
    {
579
        $isValid = is_string($column)
580
            || is_a($column, Expression::class)
581
        ;
582
583
        if (!$isValid) {
584
            throw new QBException('Incorrect order type');
585
        }
586
587
        if (is_string($column) && strpos($column, ',') !== false) {
588
            $columns = preg_split('/\s*,\s*/', $column, -1, PREG_SPLIT_NO_EMPTY);
589
590
            foreach ($columns as $_column) {
591
                $temp = explode(' ', $_column);
592
                if (count($temp) === 2) {
593
594
                    $_column = $temp[0];
595
                    if (strtoupper(trim($temp[1])) === 'DESC') {
596
                        $_column = '-' . $_column;
597
                    }
598
599
                    $this->_order[] = $_column;
600
                }
601
                else {
602
                    $this->_order[] = current($temp);
603
                }
604
            }
605
        }
606
        else {
607
            $this->_order[] = $column;
608
        }
609
610
        return $this;
611
    }
612
613
    /**
614
     * @param $tableName
615
     * @param array $rows
616
     * @return QueryBuilderInterface
617
     */
618
    public function insert($tableName, $rows)
619
    {
620
        $this->setTypeInsert();
621
        $this->_update = [$tableName, $rows];
622
        return $this;
623
    }
624
625
    /**
626
     * @param $tableName string
627
     * @param array $values columns [name => value...]
628
     * @return QueryBuilderInterface
629
     */
630
    public function update($tableName, array $values)
631
    {
632
        $this->setTypeUpdate();
633
        $this->_update = [$tableName, $values];
634
        return $this;
635
    }
636
637
    public function getAlias()
638
    {
639
        return $this->_alias;
640
    }
641
642
    public function setAlias($alias = null)
643
    {
644
        if (empty($alias)) {
645
            $alias = null;
646
        }
647
648
        $this->_alias = $alias;
649
        return $this;
650
    }
651
652
    protected function buildCondition($condition, &$params = [])
653
    {
654
        if (!is_array($condition)) {
655
            return (string)$condition;
656
        }
657
658
        if (empty($condition)) {
659
            return '';
660
        }
661
662
        if (isset($condition[0]) && is_string($condition[0])) {
663
            $operatorRaw = array_shift($condition);
664
            $operator = strtoupper($operatorRaw);
665
            return $this->buildAndCondition($operator, $condition, $params);
666
        }
667
668
        return $this->parseCondition($condition);
669
    }
670
671
    public function getJoinAlias($tableName)
672
    {
673
        return $this->_joinAlias[$tableName];
674
    }
675
676
    public function getJoins()
677
    {
678
        return $this->_join;
679
    }
680
681
    /**
682
     * @param $condition
683
     * @param string $operator
684
     * @return string
685
     */
686
    public function parseCondition($condition, $operator = 'AND')
687
    {
688
        $parts = [];
689
690
        if (is_array($condition)) {
691
            foreach ($condition as $key => $value) {
692
                if (is_numeric($key)) {
693
                    if ($value instanceof IToSql) {
694
                        $parts[] = $this->parseCondition($value, $operator);
695
                    }
696
                    elseif ($value instanceof QueryBuilder) {
697
                        $parts[] = $this->parseCondition($value, $operator);
698
                    }
699
                    else if (is_array($value)) {
700
                        $parts[] = $this->parseCondition($value, $operator);
701
                    }
702
                    else if (is_string($value)) {
703
                        $parts[] = $value;
704
                    }
705
                }
706
                else {
707
                    $tableAlias = $this->getAlias();
708
                    $value = $this->getAdapter()->prepareValue($value);
709
710
                    list($lookup, $column, $lookupValue) = $this->lookupBuilder->parseLookup($this, $key, $value);
711
                    $column = $this->getLookupBuilder()->fetchColumnName($column);
712
                    if ($tableAlias !== null && strpos($column, '.') === false) {
713
                        $column = $tableAlias . '.' . $column;
714
                    }
715
                    $parts[] = $this->lookupBuilder->runLookup($this->getAdapter(), $lookup, $column, $lookupValue);
716
                }
717
            }
718
719
            if ($parts) {
720
                if (count($parts) === 1) {
721
                    return $parts[0];
722
                }
723
724
                return '(' . implode(') ' . $operator . ' (', $parts) . ')';
725
            }
726
727
        }
728
        else if ($condition instanceof IToSql) {
729
            return $condition
730
                ->setQb($this)
731
                ->toSql();
732
        }
733
        else if ($condition instanceof QueryBuilder) {
734
            return $condition->toSQL();
735
        }
736
        else if (is_string($condition)) {
737
            return $condition;
738
        }
739
740
        return '';
741
    }
742
743
    protected function buildAndCondition($operator, $operands, &$params)
744
    {
745
        $parts = [];
746
        foreach ($operands as $operand) {
747
            if (is_array($operand)) {
748
                $operand = $this->buildCondition($operand, $params);
749
            }
750
            else {
751
                $operand = $this->parseCondition($operand);
752
            }
753
            if ($operand !== '') {
754
                $parts[] = $this->getAdapter()->quoteSql($operand);
755
            }
756
        }
757
        if (!empty($parts)) {
758
            return '(' . implode(') ' . $operator . ' (', $parts) . ')';
759
        }
760
761
        return '';
762
    }
763
764
    /**
765
     * @param $condition
766
     * @return QueryBuilderInterface
767
     */
768
    public function addWhere($condition)
769
    {
770
        if (!empty($condition)) {
771
            $this->_whereAnd[] = $this->hydrate($condition);
772
        }
773
        return $this;
774
    }
775
776
    public function setWhere($condition)
777
    {
778
        $this->_whereAnd = [];
779
780
        return $this->addWhere($condition);
781
    }
782
783
    /**
784
     * @param $condition
785
     * @return QueryBuilderInterface
786
     */
787
    public function addOrWhere($condition)
788
    {
789
        if (!empty($condition)) {
790
            $this->_whereOr[] = $this->hydrate($condition);
791
        }
792
        return $this;
793
    }
794
795
    public function setOrWhere($condition)
796
    {
797
        $this->_whereOr = [];
798
799
        return $this->addWhere($condition);
800
    }
801
802
    /**
803
     * @return array
804
     */
805
    protected function buildWhereTree()
806
    {
807
        $where = [];
808
        foreach ($this->_whereAnd as $condition) {
809
            if (empty($where)) {
810
                $where = ['and', $condition];
811
            }
812
            else {
813
                $where = ['and', $where, ['and', $condition]];
814
            }
815
        }
816
817
        foreach ($this->_whereOr as $condition) {
818
            if (empty($where)) {
819
                $where = ['or', $condition];
820
            }
821
            else {
822
                $where = ['or', $where, ['and', $condition]];
823
            }
824
        }
825
        return $where;
826
    }
827
828
    public function getSelect()
829
    {
830
        return $this->_select;
831
    }
832
833
    protected function buildWhere()
834
    {
835
        $params = [];
836
        $sql = $this->buildCondition($this->buildWhereTree(), $params);
837
        return empty($sql) ? '' : ' WHERE ' . $sql;
838
    }
839
840
841
    protected function generateSelectSql()
842
    {
843
        // Fetch where conditions before pass it to adapter.
844
        // Reason: Dynamic sql build in callbacks
845
846
        // $this->prepareJoin();
847
848
        $where = $this->buildWhere();
849
        $order = $this->buildOrder();
850
        $union = $this->buildUnion();
851
852
        $select = $this->buildSelect();
853
        $from = $this->buildFrom();
854
        $join = $this->buildJoin();
855
        $group = $this->buildGroup();
856
        $having = $this->buildHaving();
857
        $limitOffset = $this->buildLimitOffset();
858
        return strtr('{select}{from}{join}{where}{group}{having}{order}{limit_offset}{union}', [
859
            '{select}' => $select,
860
            '{from}' => $from,
861
            '{where}' => $where,
862
            '{group}' => $group,
863
            '{order}' => empty($union) ? $order : '',
864
            '{having}' => $having,
865
            '{join}' => $join,
866
            '{limit_offset}' => $limitOffset,
867
            '{union}' => empty($union) ? '' : $union . $order
868
        ]);
869
    }
870
871
    protected function generateDeleteSql()
872
    {
873
        $options = $this->_queryOptions;
874
        if ($options) {
875
            $options = " {$options} ";
876
        }
877
878
        $limitOffset = $this->buildLimitOffset();
879
        return strtr('{delete}{options}{from}{where}{limit_offset}', [
880
            '{delete}' => 'DELETE',
881
            '{options}' => $options,
882
            '{from}' => $this->buildFrom(),
883
            '{where}' => $this->buildWhere(),
884
            '{limit_offset}' => $limitOffset,
885
        ]);
886
    }
887
888
    protected function generateInsertSql()
889
    {
890
        list($tableName, $values) = $this->_update;
891
        $this->setAlias();
892
        return $this->getAdapter()->sqlInsert($tableName, $values, $this->_queryOptions);
893
    }
894
895
    protected function generateUpdateSql()
896
    {
897
        list($tableName, $values) = $this->_update;
898
        $this->setAlias();
899
        return strtr('{update}{where}', [
900
            '{update}' => $this->getAdapter()->sqlUpdate($tableName, $values, $this->_queryOptions),
901
            '{where}' => $this->buildWhere(),
902
        ]);
903
    }
904
905
    /**
906
     * @return string
907
     * @throws QBException
908
     */
909
    public function toSQL()
910
    {
911
        switch ($this->getType())
912
        {
913
            case self::TYPE_SELECT:
914
                return $this->generateSelectSql();
915
916
            case self::TYPE_INSERT:
917
                return $this->generateInsertSql();
918
919
            case self::TYPE_UPDATE:
920
                return $this->generateUpdateSql();
921
922
            case self::TYPE_DELETE:
923
                return $this->generateDeleteSql();
924
        }
925
926
        throw new QBException('Unknown query type');
927
    }
928
929
    protected function buildHaving()
930
    {
931
        return $this->getAdapter()->sqlHaving(
932
            $this->parseCondition($this->_having)
933
        );
934
    }
935
936
    protected function buildLimitOffset()
937
    {
938
        return $this->getAdapter()->sqlLimitOffset(
939
            $this->_limit,
940
            $this->_offset
941
        );
942
    }
943
944
    protected function buildUnion()
945
    {
946
        $sql = '';
947
        foreach ($this->_union as  list($union, $all)) {
948
            $sql .= ' ' . $this->getAdapter()->sqlUnion($union, $all);
949
        }
950
951
        return empty($sql) ? '' : $sql;
952
    }
953
954
    /**
955
     * @param array|string|Q $having lookups
956
     * @return QueryBuilderInterface
957
     */
958
    public function setHaving($having)
959
    {
960
        $this->_having = [];
961
962
        return $this->addHaving($having);
963
    }
964
965
    public function addHaving($having)
966
    {
967
        if (!empty($having)) {
968
            $this->_having[] = $this->hydrate($having);
969
        }
970
971
        return $this;
972
    }
973
974
    public function setUnions(array $unions, $all = false)
975
    {
976
        $this->_union = [];
977
978
        if (!empty($unions)) {
979
            foreach ($unions as $union) {
980
                $this->addUnion($union, $all);
981
            }
982
        }
983
984
        return $this;
985
    }
986
987
    public function addUnion($union, $all = false)
988
    {
989
        $this->_union[] = [$union, $all];
990
        return $this;
991
    }
992
993
    /**
994
     * Makes alias for joined table
995
     * @param $table
996
     * @param bool $increment
997
     * @return string
998
     */
999
    public function makeAliasKey($table, $increment = false)
1000
    {
1001
        if ($increment) {
1002
            ++$this->_aliasesCount;
1003
        }
1004
1005
        $tableName = $this->getAdapter()->getRawTableName($table);
1006
1007
        if (strpos($tableName, '.') !== false) {
1008
            $tableName = substr($tableName, strpos($tableName, '.') + 1);
1009
        }
1010
1011
        return strtr('{table}_{count}', [
1012
            '{table}' => $tableName,
1013
            '{count}' => $this->_aliasesCount + 1
1014
        ]);
1015
    }
1016
1017
    /**
1018
     * @param string $table
1019
     * @param string $code
1020
     * @param string $topAlias
1021
     *
1022
     * @return string
1023
     */
1024
    public function makeMappedAliasKey($table, $code, $topAlias = null)
1025
    {
1026
        $key = $topAlias . '_' . $code;
1027
1028
        if (empty($this->_joinAlias[$table])) {
1029
            $this->_joinAlias[$table]['__alias_count__'] = 1;
1030
        }
1031
1032
        if (!empty($this->_joinAlias[$table][$key])) {
1033
            return $this->_joinAlias[$table][$key];
1034
        }
1035
1036
        $this->_joinAlias[$table][$key] = strtr('{table}_{count}', [
1037
            '{table}' => $this->getAdapter()->getRawTableName($table),
1038
            '{count}' => $this->_joinAlias[$table]['__alias_count__'] += 1
1039
        ]);
1040
1041
        return $this->_joinAlias[$table][$key];
1042
    }
1043
1044
    public function getJoin($tableName)
1045
    {
1046
        return $this->_join[$tableName];
1047
    }
1048
1049
    /**
1050
     * @param $column
1051
     * @return string
1052
     */
1053
    protected function addColumnAlias($column)
1054
    {
1055
        $tableAlias = $this->getAlias();
1056
        if ($tableAlias === null) {
1057
            return $column;
1058
        }
1059
1060
        if (strpos($column, '.') === false &&
1061
            strpos($column, '(') === false &&
1062
            strpos($column, 'SELECT') === false
1063
        ) {
1064
            return $tableAlias . '.' . $column;
1065
        }
1066
1067
        return $column;
1068
    }
1069
1070
    protected function hasAliasedField($column)
1071
    {
1072
        foreach ($this->_select as $alias => $item)
1073
        {
1074
            if (!is_numeric($alias) && $column === $alias) {
1075
                return true;
1076
            }
1077
        }
1078
1079
        return false;
1080
    }
1081
1082
    protected function applyTableAlias($column)
1083
    {
1084
        // If column already has alias - skip
1085
        if ((strpos($column, '.') === false) && !$this->hasAliasedField($column)) {
1086
            $tableAlias = $this->getAlias();
1087
            return $tableAlias === null ? $column : $tableAlias . '.' . $column;
1088
        }
1089
1090
        return $column;
1091
    }
1092
1093
    protected function buildJoin()
1094
    {
1095
        if (empty($this->_join)) {
1096
            return '';
1097
        }
1098
        $join = [];
1099
        foreach ($this->_join as $part) {
1100
            $join[] = $part;
1101
        }
1102
        return ' ' . implode(' ', $join);
1103
    }
1104
1105
1106
    /**
1107
     * @param $order
1108
     * @return array
1109
     */
1110
    protected function buildOrderJoin($order)
1111
    {
1112
        if (strpos($order, '-') === false) {
1113
            $direction = 'ASC';
1114
        }
1115
        else {
1116
            $direction = 'DESC';
1117
            $order = substr($order, 1);
1118
        }
1119
        $order = $this->getLookupBuilder()->fetchColumnName($order);
1120
        $newOrder = $this->getLookupBuilder()->buildJoin($this, $order);
1121
        if ($newOrder === false) {
1122
            return [$order, $direction];
1123
        }
1124
1125
        list($alias, $column) = $newOrder;
1126
        return [$alias . '.' . $column, $direction];
1127
    }
1128
1129
    public function getOrder()
1130
    {
1131
        return $this->_order;
1132
    }
1133
1134
    protected function buildOrder()
1135
    {
1136
        /**
1137
         * не делать проверку по empty(), проваливается половина тестов с ORDER BY
1138
         * и проваливается тест с построением JOIN по lookup
1139
         */
1140
        if ($this->_order === null) {
1141
            return '';
1142
        }
1143
1144
        $order = [];
1145
        if (is_array($this->_order)) {
1146
            foreach ($this->_order as $column) {
1147
                if ($column instanceof Expression) {
1148
                    $order[$column->toSQL()] = '';
1149
                }
1150
                else if ($column === '?') {
1151
                    $order[] = $this->getAdapter()->getRandomOrder();
1152
                }
1153
                else {
1154
                    list($newColumn, $direction) = $this->buildOrderJoin($column);
1155
                    $order[$this->applyTableAlias($newColumn)] = $direction;
1156
                }
1157
            }
1158
        }
1159
        else {
1160
            $order[] = $this->buildOrderJoin($this->_order);
1161
        }
1162
1163
        $sql = $this->getAdapter()->sqlOrderBy($order);
1164
        return empty($sql) ? '' : ' ORDER BY ' . $sql;
1165
    }
1166
1167
    /**
1168
     * @param $group
1169
     * @return string
1170
     */
1171
    protected function buildGroupJoin($group)
1172
    {
1173
        if (strpos($group, '.') === false) {
1174
            $newGroup = $this->getLookupBuilder()->fetchColumnName($group);
1175
            $newGroup = $this->getLookupBuilder()->buildJoin($this, $newGroup);
1176
1177
            if ($newGroup === false) {
1178
                return $group;
1179
            }
1180
1181
            list($alias, $column) = $newGroup;
1182
            return $alias . '.' . $column;
1183
        }
1184
1185
        return $group;
1186
    }
1187
1188
    protected function buildGroup()
1189
    {
1190
        $group = [];
1191
        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...
1192
            foreach ($this->_group as $key => $column) {
1193
                $newColumn = $this->buildGroupJoin($column);
1194
                $group[] = $this->applyTableAlias($newColumn);
1195
            }
1196
        }
1197
1198
        $sql = $this->getAdapter()->sqlGroupBy($group);
1199
        return empty($sql) ? '' : ' GROUP BY ' . $sql;
1200
    }
1201
1202
    protected function buildFrom()
1203
    {
1204
        if ($this->_alias !== null && !is_array($this->_from)) {
1205
            $from = [$this->_alias => $this->_from];
1206
        }
1207
        else {
1208
            $from = $this->_from;
1209
        }
1210
        $sql = $this->getAdapter()->sqlFrom($from);
1211
        return empty($sql) ? '' : ' FROM ' . $sql;
1212
    }
1213
1214
    protected function hydrate($val)
1215
    {
1216
        if (is_object($val)) {
1217
            if ($val instanceof IToSql) {
1218
                $val->setQb($this);
1219
            }
1220
        }
1221
1222
        return $val;
1223
    }
1224
}
1225