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