Passed
Push — master ( 3ed246...04b8fa )
by Maksim
07:23
created

QueryBuilder::getLimit()   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
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
     * @return \Doctrine\DBAL\Platforms\AbstractPlatform
128
     */
129
    public function getDatabasePlatform()
130
    {
131
        return $this->getConnection()->getDatabasePlatform();
0 ignored issues
show
Bug introduced by
The method getDatabasePlatform() 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

131
        return $this->getConnection()->/** @scrutinizer ignore-call */ getDatabasePlatform();
Loading history...
132
    }
133
134
    /**
135
     * @param Connection $connection
136
     * @param BaseAdapter|null $adapter
137
     * @param LookupBuilder|null $lookupBuilder
138
     * @return QueryBuilder
139
     */
140
    public static function getInstance(Connection $connection, $adapter = null, $lookupBuilder = null)
141
    {
142
        if ($adapter === null) {
143
            $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

143
            /** @scrutinizer ignore-call */ 
144
            $driver = $connection->getDriver();
Loading history...
144
            switch ($driver->getName()) {
145
                case 'pdo_mysql':
146
                    $adapter = new MysqlAdapter($connection);
147
                    break;
148
                case 'pdo_sqlite':
149
                    $adapter = new SqliteAdapter($connection);
150
                    break;
151
                case 'pdo_pgsql':
152
                    $adapter = new PgsqlAdapter($connection);
153
                    break;
154
                default:
155
                    throw new QBException('Unknown driver');
156
            }
157
        }
158
159
        $lookupBuilder = $lookupBuilder ?: new LookupBuilder();
160
        $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

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

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

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

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

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

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