Completed
Pull Request — master (#2)
by Maksim
04:01
created

QueryBuilder::getDatabasePlatform()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

135
            /** @scrutinizer ignore-call */ 
136
            $driver = $connection->getDriver();
Loading history...
136
            switch ($driver->getName()) {
137
                case 'pdo_mysql':
138
                    $adapter = new MysqlAdapter($connection);
139
                    break;
140
                case 'pdo_sqlite':
141
                    $adapter = new SqliteAdapter($connection);
142
                    break;
143
                case 'pdo_pgsql':
144
                    $adapter = new PgsqlAdapter($connection);
145
                    break;
146
                default:
147
                    throw new QBException('Unknown driver');
148
            }
149
        }
150
151
        $lookupBuilder = $lookupBuilder ?: new LookupBuilder();
152
        $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

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

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

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

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

482
                $alias = count(/** @scrutinizer ignore-type */ $this->_join);
Loading history...
483
            }
484
            $this->_join[$alias] = $join;
485
            $this->_joinAlias[$tableName][] = $alias;
486
        }
487
        return $this;
488
    }
489
490
    /**
491
     * @param $sql
492
     * @param string $alias
493
     * @return QueryBuilderInterface
494
     */
495
    public function joinRaw($sql, $alias = null)
496
    {
497
        return $this->join('RAW', $sql, [], $alias);
498
    }
499
500
    /**
501
     * @param array|string $columns columns
502
     * @return QueryBuilderInterface
503
     */
504
    public function setGroup($columns)
505
    {
506
        if ($columns && is_string($columns)) {
507
            $columns = array_map('trim', explode(',', $columns));
508
        }
509
510
        $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...
511
        return $this;
512
    }
513
514
    /**
515
     * @param array|string $columns columns
516
     * @return QueryBuilderInterface
517
     */
518
    public function addGroup($columns)
519
    {
520
        if ($columns && is_string($columns)) {
521
            $columns = array_map('trim', explode(',', $columns));
522
        }
523
        $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

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