Passed
Pull Request — master (#2)
by Maksim
02:06
created

QueryBuilder::setTypeInsert()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
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 if ($select instanceof IToSql) {
337
            $this->pushToSelect($select->setQb($this), $alias);
338
        } else {
339
            $this->pushToSelect($select, $alias);
340
        }
341
342
        return $this;
343
    }
344
345
    /**
346
     * @param array|string $select
347
     * @return QueryBuilderInterface
348
     */
349
    public function setSelect($select)
350
    {
351
        $this->_select = [];
352
353
        if (empty($select)) {
354
            return $this;
355
        }
356
357
        if (is_array($select)) {
358
            foreach ($select as $key => $part) {
359
                $this->addSelect($part, $key);
360
            }
361
        } else {
362
            $this->addSelect($select);
363
        }
364
365
        return $this;
366
    }
367
368
    /**
369
     * @param array|string $tableName
370
     * @param null|string $alias
371
     * @return QueryBuilderInterface
372
     */
373
    public function setFrom($tableName, $alias = null)
374
    {
375
        if ($alias && is_string($alias)) {
376
            if (is_array($tableName)) {
377
                $tableName = current($tableName);
378
            }
379
380
            $tableName = [$alias => $tableName];
381
        }
382
383
        $this->_from = $tableName;
384
        return $this;
385
    }
386
387
    /**
388
     * @param $alias string join alias
389
     * @return bool
390
     */
391
    public function hasJoin($alias)
392
    {
393
        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

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

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

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