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

QueryBuilder::hydrate()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 3
nop 1
dl 0
loc 9
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
        }
186
        else {
187
            throw new QBException('Incorrect type');
188
        }
189
190
191
        return $this;
192
    }
193
194
    /**
195
     * @return QueryBuilderInterface
196
     */
197
    public function setTypeSelect()
198
    {
199
        $this->_type = self::TYPE_SELECT;
200
        return $this;
201
    }
202
203
    /**
204
     * @return QueryBuilderInterface
205
     */
206
    public function setTypeInsert()
207
    {
208
        $this->_type = self::TYPE_INSERT;
209
        return $this;
210
    }
211
212
    /**
213
     * @return QueryBuilderInterface
214
     */
215
    public function setTypeUpdate()
216
    {
217
        $this->_type = self::TYPE_UPDATE;
218
        return $this;
219
    }
220
221
    /**
222
     * @return QueryBuilderInterface
223
     */
224
    public function setTypeDelete()
225
    {
226
        $this->_type = self::TYPE_DELETE;
227
        return $this;
228
    }
229
230
    /**
231
     * If type is null return TYPE_SELECT
232
     * @return string
233
     */
234
    public function getType()
235
    {
236
        return $this->_type === null
237
            ? self::TYPE_SELECT
238
            : $this->_type;
239
    }
240
241
    public function setOptions($options = '')
242
    {
243
        $this->_queryOptions = $options;
244
        return $this;
245
    }
246
247
    /**
248
     * @param Aggregation $aggregation
249
     * @return string
250
     */
251
    protected function buildSelectFromAggregation(Aggregation $aggregation)
252
    {
253
        $tableAlias = $this->getAlias();
254
        $rawColumn = $aggregation->getField();
255
        $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

255
        $newSelect = $this->getLookupBuilder()->/** @scrutinizer ignore-call */ buildJoin($this, $rawColumn);
Loading history...
256
        if ($newSelect === false) {
257
            if ($tableAlias === null || $rawColumn === '*') {
258
                $columns = $rawColumn;
259
            }
260
            elseif (strpos($rawColumn, '.') !== false) {
261
                $columns = $rawColumn;
262
            }
263
            else {
264
                $columns = $tableAlias . '.' . $rawColumn;
265
            }
266
        }
267
        else {
268
            list($alias, $joinColumn) = $newSelect;
269
            $columns = $alias . '.' . $joinColumn;
270
        }
271
        $fieldsSql = $this->getAdapter()->buildColumns($columns);
272
        $aggregation->setFieldSql($fieldsSql);
273
274
        return $aggregation->setQB($this)->toSQL();
275
    }
276
277
    /**
278
     * @return string
279
     * @throws \Exception
280
     */
281
    protected function buildSelect()
282
    {
283
        if (empty($this->_select)) {
284
            $this->_select = ['*'];
285
        }
286
287
        $select = [];
288
        $builder = $this->getLookupBuilder();
289
        if (is_array($this->_select)) {
290
            foreach ($this->_select as $alias => $column) {
291
                if ($column instanceof Aggregation) {
292
                    $select[$alias] = $this->buildSelectFromAggregation($column);
293
                }
294
                else if (is_string($column)) {
295
                    if (strpos($column, 'SELECT') !== false) {
296
                        $select[$alias] = $column;
297
                    }
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
                }
302
                else {
303
                    $select[$alias] = $column;
304
                }
305
            }
306
        }
307
        else if (is_string($this->_select)) {
308
            $select = $this->addColumnAlias($this->_select);
309
        }
310
        return $this->getAdapter()->sqlSelect($select, $this->_queryOptions);
311
    }
312
313
    protected function pushToSelect($select, $alias = null)
314
    {
315
        $isValid = is_string($select)
316
            || (is_numeric($select) && is_finite($select))
317
            || is_a($select, Expression::class)
318
            || is_a($select, Aggregation::class)
319
        ;
320
321
        if (!$isValid) {
322
            throw new QBException('Incorrect select type');
323
        }
324
325
        if ($alias) {
326
            $this->_select[$alias] = $select;
327
        }
328
        else {
329
            $this->_select[] = $select;
330
        }
331
332
        return $this;
333
    }
334
335
    /**
336
     * @param string|IToSql $select
337
     * @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...
338
     * @return QueryBuilderInterface
339
     */
340
    public function addSelect($select, $alias = null)
341
    {
342
        if (is_string($select) && $newSelect = $this->getLookupBuilder()->buildJoin($this, $select)) {
343
            list($t_alias, $column) = $newSelect;
344
            $this->pushToSelect($t_alias . '.' . $column, $alias);
345
        }
346
        else {
347
            $this->pushToSelect(
348
                $this->hydrate($select),
349
                $alias
350
            );
351
        }
352
353
        return $this;
354
    }
355
356
    /**
357
     * @param array|string $select
358
     * @return QueryBuilderInterface
359
     */
360
    public function setSelect($select)
361
    {
362
        $this->_select = [];
363
364
        if (empty($select)) {
365
            return $this;
366
        }
367
368
        if (is_array($select)) {
369
            foreach ($select as $key => $part) {
370
                $this->addSelect($part, $key);
371
            }
372
        }
373
        else {
374
            $this->addSelect($select);
375
        }
376
377
        return $this;
378
    }
379
380
    /**
381
     * @param array|string $tableName
382
     * @param null|string $alias
383
     * @return QueryBuilderInterface
384
     */
385
    public function setFrom($tableName, $alias = null)
386
    {
387
        if ($alias && is_string($alias)) {
388
            if (is_array($tableName)) {
389
                $tableName = current($tableName);
390
            }
391
392
            $tableName = [$alias => $tableName];
393
        }
394
395
        $this->_from = $tableName;
396
        return $this;
397
    }
398
399
    /**
400
     * @param $alias string join alias
401
     * @return bool
402
     */
403
    public function hasJoin($alias)
404
    {
405
        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

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

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

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