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\ISQLGenerator; |
||
13 | use Tsukasa\QueryBuilder\Interfaces\IToSql; |
||
14 | use Tsukasa\QueryBuilder\Interfaces\QueryBuilderInterface; |
||
15 | use Tsukasa\QueryBuilder\LookupBuilder\LookupBuilder; |
||
16 | use Tsukasa\QueryBuilder\Q\Q; |
||
17 | |||
18 | use Tsukasa\QueryBuilder\Database\Mysql\Adapter as MysqlAdapter; |
||
19 | use Tsukasa\QueryBuilder\Database\Sqlite\Adapter as SqliteAdapter; |
||
20 | use Tsukasa\QueryBuilder\Database\Pgsql\Adapter as PgsqlAdapter; |
||
21 | |||
22 | class QueryBuilder implements QueryBuilderInterface |
||
23 | { |
||
24 | const TYPE_SELECT = 'SELECT'; |
||
25 | const TYPE_INSERT = 'INSERT'; |
||
26 | const TYPE_UPDATE = 'UPDATE'; |
||
27 | const TYPE_DELETE = 'DELETE'; |
||
28 | |||
29 | /** |
||
30 | * @var array|Q|string |
||
31 | */ |
||
32 | private $_whereAnd = []; |
||
33 | /** |
||
34 | * @var array|Q|string |
||
35 | */ |
||
36 | private $_whereOr = []; |
||
37 | /** |
||
38 | * @var array|string |
||
39 | */ |
||
40 | private $_join = []; |
||
41 | /** |
||
42 | * @var array|string |
||
43 | */ |
||
44 | private $_order = []; |
||
45 | /** |
||
46 | * @var array |
||
47 | */ |
||
48 | private $_group = []; |
||
49 | /** |
||
50 | * @var array|string|\Tsukasa\QueryBuilder\Aggregation\Aggregation |
||
51 | */ |
||
52 | private $_select = []; |
||
53 | /** |
||
54 | * @var array|string|null |
||
55 | */ |
||
56 | private $_from; |
||
57 | /** |
||
58 | * @var array |
||
59 | */ |
||
60 | private $_union = []; |
||
61 | /** |
||
62 | * @var null|int |
||
63 | */ |
||
64 | private $_limit; |
||
65 | /** |
||
66 | * @var null|int |
||
67 | */ |
||
68 | private $_offset; |
||
69 | /** |
||
70 | * @var array |
||
71 | */ |
||
72 | private $_having = []; |
||
73 | /** |
||
74 | * @var null|string |
||
75 | */ |
||
76 | private $_alias; |
||
77 | /** |
||
78 | * @var null|string sql query type SELECT|UPDATE|DELETE |
||
79 | */ |
||
80 | private $_type; |
||
81 | /** |
||
82 | * @var array |
||
83 | */ |
||
84 | private $_update = []; |
||
85 | /** |
||
86 | * @var ISQLGenerator |
||
87 | */ |
||
88 | protected $adapter; |
||
89 | /** |
||
90 | * @var ILookupBuilder |
||
91 | */ |
||
92 | protected $lookupBuilder; |
||
93 | /** |
||
94 | * @var null |
||
95 | */ |
||
96 | protected $schema; |
||
97 | /** |
||
98 | * Counter of joined tables aliases |
||
99 | * @var int |
||
100 | */ |
||
101 | private $_aliasesCount = 0; |
||
102 | |||
103 | private $_joinAlias = []; |
||
104 | |||
105 | /** |
||
106 | * Strings options query |
||
107 | * @var string |
||
108 | */ |
||
109 | private $_queryOptions = ''; |
||
110 | /** |
||
111 | * @var Connection |
||
112 | */ |
||
113 | protected $connection; |
||
114 | |||
115 | public function getConnection() |
||
116 | { |
||
117 | return $this->connection; |
||
118 | } |
||
119 | |||
120 | /** |
||
121 | * @param $connection \Doctrine\DBAL\Connection |
||
122 | */ |
||
123 | public function setConnection($connection) |
||
124 | { |
||
125 | $this->connection = $connection; |
||
126 | } |
||
127 | |||
128 | /** |
||
129 | * @param Connection $connection |
||
130 | * @param ISQLGenerator|null $adapter |
||
131 | * @param LookupBuilder|null $lookupBuilder |
||
132 | * @return QueryBuilderInterface |
||
133 | */ |
||
134 | public static function getInstance(Connection $connection, $adapter = null, $lookupBuilder = null) |
||
135 | { |
||
136 | if ($adapter === null) { |
||
137 | $driver = $connection->getDriver(); |
||
138 | switch ($driver->getName()) { |
||
139 | case 'pdo_mysql': |
||
140 | $adapter = new MysqlAdapter($connection); |
||
141 | break; |
||
142 | case 'pdo_sqlite': |
||
143 | $adapter = new SqliteAdapter($connection); |
||
144 | break; |
||
145 | case 'pdo_pgsql': |
||
146 | $adapter = new PgsqlAdapter($connection); |
||
147 | break; |
||
148 | default: |
||
149 | throw new QBException('Unknown driver'); |
||
150 | } |
||
151 | } |
||
152 | |||
153 | $lookupBuilder = $lookupBuilder ?: new LookupBuilder(); |
||
154 | $lookupBuilder->addLookupCollection($adapter->getLookupCollection()); |
||
155 | return new static($connection, $adapter, $lookupBuilder); |
||
156 | } |
||
157 | |||
158 | /** |
||
159 | * QueryBuilder constructor. |
||
160 | * @param Connection $connection |
||
161 | * @param ISQLGenerator $adapter |
||
162 | * @param ILookupBuilder $lookupBuilder |
||
163 | */ |
||
164 | public function __construct(Connection $connection, ISQLGenerator $adapter, ILookupBuilder $lookupBuilder) |
||
165 | { |
||
166 | $this->connection = $connection; |
||
167 | $this->adapter = $adapter; |
||
168 | $this->lookupBuilder = $lookupBuilder; |
||
169 | } |
||
170 | |||
171 | /** |
||
172 | * @param ILookupCollection $lookupCollection |
||
173 | * @return QueryBuilderInterface |
||
174 | */ |
||
175 | public function addLookupCollection(ILookupCollection $lookupCollection) |
||
176 | { |
||
177 | $this->lookupBuilder->addLookupCollection($lookupCollection); |
||
178 | return $this; |
||
179 | } |
||
180 | |||
181 | public function setType($type) |
||
182 | { |
||
183 | $types = [static::TYPE_INSERT, static::TYPE_UPDATE, static::TYPE_DELETE, static::TYPE_SELECT]; |
||
184 | if (in_array($type, $types, true)) { |
||
185 | $this->_type = $type; |
||
186 | } |
||
187 | else { |
||
188 | throw new QBException('Incorrect type'); |
||
189 | } |
||
190 | |||
191 | |||
192 | return $this; |
||
193 | } |
||
194 | |||
195 | /** |
||
196 | * @return QueryBuilderInterface |
||
197 | */ |
||
198 | public function setTypeSelect() |
||
199 | { |
||
200 | $this->_type = self::TYPE_SELECT; |
||
201 | return $this; |
||
202 | } |
||
203 | |||
204 | /** |
||
205 | * @return QueryBuilderInterface |
||
206 | */ |
||
207 | public function setTypeInsert() |
||
208 | { |
||
209 | $this->_type = self::TYPE_INSERT; |
||
210 | return $this; |
||
211 | } |
||
212 | |||
213 | /** |
||
214 | * @return QueryBuilderInterface |
||
215 | */ |
||
216 | public function setTypeUpdate() |
||
217 | { |
||
218 | $this->_type = self::TYPE_UPDATE; |
||
219 | return $this; |
||
220 | } |
||
221 | |||
222 | /** |
||
223 | * @return QueryBuilderInterface |
||
224 | */ |
||
225 | public function setTypeDelete() |
||
226 | { |
||
227 | $this->_type = self::TYPE_DELETE; |
||
228 | return $this; |
||
229 | } |
||
230 | |||
231 | /** |
||
232 | * If type is null return TYPE_SELECT |
||
233 | * @return string |
||
234 | */ |
||
235 | public function getType() |
||
236 | { |
||
237 | return $this->_type === null |
||
238 | ? self::TYPE_SELECT |
||
239 | : $this->_type; |
||
240 | } |
||
241 | |||
242 | public function setOptions($options = '') |
||
243 | { |
||
244 | $this->_queryOptions = $options; |
||
245 | return $this; |
||
246 | } |
||
247 | |||
248 | /** |
||
249 | * @param Aggregation $aggregation |
||
250 | * @return string |
||
251 | */ |
||
252 | protected function buildSelectFromAggregation(Aggregation $aggregation) |
||
253 | { |
||
254 | $tableAlias = $this->getAlias(); |
||
255 | $rawColumn = $aggregation->getField(); |
||
256 | $newSelect = $this->getLookupBuilder()->buildJoin($this, $rawColumn); |
||
257 | if ($newSelect === false) { |
||
258 | if ($tableAlias === null || $rawColumn === '*') { |
||
259 | $columns = $rawColumn; |
||
260 | } |
||
261 | elseif (strpos($rawColumn, '.') !== false) { |
||
262 | $columns = $rawColumn; |
||
263 | } |
||
264 | else { |
||
265 | $columns = $tableAlias . '.' . $rawColumn; |
||
266 | } |
||
267 | } |
||
268 | else { |
||
269 | list($alias, $joinColumn) = $newSelect; |
||
270 | $columns = $alias . '.' . $joinColumn; |
||
271 | } |
||
272 | $fieldsSql = $this->getAdapter()->buildColumns($columns); |
||
273 | $aggregation->setFieldSql($fieldsSql); |
||
274 | |||
275 | return $aggregation->setQB($this)->toSQL(); |
||
276 | } |
||
277 | |||
278 | /** |
||
279 | * @return string |
||
280 | * @throws \Exception |
||
281 | */ |
||
282 | protected function buildSelect() |
||
283 | { |
||
284 | if (empty($this->_select)) { |
||
285 | $this->_select = ['*']; |
||
286 | } |
||
287 | |||
288 | $select = []; |
||
289 | $builder = $this->getLookupBuilder(); |
||
290 | if (is_array($this->_select)) { |
||
291 | foreach ($this->_select as $alias => $column) { |
||
292 | if ($column instanceof Aggregation) { |
||
293 | $select[$alias] = $this->buildSelectFromAggregation($column); |
||
294 | } |
||
295 | else if (is_string($column)) { |
||
296 | if (strpos($column, 'SELECT') !== false) { |
||
297 | $select[$alias] = $column; |
||
298 | } |
||
299 | else { |
||
300 | $select[$alias] = $this->addColumnAlias($builder->fetchColumnName($column)); |
||
301 | } |
||
302 | } |
||
303 | else { |
||
304 | $select[$alias] = $column; |
||
305 | } |
||
306 | } |
||
307 | } |
||
308 | else if (is_string($this->_select)) { |
||
309 | $select = $this->addColumnAlias($this->_select); |
||
310 | } |
||
311 | return $this->getAdapter()->sqlSelect($select, $this->_queryOptions); |
||
312 | } |
||
313 | |||
314 | protected function pushToSelect($select, $alias = null) |
||
315 | { |
||
316 | $isValid = is_string($select) |
||
317 | || (is_numeric($select) && is_finite($select)) |
||
318 | || is_a($select, Expression::class) |
||
319 | || is_a($select, Aggregation::class) |
||
320 | ; |
||
321 | |||
322 | if (!$isValid) { |
||
323 | throw new QBException('Incorrect select type'); |
||
324 | } |
||
325 | |||
326 | if ($alias) { |
||
327 | $this->_select[$alias] = $select; |
||
328 | } |
||
329 | else { |
||
330 | $this->_select[] = $select; |
||
331 | } |
||
332 | |||
333 | return $this; |
||
334 | } |
||
335 | |||
336 | /** |
||
337 | * @param string|IToSql $select |
||
338 | * @param null $alias |
||
339 | * @return QueryBuilderInterface |
||
340 | */ |
||
341 | public function addSelect($select, $alias = null) |
||
342 | { |
||
343 | if (is_string($select) && $newSelect = $this->getLookupBuilder()->buildJoin($this, $select)) { |
||
344 | list($t_alias, $column) = $newSelect; |
||
345 | $this->pushToSelect($t_alias . '.' . $column, $alias); |
||
346 | } |
||
347 | else { |
||
348 | $this->pushToSelect( |
||
349 | $this->hydrate($select), |
||
350 | $alias |
||
351 | ); |
||
352 | } |
||
353 | |||
354 | return $this; |
||
355 | } |
||
356 | |||
357 | /** |
||
358 | * @param array|string $select |
||
359 | * @return QueryBuilderInterface |
||
360 | */ |
||
361 | public function setSelect($select) |
||
362 | { |
||
363 | $this->_select = []; |
||
364 | |||
365 | if (empty($select)) { |
||
366 | return $this; |
||
367 | } |
||
368 | |||
369 | if (is_array($select)) { |
||
370 | foreach ($select as $key => $part) { |
||
371 | $this->addSelect($part, $key); |
||
372 | } |
||
373 | } |
||
374 | else { |
||
375 | $this->addSelect($select); |
||
376 | } |
||
377 | |||
378 | return $this; |
||
379 | } |
||
380 | |||
381 | /** |
||
382 | * @param array|string $tableName |
||
383 | * @param null|string $alias |
||
384 | * @return QueryBuilderInterface |
||
385 | */ |
||
386 | public function setFrom($tableName, $alias = null) |
||
387 | { |
||
388 | if ($alias && is_string($alias)) { |
||
389 | if (is_array($tableName)) { |
||
390 | $tableName = current($tableName); |
||
391 | } |
||
392 | |||
393 | $tableName = [$alias => $tableName]; |
||
394 | } |
||
395 | |||
396 | $this->_from = $tableName; |
||
397 | return $this; |
||
398 | } |
||
399 | |||
400 | /** |
||
401 | * @param $alias string join alias |
||
402 | * @return bool |
||
403 | */ |
||
404 | public function hasJoin($alias) |
||
405 | { |
||
406 | return array_key_exists($alias, $this->_join); |
||
407 | } |
||
408 | |||
409 | /** |
||
410 | * @param int $page |
||
411 | * @param int $pageSize |
||
412 | * @return QueryBuilderInterface |
||
413 | */ |
||
414 | public function paginate($page = 1, $pageSize = 10) |
||
415 | { |
||
416 | $page = (int)$page; |
||
417 | $pageSize = (int)$pageSize; |
||
418 | |||
419 | $this->setLimit($pageSize); |
||
420 | $this->setOffset($page > 1 ? $pageSize * ($page - 1) : 0); |
||
421 | return $this; |
||
422 | } |
||
423 | |||
424 | /** |
||
425 | * @param string|number $limit |
||
426 | * @return QueryBuilderInterface |
||
427 | */ |
||
428 | public function setLimit($limit) |
||
429 | { |
||
430 | $this->_limit = (int)$limit; |
||
431 | return $this; |
||
432 | } |
||
433 | |||
434 | /** |
||
435 | * @return int |
||
436 | */ |
||
437 | public function getLimit() |
||
438 | { |
||
439 | return $this->_limit; |
||
440 | } |
||
441 | |||
442 | /** |
||
443 | * @param string|number $offset |
||
444 | * @return QueryBuilderInterface |
||
445 | */ |
||
446 | public function setOffset($offset) |
||
447 | { |
||
448 | $this->_offset = (int)$offset; |
||
449 | return $this; |
||
450 | } |
||
451 | |||
452 | /** |
||
453 | * @return int|string|null |
||
454 | */ |
||
455 | public function getOffset() |
||
456 | { |
||
457 | return $this->_offset; |
||
458 | } |
||
459 | |||
460 | /** |
||
461 | * @return ILookupBuilder|\Tsukasa\QueryBuilder\LookupBuilder\LookupBuilder |
||
462 | */ |
||
463 | public function getLookupBuilder() |
||
464 | { |
||
465 | return $this->lookupBuilder; |
||
466 | } |
||
467 | |||
468 | /** |
||
469 | * @return ISQLGenerator |
||
470 | */ |
||
471 | public function getAdapter() |
||
472 | { |
||
473 | return $this->adapter; |
||
474 | } |
||
475 | |||
476 | /** |
||
477 | * @param string $joinType LEFT JOIN, RIGHT JOIN, etc... |
||
478 | * @param string|QueryBuilderInterface $tableName |
||
479 | * @param array $on link columns |
||
480 | * @param string|null $alias string |
||
481 | * @param string|null $index |
||
482 | * @return QueryBuilderInterface |
||
483 | */ |
||
484 | public function join($joinType, $tableName, array $on = [], $alias = null, $index = null) |
||
485 | { |
||
486 | if ($tableName instanceof QueryBuilderInterface) { |
||
487 | $this->_join[] = $this->getAdapter()->sqlJoin($joinType, $tableName, $on, $alias, $index); |
||
488 | } |
||
489 | else { |
||
490 | if ($joinType === 'RAW' && !empty($tableName)) { |
||
491 | $join = $this->getAdapter()->quoteSql($tableName); |
||
492 | } |
||
493 | else { |
||
494 | $join = $this->getAdapter()->sqlJoin($joinType, $tableName, $on, $alias); |
||
495 | } |
||
496 | |||
497 | if (!$alias) { |
||
498 | $alias = count($this->_join); |
||
499 | } |
||
500 | $this->_join[$alias] = $join; |
||
501 | $this->_joinAlias[$tableName][] = $alias; |
||
502 | } |
||
503 | return $this; |
||
504 | } |
||
505 | |||
506 | /** |
||
507 | * @param $sql |
||
508 | * @param string $alias |
||
509 | * @return QueryBuilderInterface |
||
510 | */ |
||
511 | public function joinRaw($sql, $alias = null) |
||
512 | { |
||
513 | return $this->join('RAW', $sql, [], $alias); |
||
514 | } |
||
515 | |||
516 | /** |
||
517 | * @param array|string $columns columns |
||
518 | * @return QueryBuilderInterface |
||
519 | */ |
||
520 | public function setGroup($columns) |
||
521 | { |
||
522 | if ($columns && is_string($columns)) { |
||
523 | $columns = array_map('trim', explode(',', $columns)); |
||
524 | } |
||
525 | |||
526 | $this->_group = $columns; |
||
527 | return $this; |
||
528 | } |
||
529 | |||
530 | /** |
||
531 | * @param array|string $columns columns |
||
532 | * @return QueryBuilderInterface |
||
533 | */ |
||
534 | public function addGroup($columns) |
||
535 | { |
||
536 | if ($columns && is_string($columns)) { |
||
537 | $columns = array_map('trim', explode(',', $columns)); |
||
538 | } |
||
539 | $this->_group = array_merge($this->_group, $columns); |
||
540 | return $this; |
||
541 | } |
||
542 | |||
543 | protected function pushOrder($column) |
||
544 | { |
||
545 | |||
546 | } |
||
547 | |||
548 | /** |
||
549 | * @param array|string|null $columns columns |
||
550 | * @return QueryBuilderInterface |
||
551 | */ |
||
552 | public function setOrder($columns) |
||
553 | { |
||
554 | |||
555 | $this->_order = []; |
||
556 | |||
557 | if (empty($columns)) { |
||
558 | return $this; |
||
559 | } |
||
560 | |||
561 | if (is_array($columns)) { |
||
562 | foreach ($columns as $column) { |
||
563 | $this->addOrder($column); |
||
564 | } |
||
565 | } |
||
566 | else { |
||
567 | $this->addOrder($columns); |
||
568 | } |
||
569 | |||
570 | return $this; |
||
571 | } |
||
572 | |||
573 | /** |
||
574 | * @param string|Expression $column |
||
575 | * @return QueryBuilderInterface |
||
576 | */ |
||
577 | public function addOrder($column) |
||
578 | { |
||
579 | $isValid = is_string($column) |
||
580 | || is_a($column, Expression::class) |
||
581 | ; |
||
582 | |||
583 | if (!$isValid) { |
||
584 | throw new QBException('Incorrect order type'); |
||
585 | } |
||
586 | |||
587 | if (is_string($column) && strpos($column, ',') !== false) { |
||
588 | $columns = preg_split('/\s*,\s*/', $column, -1, PREG_SPLIT_NO_EMPTY); |
||
589 | |||
590 | foreach ($columns as $_column) { |
||
591 | $temp = explode(' ', $_column); |
||
592 | if (count($temp) === 2) { |
||
593 | |||
594 | $_column = $temp[0]; |
||
595 | if (strtoupper(trim($temp[1])) === 'DESC') { |
||
596 | $_column = '-' . $_column; |
||
597 | } |
||
598 | |||
599 | $this->_order[] = $_column; |
||
600 | } |
||
601 | else { |
||
602 | $this->_order[] = current($temp); |
||
603 | } |
||
604 | } |
||
605 | } |
||
606 | else { |
||
607 | $this->_order[] = $column; |
||
608 | } |
||
609 | |||
610 | return $this; |
||
611 | } |
||
612 | |||
613 | /** |
||
614 | * @param $tableName |
||
615 | * @param array $rows |
||
616 | * @return QueryBuilderInterface |
||
617 | */ |
||
618 | public function insert($tableName, $rows) |
||
619 | { |
||
620 | $this->setTypeInsert(); |
||
621 | $this->_update = [$tableName, $rows]; |
||
622 | return $this; |
||
623 | } |
||
624 | |||
625 | /** |
||
626 | * @param $tableName string |
||
627 | * @param array $values columns [name => value...] |
||
628 | * @return QueryBuilderInterface |
||
629 | */ |
||
630 | public function update($tableName, array $values) |
||
631 | { |
||
632 | $this->setTypeUpdate(); |
||
633 | $this->_update = [$tableName, $values]; |
||
634 | return $this; |
||
635 | } |
||
636 | |||
637 | public function getAlias() |
||
638 | { |
||
639 | return $this->_alias; |
||
640 | } |
||
641 | |||
642 | public function setAlias($alias = null) |
||
643 | { |
||
644 | if (empty($alias)) { |
||
645 | $alias = null; |
||
646 | } |
||
647 | |||
648 | $this->_alias = $alias; |
||
649 | return $this; |
||
650 | } |
||
651 | |||
652 | protected function buildCondition($condition, &$params = []) |
||
653 | { |
||
654 | if (!is_array($condition)) { |
||
655 | return (string)$condition; |
||
656 | } |
||
657 | |||
658 | if (empty($condition)) { |
||
659 | return ''; |
||
660 | } |
||
661 | |||
662 | if (isset($condition[0]) && is_string($condition[0])) { |
||
663 | $operatorRaw = array_shift($condition); |
||
664 | $operator = strtoupper($operatorRaw); |
||
665 | return $this->buildAndCondition($operator, $condition, $params); |
||
666 | } |
||
667 | |||
668 | return $this->parseCondition($condition); |
||
669 | } |
||
670 | |||
671 | public function getJoinAlias($tableName) |
||
672 | { |
||
673 | return $this->_joinAlias[$tableName]; |
||
674 | } |
||
675 | |||
676 | public function getJoins() |
||
677 | { |
||
678 | return $this->_join; |
||
679 | } |
||
680 | |||
681 | /** |
||
682 | * @param $condition |
||
683 | * @param string $operator |
||
684 | * @return string |
||
685 | */ |
||
686 | public function parseCondition($condition, $operator = 'AND') |
||
687 | { |
||
688 | $parts = []; |
||
689 | |||
690 | if (is_array($condition)) { |
||
691 | foreach ($condition as $key => $value) { |
||
692 | if (is_numeric($key)) { |
||
693 | if ($value instanceof IToSql) { |
||
694 | $parts[] = $this->parseCondition($value, $operator); |
||
695 | } |
||
696 | elseif ($value instanceof QueryBuilder) { |
||
697 | $parts[] = $this->parseCondition($value, $operator); |
||
698 | } |
||
699 | else if (is_array($value)) { |
||
700 | $parts[] = $this->parseCondition($value, $operator); |
||
701 | } |
||
702 | else if (is_string($value)) { |
||
703 | $parts[] = $value; |
||
704 | } |
||
705 | } |
||
706 | else { |
||
707 | $tableAlias = $this->getAlias(); |
||
708 | $value = $this->getAdapter()->prepareValue($value); |
||
709 | |||
710 | list($lookup, $column, $lookupValue) = $this->lookupBuilder->parseLookup($this, $key, $value); |
||
711 | $column = $this->getLookupBuilder()->fetchColumnName($column); |
||
712 | if ($tableAlias !== null && strpos($column, '.') === false) { |
||
713 | $column = $tableAlias . '.' . $column; |
||
714 | } |
||
715 | $parts[] = $this->lookupBuilder->runLookup($this->getAdapter(), $lookup, $column, $lookupValue); |
||
716 | } |
||
717 | } |
||
718 | |||
719 | if ($parts) { |
||
720 | if (count($parts) === 1) { |
||
721 | return $parts[0]; |
||
722 | } |
||
723 | |||
724 | return '(' . implode(') ' . $operator . ' (', $parts) . ')'; |
||
725 | } |
||
726 | |||
727 | } |
||
728 | else if ($condition instanceof IToSql) { |
||
729 | return $condition |
||
730 | ->setQb($this) |
||
731 | ->toSql(); |
||
732 | } |
||
733 | else if ($condition instanceof QueryBuilder) { |
||
734 | return $condition->toSQL(); |
||
735 | } |
||
736 | else if (is_string($condition)) { |
||
737 | return $condition; |
||
738 | } |
||
739 | |||
740 | return ''; |
||
741 | } |
||
742 | |||
743 | protected function buildAndCondition($operator, $operands, &$params) |
||
744 | { |
||
745 | $parts = []; |
||
746 | foreach ($operands as $operand) { |
||
747 | if (is_array($operand)) { |
||
748 | $operand = $this->buildCondition($operand, $params); |
||
749 | } |
||
750 | else { |
||
751 | $operand = $this->parseCondition($operand); |
||
752 | } |
||
753 | if ($operand !== '') { |
||
754 | $parts[] = $this->getAdapter()->quoteSql($operand); |
||
755 | } |
||
756 | } |
||
757 | if (!empty($parts)) { |
||
758 | return '(' . implode(') ' . $operator . ' (', $parts) . ')'; |
||
759 | } |
||
760 | |||
761 | return ''; |
||
762 | } |
||
763 | |||
764 | /** |
||
765 | * @param $condition |
||
766 | * @return QueryBuilderInterface |
||
767 | */ |
||
768 | public function addWhere($condition) |
||
769 | { |
||
770 | if (!empty($condition)) { |
||
771 | $this->_whereAnd[] = $this->hydrate($condition); |
||
772 | } |
||
773 | return $this; |
||
774 | } |
||
775 | |||
776 | public function setWhere($condition) |
||
777 | { |
||
778 | $this->_whereAnd = []; |
||
779 | |||
780 | return $this->addWhere($condition); |
||
781 | } |
||
782 | |||
783 | /** |
||
784 | * @param $condition |
||
785 | * @return QueryBuilderInterface |
||
786 | */ |
||
787 | public function addOrWhere($condition) |
||
788 | { |
||
789 | if (!empty($condition)) { |
||
790 | $this->_whereOr[] = $this->hydrate($condition); |
||
791 | } |
||
792 | return $this; |
||
793 | } |
||
794 | |||
795 | public function setOrWhere($condition) |
||
796 | { |
||
797 | $this->_whereOr = []; |
||
798 | |||
799 | return $this->addWhere($condition); |
||
800 | } |
||
801 | |||
802 | /** |
||
803 | * @return array |
||
804 | */ |
||
805 | protected function buildWhereTree() |
||
806 | { |
||
807 | $where = []; |
||
808 | foreach ($this->_whereAnd as $condition) { |
||
809 | if (empty($where)) { |
||
810 | $where = ['and', $condition]; |
||
811 | } |
||
812 | else { |
||
813 | $where = ['and', $where, ['and', $condition]]; |
||
814 | } |
||
815 | } |
||
816 | |||
817 | foreach ($this->_whereOr as $condition) { |
||
818 | if (empty($where)) { |
||
819 | $where = ['or', $condition]; |
||
820 | } |
||
821 | else { |
||
822 | $where = ['or', $where, ['and', $condition]]; |
||
823 | } |
||
824 | } |
||
825 | return $where; |
||
826 | } |
||
827 | |||
828 | public function getSelect() |
||
829 | { |
||
830 | return $this->_select; |
||
831 | } |
||
832 | |||
833 | protected function buildWhere() |
||
834 | { |
||
835 | $params = []; |
||
836 | $sql = $this->buildCondition($this->buildWhereTree(), $params); |
||
837 | return empty($sql) ? '' : ' WHERE ' . $sql; |
||
838 | } |
||
839 | |||
840 | |||
841 | protected function generateSelectSql() |
||
842 | { |
||
843 | // Fetch where conditions before pass it to adapter. |
||
844 | // Reason: Dynamic sql build in callbacks |
||
845 | |||
846 | // $this->prepareJoin(); |
||
847 | |||
848 | $where = $this->buildWhere(); |
||
849 | $order = $this->buildOrder(); |
||
850 | $union = $this->buildUnion(); |
||
851 | |||
852 | $select = $this->buildSelect(); |
||
853 | $from = $this->buildFrom(); |
||
854 | $join = $this->buildJoin(); |
||
855 | $group = $this->buildGroup(); |
||
856 | $having = $this->buildHaving(); |
||
857 | $limitOffset = $this->buildLimitOffset(); |
||
858 | return strtr('{select}{from}{join}{where}{group}{having}{order}{limit_offset}{union}', [ |
||
859 | '{select}' => $select, |
||
860 | '{from}' => $from, |
||
861 | '{where}' => $where, |
||
862 | '{group}' => $group, |
||
863 | '{order}' => empty($union) ? $order : '', |
||
864 | '{having}' => $having, |
||
865 | '{join}' => $join, |
||
866 | '{limit_offset}' => $limitOffset, |
||
867 | '{union}' => empty($union) ? '' : $union . $order |
||
868 | ]); |
||
869 | } |
||
870 | |||
871 | protected function generateDeleteSql() |
||
872 | { |
||
873 | $options = $this->_queryOptions; |
||
874 | if ($options) { |
||
875 | $options = " {$options} "; |
||
876 | } |
||
877 | |||
878 | $limitOffset = $this->buildLimitOffset(); |
||
879 | return strtr('{delete}{options}{from}{where}{limit_offset}', [ |
||
880 | '{delete}' => 'DELETE', |
||
881 | '{options}' => $options, |
||
882 | '{from}' => $this->buildFrom(), |
||
883 | '{where}' => $this->buildWhere(), |
||
884 | '{limit_offset}' => $limitOffset, |
||
885 | ]); |
||
886 | } |
||
887 | |||
888 | protected function generateInsertSql() |
||
889 | { |
||
890 | list($tableName, $values) = $this->_update; |
||
891 | $this->setAlias(); |
||
892 | return $this->getAdapter()->sqlInsert($tableName, $values, $this->_queryOptions); |
||
893 | } |
||
894 | |||
895 | protected function generateUpdateSql() |
||
896 | { |
||
897 | list($tableName, $values) = $this->_update; |
||
898 | $this->setAlias(); |
||
899 | return strtr('{update}{where}', [ |
||
900 | '{update}' => $this->getAdapter()->sqlUpdate($tableName, $values, $this->_queryOptions), |
||
901 | '{where}' => $this->buildWhere(), |
||
902 | ]); |
||
903 | } |
||
904 | |||
905 | /** |
||
906 | * @return string |
||
907 | * @throws QBException |
||
908 | */ |
||
909 | public function toSQL() |
||
910 | { |
||
911 | switch ($this->getType()) |
||
912 | { |
||
913 | case self::TYPE_SELECT: |
||
914 | return $this->generateSelectSql(); |
||
915 | |||
916 | case self::TYPE_INSERT: |
||
917 | return $this->generateInsertSql(); |
||
918 | |||
919 | case self::TYPE_UPDATE: |
||
920 | return $this->generateUpdateSql(); |
||
921 | |||
922 | case self::TYPE_DELETE: |
||
923 | return $this->generateDeleteSql(); |
||
924 | } |
||
925 | |||
926 | throw new QBException('Unknown query type'); |
||
927 | } |
||
928 | |||
929 | protected function buildHaving() |
||
930 | { |
||
931 | return $this->getAdapter()->sqlHaving( |
||
932 | $this->parseCondition($this->_having) |
||
933 | ); |
||
934 | } |
||
935 | |||
936 | protected function buildLimitOffset() |
||
937 | { |
||
938 | return $this->getAdapter()->sqlLimitOffset( |
||
939 | $this->_limit, |
||
940 | $this->_offset |
||
941 | ); |
||
942 | } |
||
943 | |||
944 | protected function buildUnion() |
||
945 | { |
||
946 | $sql = ''; |
||
947 | foreach ($this->_union as list($union, $all)) { |
||
948 | $sql .= ' ' . $this->getAdapter()->sqlUnion($union, $all); |
||
949 | } |
||
950 | |||
951 | return empty($sql) ? '' : $sql; |
||
952 | } |
||
953 | |||
954 | /** |
||
955 | * @param array|string|Q $having lookups |
||
956 | * @return QueryBuilderInterface |
||
957 | */ |
||
958 | public function setHaving($having) |
||
959 | { |
||
960 | $this->_having = []; |
||
961 | |||
962 | return $this->addHaving($having); |
||
963 | } |
||
964 | |||
965 | public function addHaving($having) |
||
966 | { |
||
967 | if (!empty($having)) { |
||
968 | $this->_having[] = $this->hydrate($having); |
||
969 | } |
||
970 | |||
971 | return $this; |
||
972 | } |
||
973 | |||
974 | public function setUnions(array $unions, $all = false) |
||
975 | { |
||
976 | $this->_union = []; |
||
977 | |||
978 | if (!empty($unions)) { |
||
979 | foreach ($unions as $union) { |
||
980 | $this->addUnion($union, $all); |
||
981 | } |
||
982 | } |
||
983 | |||
984 | return $this; |
||
985 | } |
||
986 | |||
987 | public function addUnion($union, $all = false) |
||
988 | { |
||
989 | $this->_union[] = [$union, $all]; |
||
990 | return $this; |
||
991 | } |
||
992 | |||
993 | /** |
||
994 | * Makes alias for joined table |
||
995 | * @param $table |
||
996 | * @param bool $increment |
||
997 | * @return string |
||
998 | */ |
||
999 | public function makeAliasKey($table, $increment = false) |
||
1000 | { |
||
1001 | if ($increment) { |
||
1002 | ++$this->_aliasesCount; |
||
1003 | } |
||
1004 | |||
1005 | $tableName = $this->getAdapter()->getRawTableName($table); |
||
1006 | |||
1007 | if (strpos($tableName, '.') !== false) { |
||
1008 | $tableName = substr($tableName, strpos($tableName, '.') + 1); |
||
1009 | } |
||
1010 | |||
1011 | return strtr('{table}_{count}', [ |
||
1012 | '{table}' => $tableName, |
||
1013 | '{count}' => $this->_aliasesCount + 1 |
||
1014 | ]); |
||
1015 | } |
||
1016 | |||
1017 | /** |
||
1018 | * @param string $table |
||
1019 | * @param string $code |
||
1020 | * @param string $topAlias |
||
1021 | * |
||
1022 | * @return string |
||
1023 | */ |
||
1024 | public function makeMappedAliasKey($table, $code, $topAlias = null) |
||
1025 | { |
||
1026 | $key = $topAlias . '_' . $code; |
||
1027 | |||
1028 | if (empty($this->_joinAlias[$table])) { |
||
1029 | $this->_joinAlias[$table]['__alias_count__'] = 1; |
||
1030 | } |
||
1031 | |||
1032 | if (!empty($this->_joinAlias[$table][$key])) { |
||
1033 | return $this->_joinAlias[$table][$key]; |
||
1034 | } |
||
1035 | |||
1036 | $this->_joinAlias[$table][$key] = strtr('{table}_{count}', [ |
||
1037 | '{table}' => $this->getAdapter()->getRawTableName($table), |
||
1038 | '{count}' => $this->_joinAlias[$table]['__alias_count__'] += 1 |
||
1039 | ]); |
||
1040 | |||
1041 | return $this->_joinAlias[$table][$key]; |
||
1042 | } |
||
1043 | |||
1044 | public function getJoin($tableName) |
||
1045 | { |
||
1046 | return $this->_join[$tableName]; |
||
1047 | } |
||
1048 | |||
1049 | /** |
||
1050 | * @param $column |
||
1051 | * @return string |
||
1052 | */ |
||
1053 | protected function addColumnAlias($column) |
||
1054 | { |
||
1055 | $tableAlias = $this->getAlias(); |
||
1056 | if ($tableAlias === null) { |
||
1057 | return $column; |
||
1058 | } |
||
1059 | |||
1060 | if (strpos($column, '.') === false && |
||
1061 | strpos($column, '(') === false && |
||
1062 | strpos($column, 'SELECT') === false |
||
1063 | ) { |
||
1064 | return $tableAlias . '.' . $column; |
||
1065 | } |
||
1066 | |||
1067 | return $column; |
||
1068 | } |
||
1069 | |||
1070 | protected function hasAliasedField($column) |
||
1071 | { |
||
1072 | foreach ($this->_select as $alias => $item) |
||
1073 | { |
||
1074 | if (!is_numeric($alias) && $column === $alias) { |
||
1075 | return true; |
||
1076 | } |
||
1077 | } |
||
1078 | |||
1079 | return false; |
||
1080 | } |
||
1081 | |||
1082 | protected function applyTableAlias($column) |
||
1083 | { |
||
1084 | // If column already has alias - skip |
||
1085 | if ((strpos($column, '.') === false) && !$this->hasAliasedField($column)) { |
||
1086 | $tableAlias = $this->getAlias(); |
||
1087 | return $tableAlias === null ? $column : $tableAlias . '.' . $column; |
||
1088 | } |
||
1089 | |||
1090 | return $column; |
||
1091 | } |
||
1092 | |||
1093 | protected function buildJoin() |
||
1094 | { |
||
1095 | if (empty($this->_join)) { |
||
1096 | return ''; |
||
1097 | } |
||
1098 | $join = []; |
||
1099 | foreach ($this->_join as $part) { |
||
1100 | $join[] = $part; |
||
1101 | } |
||
1102 | return ' ' . implode(' ', $join); |
||
1103 | } |
||
1104 | |||
1105 | |||
1106 | /** |
||
1107 | * @param $order |
||
1108 | * @return array |
||
1109 | */ |
||
1110 | protected function buildOrderJoin($order) |
||
1111 | { |
||
1112 | if (strpos($order, '-') === false) { |
||
1113 | $direction = 'ASC'; |
||
1114 | } |
||
1115 | else { |
||
1116 | $direction = 'DESC'; |
||
1117 | $order = substr($order, 1); |
||
1118 | } |
||
1119 | $order = $this->getLookupBuilder()->fetchColumnName($order); |
||
1120 | $newOrder = $this->getLookupBuilder()->buildJoin($this, $order); |
||
1121 | if ($newOrder === false) { |
||
1122 | return [$order, $direction]; |
||
1123 | } |
||
1124 | |||
1125 | list($alias, $column) = $newOrder; |
||
1126 | return [$alias . '.' . $column, $direction]; |
||
1127 | } |
||
1128 | |||
1129 | public function getOrder() |
||
1130 | { |
||
1131 | return $this->_order; |
||
1132 | } |
||
1133 | |||
1134 | protected function buildOrder() |
||
1135 | { |
||
1136 | /** |
||
1137 | * не делать проверку по empty(), проваливается половина тестов с ORDER BY |
||
1138 | * и проваливается тест с построением JOIN по lookup |
||
1139 | */ |
||
1140 | if ($this->_order === null) { |
||
1141 | return ''; |
||
1142 | } |
||
1143 | |||
1144 | $order = []; |
||
1145 | if (is_array($this->_order)) { |
||
1146 | foreach ($this->_order as $column) { |
||
1147 | if ($column instanceof Expression) { |
||
1148 | $order[$column->toSQL()] = ''; |
||
1149 | } |
||
1150 | else if ($column === '?') { |
||
1151 | $order[] = $this->getAdapter()->getRandomOrder(); |
||
1152 | } |
||
1153 | else { |
||
1154 | list($newColumn, $direction) = $this->buildOrderJoin($column); |
||
1155 | $order[$this->applyTableAlias($newColumn)] = $direction; |
||
1156 | } |
||
1157 | } |
||
1158 | } |
||
1159 | else { |
||
1160 | $order[] = $this->buildOrderJoin($this->_order); |
||
1161 | } |
||
1162 | |||
1163 | $sql = $this->getAdapter()->sqlOrderBy($order); |
||
1164 | return empty($sql) ? '' : ' ORDER BY ' . $sql; |
||
1165 | } |
||
1166 | |||
1167 | /** |
||
1168 | * @param $group |
||
1169 | * @return string |
||
1170 | */ |
||
1171 | protected function buildGroupJoin($group) |
||
1172 | { |
||
1173 | if (strpos($group, '.') === false) { |
||
1174 | $newGroup = $this->getLookupBuilder()->fetchColumnName($group); |
||
1175 | $newGroup = $this->getLookupBuilder()->buildJoin($this, $newGroup); |
||
1176 | |||
1177 | if ($newGroup === false) { |
||
1178 | return $group; |
||
1179 | } |
||
1180 | |||
1181 | list($alias, $column) = $newGroup; |
||
1182 | return $alias . '.' . $column; |
||
1183 | } |
||
1184 | |||
1185 | return $group; |
||
1186 | } |
||
1187 | |||
1188 | protected function buildGroup() |
||
1189 | { |
||
1190 | $group = []; |
||
1191 | if ($this->_group) { |
||
0 ignored issues
–
show
|
|||
1192 | foreach ($this->_group as $key => $column) { |
||
1193 | $newColumn = $this->buildGroupJoin($column); |
||
1194 | $group[] = $this->applyTableAlias($newColumn); |
||
1195 | } |
||
1196 | } |
||
1197 | |||
1198 | $sql = $this->getAdapter()->sqlGroupBy($group); |
||
1199 | return empty($sql) ? '' : ' GROUP BY ' . $sql; |
||
1200 | } |
||
1201 | |||
1202 | protected function buildFrom() |
||
1203 | { |
||
1204 | if ($this->_alias !== null && !is_array($this->_from)) { |
||
1205 | $from = [$this->_alias => $this->_from]; |
||
1206 | } |
||
1207 | else { |
||
1208 | $from = $this->_from; |
||
1209 | } |
||
1210 | $sql = $this->getAdapter()->sqlFrom($from); |
||
1211 | return empty($sql) ? '' : ' FROM ' . $sql; |
||
1212 | } |
||
1213 | |||
1214 | protected function hydrate($val) |
||
1215 | { |
||
1216 | if (is_object($val)) { |
||
1217 | if ($val instanceof IToSql) { |
||
1218 | $val->setQb($this); |
||
1219 | } |
||
1220 | } |
||
1221 | |||
1222 | return $val; |
||
1223 | } |
||
1224 | } |
||
1225 |
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.