1<?php 2 3namespace go\core\db; 4 5use Exception; 6use go\core\db\Column; 7use go\core\db\Criteria; 8use go\core\util\ArrayUtil; 9 10/** 11 * QueryBuilder 12 * 13 * Builds or executes an SQL string with a {@see Query} object anmd {@see AbstractRecord} 14 * 15 * @copyright (c) 2015, Intermesh BV http://www.intermesh.nl 16 * @author Merijn Schering <mschering@intermesh.nl> 17 * @license http://www.gnu.org/licenses/agpl-3.0.html AGPLv3 18 */ 19class QueryBuilder { 20 21 22 23 /** 24 * 25 * @var Query 26 */ 27 private $query; 28 29 /** 30 * The main table name 31 * 32 * @var string 33 */ 34 protected $tableName; 35 36 /** 37 * The main table alias 38 * 39 * @var string 40 */ 41 protected $tableAlias; 42 43 /** 44 * Key value array of parameters to bind to the SQL Statement 45 * 46 * Query::where() parameters will be put in here to bind. 47 * 48 * @var array[] 49 */ 50 private $buildBindParameters = []; 51 52 /** 53 * To generate unique param tags for binding 54 * 55 * @var int 56 */ 57 private static $paramCount = 0; 58 59 /** 60 * Prefix of the bind parameter tag 61 * @var type 62 */ 63 private static $paramPrefix = ':go'; 64 65 /** 66 * Key value array with [tableAlias => Table()] 67 * 68 * Used to find the model that belongs to an alias to find column types 69 * @var Table[] 70 */ 71 protected $aliasMap = []; 72 73 /** 74 * 75 * @var Table 76 */ 77 private $table; 78 79 80 /** 81 * @var Connection 82 */ 83 private $conn; 84 85 public function __construct(Connection $conn) { 86 $this->conn = $conn; 87 } 88 89 90 /** 91 * Constructor 92 * 93 * @param string $tableName The table to operate on 94 * @throws Exception 95 */ 96 public function setTableName($tableName) { 97 98 if(!isset($tableName)) { 99 throw new \Exception("No from() table set for the select query"); 100 101 } 102 $this->tableName = $tableName; 103 $this->table = Table::getInstance($tableName, $this->conn); 104 } 105 106 /** 107 * Used when building sub queries for when aliases of the main query are used 108 * in the subquery. 109 * 110 * @param array $aliasMap 111 */ 112 public function mergeAliasMap($aliasMap) { 113 $this->aliasMap = array_merge($this->aliasMap, $aliasMap); 114 } 115 116 /** 117 * Get the query parameters 118 * 119 * @return Query 120 */ 121 public function getQuery() { 122 return $this->query; 123 } 124 125 /** 126 * Get the name of the record this query builder is for. 127 * 128 * @return string 129 */ 130 public function getTableName() { 131 return $this->tableName; 132 } 133 134 /** 135 * @param $tableName 136 * @param $data 137 * @param array $columns 138 * @param string $command 139 * @return array 140 * @throws Exception 141 */ 142 public function buildInsert($tableName, $data, $columns = [], $command = "INSERT") { 143 144 $this->reset(); 145 $this->setTableName($tableName); 146 $this->aliasMap[$tableName] = Table::getInstance($this->tableName, $this->conn); 147 148 $sql = $command . " "; 149 150 $sql .= "INTO `{$this->tableName}` "; 151 152 if ($data instanceof \go\core\db\Query) { 153 if(!empty($columns)) { 154 $sql .= " (`" . implode("`, `", $columns ) . "`)\n"; 155 } 156 157 $build = $data->build(); 158 159 $sql .= ' ' . $build['sql']; 160 $this->buildBindParameters = array_merge($this->buildBindParameters, $build['params']); 161 } else { 162 if(ArrayUtil::isAssociative($data)) { 163 $data = [$data]; 164 } 165 if(empty($columns)) { 166 reset($data); 167 $columns = array_keys(current($data)); 168 } 169 $sql .= " (\n\t`" . implode("`,\n\t`", $columns) . "`\n)\n" . 170 "VALUES \n"; 171 172 foreach($data as $record) { 173 $tags = []; 174 foreach ($record as $colName => $value) { 175 if(is_int($colName)) { 176 $colName = $columns[$colName]; 177 } 178 179 if($value instanceof Expression) { 180 $tags[] = (string) $value; 181 } else 182 { 183 $paramTag = $this->getParamTag(); 184 $tags[] = $paramTag; 185 $this->addBuildBindParameter($paramTag, $value, $this->tableName, $colName); 186 } 187 } 188 189 $sql .= "(\n\t" . implode(",\n\t", $tags) . "\n), "; 190 } 191 192 $sql = substr($sql, 0, -2); //strip off last ', ' 193 } 194 195 return ['sql' => $sql, 'params' => $this->buildBindParameters]; 196 } 197 198 public function buildUpdate($tableName, $data, Query $query, $command = "UPDATE") { 199 200 $this->reset(); 201 202 $this->setTableName($tableName); 203 204 $this->query = $query; 205 $this->buildBindParameters = $query->getBindParameters(); 206 $this->tableAlias = $this->query->getTableAlias(); 207 $this->aliasMap[$this->tableAlias] = Table::getInstance($this->tableName, $this->conn); 208 209 if (is_array($data)) { 210 $updates = []; 211 foreach ($data as $colName => $value) { 212 213 $tableAndCol = $this->splitTableAndColumn($colName); 214 $colName = '`' . $tableAndCol[0] .'`.`'.$tableAndCol[1].'`'; 215 if($value instanceof Expression) { 216 $updates[] = $colName . ' = ' . $value; 217 } elseif($value instanceof Query) { 218 $build = $value->build(); 219 220 $updates[] = $colName . ' = (' . $build['sql'] .')'; 221 222 $this->buildBindParameters = array_merge($this->buildBindParameters, $build['params']); 223 } else 224 { 225 $paramTag = $this->getParamTag(); 226 $updates[] = $colName . ' = ' . $paramTag; 227 $this->addBuildBindParameter($paramTag, $value, $tableAndCol[0], $tableAndCol[1]); 228 } 229 } 230 $set = implode(",\n\t", $updates); 231 } else if ($data instanceof Expression) { 232 $set = (string) $data; 233 } 234 235 $sql = $command . " `{$this->tableName}` `" . $this->tableAlias . "`"; 236 237 foreach ($this->query->getJoins() as $join) { 238 $sql .= "\n" . $this->join($join, ""); 239 } 240 241 $sql .= "\nSET\n\t" . $set; 242 243 $where = $this->buildWhere($this->getQuery()->getWhere()); 244 245 if (!empty($where)) { 246 $sql .= "\nWHERE " . $where; 247 } 248 249 return ['sql' => $sql, 'params' => $this->buildBindParameters]; 250 } 251 252 public function buildDelete($tableName, Query $query) { 253 254 $this->setTableName($tableName); 255 $this->reset(); 256 $this->query = $query; 257 $this->buildBindParameters = $query->getBindParameters(); 258 $this->tableAlias = $this->query->getTableAlias(); 259 $this->aliasMap[$this->tableAlias] = Table::getInstance($this->tableName, $this->conn); 260 261 $sql = "DELETE FROM `" . $this->tableAlias . "` USING `" . $this->tableName . "` AS `" . $this->tableAlias . "` "; 262 263 foreach ($this->query->getJoins() as $join) { 264 $sql .= "\n" . $this->join($join, ""); 265 } 266 267 $where = $this->buildWhere($this->getQuery()->getWhere()); 268 269 if (!empty($where)) { 270 $sql .= "\nWHERE " . $where; 271 } 272 273 return ['sql' => $sql, 'params' => $this->buildBindParameters]; 274 } 275 276 private function reset() { 277 $this->query = null; 278 $this->buildBindParameters = []; 279 $this->aliasMap = []; 280 } 281 282 /** 283 * Build the select SQL and params 284 */ 285 public function buildSelect(Query $query = null, $prefix = "") { 286 287 $unions = $query->getUnions(); 288 289 $r = $this->internalBuildSelect($query, empty($unions) ? $prefix : $prefix . "\t"); 290 291 $unions = $query->getUnions(); 292 if(empty($unions)) { 293 return $r; 294 } 295 296 $r['sql'] = "(\n" . $r['sql']; 297 298 foreach($unions as $q) { 299 $u = $this->internalBuildSelect($q, "\t"); 300 $r['sql'] .= "\n) UNION (\n" . $u['sql']; 301 $r['params'] = array_merge($r['params'], $u['params']); 302 } 303 304 $r['sql'] .= "\n)"; 305 306 //reset to the main query object 307 $this->query = $query; 308 // Unions can't have aliases in the global scope 309 $this->aliasMap = []; 310 311 $orderBy = $this->buildOrderBy(true); 312 if(!empty($orderBy)) { 313 $r['sql'] .= "\n" . $orderBy; 314 } 315 316 if ($query->getUnionLimit() > 0) { 317 $r['sql'] .= "\nLIMIT " . $query->getUnionOffset() . ',' . $query->getUnionLimit(); 318 } 319 320 return $r; 321 322 } 323 324 protected function internalBuildSelect(Query $query, $prefix = '') { 325 $this->reset(); 326 $this->setTableName($query->getFrom()); 327 $this->tableAlias = $query->getTableAlias(); 328 $this->query = $query; 329 $this->buildBindParameters = $query->getBindParameters(); 330 331 $this->aliasMap[$this->tableAlias] = Table::getInstance($this->tableName, $this->conn); 332 333 $joins = ""; 334 foreach ($this->query->getJoins() as $join) { 335 $joins .= "\n" . $prefix . $this->join($join, $prefix); 336 } 337 338 $select = $prefix . $this->buildSelectFields(); 339 $select .= "\n" . $prefix . "FROM `" . $this->tableName . '`'; 340 341 if(isset($this->tableAlias) && $this->tableAlias != $this->tableName) { 342 $select .= ' `' . $this->tableAlias . "`"; 343 } 344 345 $where = $this->buildWhere($this->query->getWhere(), $prefix); 346 347 if (!empty($where)) { 348 $where = "\n" . $prefix . "WHERE " . $where; 349 } 350 $group = $this->buildGroupBy(); 351 if(!empty($group)) { 352 $group = "\n" . $prefix . $group; 353 } 354 355 $having = $this->buildHaving(); 356 if(!empty($having)) { 357 $having = "\n" . $prefix . $having; 358 } 359 $orderBy = $this->buildOrderBy(); 360 if(!empty($orderBy)) { 361 $orderBy = "\n" . $prefix . $orderBy; 362 } 363 364 $limit = ""; 365 if ($this->query->getLimit() > 0) { 366 $limit .= "\n" . $prefix . "LIMIT " . $this->query->getOffset() . ',' . $this->query->getLimit(); 367 } 368 369 $sql = $select . $joins . $where . $group . $having . $orderBy . $limit; 370 371 if ($this->query->getForUpdate()) { 372 $sql .= "\n" . $prefix . "FOR UPDATE"; 373 } 374 375 return ['sql' => $sql, 'params' => $this->buildBindParameters]; 376 } 377 378 /** 379 * Will replace all :paramName tags with the values. Used for debugging the SQL string. 380 * 381 * @param array $build 382 * @param string 383 */ 384 public static function debugBuild($build) { 385 $sql = $build['sql']; 386 $binds = []; 387 if(isset($build['params'])) { 388 foreach ($build['params'] as $p) { 389 if (is_string($p['value']) && !mb_check_encoding($p['value'], 'utf8')) { 390 $queryValue = "[NON UTF8 VALUE]"; 391 } else { 392 $queryValue = var_export($p['value'], true); 393 } 394 $binds[$p['paramTag']] = $queryValue; 395 } 396 } 397 398 //sort so $binds :param1 does not replace :param11 first. 399 krsort($binds); 400 401 foreach ($binds as $tag => $value) { 402 $sql = str_replace($tag, $value, $sql); 403 } 404 405 return $sql; 406 } 407 408 protected function buildSelectFields() { 409 $select = "SELECT "; 410 411 if ($this->query->getCalcFoundRows()) { 412 $select .= "SQL_CALC_FOUND_ROWS "; 413 } 414 415 if ($this->query->getDistinct()) { 416 $select .= "DISTINCT "; 417 } 418 419 $s = $this->query->getSelect(); 420 if (!empty($s)) { 421 $select .= implode(', ', $s); 422 } else { 423 $select .= '*'; 424 } 425 426 return $select . ' '; 427 } 428 429 /** 430 * 431 * @param string $tableAlias 432 * @param string $column 433 * @return Column 434 * @throws Exception 435 */ 436 private function findColumn($tableAlias, $column) { 437 438 if (!isset($this->aliasMap[$tableAlias])) { 439 440// var_dump($this); 441 throw new Exception("Alias '" . $tableAlias . "' not found in the aliasMap for " . $column); 442 } 443 444 if ($this->aliasMap[$tableAlias]->getColumn($column) == null) { 445 throw new Exception("Column '" . $column . "' not found in table " . $this->aliasMap[$tableAlias]->getName()); 446 } 447 return $this->aliasMap[$tableAlias]->getColumn($column); 448 } 449 450 private function buildGroupBy() { 451 452 $qroupBy = $this->query->getGroupBy(); 453 454 if (empty($qroupBy)) { 455 return ''; 456 } 457 458 $groupBy = "GROUP BY "; 459 460 foreach (array_unique($qroupBy) as $column) { 461 if ($column instanceof Expression) { 462 $groupBy .= $column . ', '; 463 } else { 464 $groupBy .= $this->quoteTableAndColumnName($column) . ', '; 465 } 466 } 467 468 $groupBy = trim($groupBy, ' ,'); 469 470 return $groupBy . "\n"; 471 } 472 //Clear first AND or OR. Other wise WHERE AND ... will be generated 473 private $firstWhereCondition = true; 474 475 /** 476 * Build the where part of the SQL string 477 * 478 * @param \go\core\db\Criteria $query 479 * @param string $prefix Simple string prefix not really functional but only used to add some tab spaces to the SQL string for pretty formatting. 480 * @param string 481 */ 482 protected function buildWhere(array $conditions, $prefix = "") { 483 $this->firstWhereCondition = true; 484 485 $where = ""; 486 foreach ($conditions as $condition) { 487 $str = $this->buildCondition($condition, $prefix); 488 $where .= "\n" . $prefix . $str; 489 } 490 491 return rtrim($where); 492 } 493 494 /** 495 * Convert where condition to string 496 * 497 * {@see Criteria::where()} 498 * 499 * 500 * @param string|array|Criteria $condition 501 * @param string $prefix 502 * @return string 503 * @throws Exception 504 */ 505 private function buildCondition($condition, $prefix = "") { 506 507 switch ($condition[0]) { 508 case 'column': 509 return $this->buildColumn($condition, $prefix); 510 default: 511 array_shift($condition); 512 return $this->buildTokens($condition, $prefix); 513 } 514 } 515 516 /** 517 * Tokens is always: 518 * 519 * return ["tokens", AND/OR, string/expression/query/criteria]; 520 * 521 * @param type $tokens 522 * @param type $prefix 523 * @return string 524 */ 525 private function buildTokens($tokens, $prefix) { 526 $str = ""; 527 528 if(stripos($tokens[0], "NOT_OR_NULL") !== false) { 529 $tokens[0] = str_replace('NOT_OR_NULL', 'NOT IFNULL(', $tokens[0]); 530 $tokens[] = ', false)'; 531 } 532 533 if($this->firstWhereCondition) { 534 //clear first AND/OR to avoid WHERE AND to be generated 535 $tokens[0] = str_ireplace(['AND', 'OR'], '', $tokens[0]); 536 $this->firstWhereCondition = false; 537 } 538 539 foreach ($tokens as $token) { 540 $str .= $this->buildToken($token, $prefix) . " "; 541 } 542 543 return $str; 544 } 545 546 private function buildToken($token, $prefix) { 547 if (is_string($token)) { 548 return $token; 549 } else { 550 if($token instanceof Expression) { 551 return (string) $token; 552 } 553 554 if($token instanceof Query) { 555 return $this->buildSubQuery($token, $prefix); 556 } 557 558 if($token instanceof Criteria) { 559 $this->buildBindParameters = array_merge($this->buildBindParameters, $token->getBindParameters()); 560 $where = $this->buildWhere($token->getWhere(), $prefix . "\t"); 561 562 return "(" . $where . "\n" . $prefix . ")"; 563 } 564 565 throw new \Exception("Invalid token?"); 566 } 567 } 568 569 private function buildColumnArrayValue($logicalOperator, $columnName, $comparisonOperator, $value) { 570 //If the value is an array and it's not an IN query we do: 571 // (foo like array1 OR foo like array2) 572 573 if(empty($value)) { 574 return ""; 575 } 576 577 if ($this->firstWhereCondition) { 578 //Clear first AND or OR. Other wise WHERE AND ... will be generated 579 $this->firstWhereCondition = false; 580 $logicalOperator = stripos($logicalOperator, "NOT") !== false ? "NOT" : ""; 581 } 582 583 $str = $logicalOperator . " ("; 584 585 for($i = 0, $c = count($value); $i < $c; $i++) { 586 $str .= $this->buildColumn([null, $i == 0 ? "" : "OR", $columnName, $comparisonOperator, $value[$i]], ""); 587 } 588 589 return $str .= ")"; 590 } 591 592 private function buildColumn($condition, $prefix) { 593 594 list(, $logicalOperator, $columnName, $comparisonOperator, $value) = $condition; 595 596 597 if(is_array($value) && $comparisonOperator != "=" && stripos($comparisonOperator, 'IN') === false) { 598 //single array value can be simplified and handled like non array value 599 if(count($value) == 1) { 600 $value = $value[0]; 601 } else 602 { 603 return $prefix . $this->buildColumnArrayValue($logicalOperator, $columnName, $comparisonOperator, $value); 604 } 605 } 606 607 if ($this->firstWhereCondition) { 608 //Clear first AND or OR. Other wise WHERE AND ... will be generated 609 $this->firstWhereCondition = false; 610 $logicalOperator = stripos($logicalOperator, "NOT") !== false ? "NOT" : ""; 611 } 612 613 $tokens = [$logicalOperator]; //AND / OR 614 615 $columnParts = $this->splitTableAndColumn($columnName); 616 617 if (empty($columnParts[0])) { 618 $tables = []; 619 foreach($this->aliasMap as $table) { 620 $tables[] = $table->getName(); 621 } 622 throw new \Exception("Invalid column name '" . $columnName . "'. Not a column of any table: ".implode(', ', $tables)); 623 } 624 625 $tokens[] = $this->quoteTableName($columnParts[0]) . '.' . $this->quoteColumnName($columnParts[1]); //column name 626 627 if (!isset($value)) { 628 if ($comparisonOperator == '=' || $comparisonOperator == 'IS') { 629 $tokens[] = "IS NULL"; 630 } elseif ($comparisonOperator == '!=' || $comparisonOperator == 'IS NOT') { 631 $tokens[] = "IS NOT NULL"; 632 } else { 633 throw new Exception('Null value not possible with comparator: ' . $comparisonOperator); 634 } 635 } else if (is_array($value)) { 636 $tokens[] = $comparisonOperator == "=" ? "IN" : $comparisonOperator; 637 $tokens[] = $this->buildInValues($columnParts, $value); 638 } else if ($value instanceof \go\core\db\Query) { 639 $tokens[] = $comparisonOperator; 640 $tokens[] = $value; 641 } else { 642 $paramTag = $this->getParamTag(); 643 644 $this->addBuildBindParameter($paramTag, $value, $columnParts[0], $columnParts[1]); 645 646 $tokens[] = $comparisonOperator; 647 $tokens[] = $paramTag; 648 } 649 650 return $this->buildTokens($tokens, $prefix); 651 } 652 653 private function buildSubQuery(\go\core\db\Query $query, $prefix) { 654 655 //subquery 656 if ($query->getTableAlias() == 't' && $this->getQuery()->getTableAlias() == 't') { 657 $query->tableAlias('sub'); 658 } 659 660 $builder = new QueryBuilder($this->conn); 661 $builder->aliasMap = $this->aliasMap; 662 663 $build = $builder->buildSelect($query, $prefix . "\t"); 664 665 $str = "(\n" . $prefix . $build['sql'] . "\n" . $prefix . ")"; 666 667 $this->buildBindParameters = array_merge($this->buildBindParameters, $build['params']); 668 669 return $str; 670 } 671 672 private function splitTableAndColumn($tableAndCol) { 673 $dot = strpos($tableAndCol, '.'); 674 675 if ($dot !== false) { 676 $column = substr($tableAndCol, $dot + 1); 677 $alias = substr($tableAndCol, 0, $dot); 678 return [trim($alias, ' `'), trim($column, ' `')]; 679 } else { 680 $colName = trim($tableAndCol, ' `'); 681 682 //if column not found then don'use an alias. It could be an alias defined in the select part or a function. 683 $alias = null; 684 685 //find table for column 686 foreach($this->aliasMap as $tableAlias => $table) { 687 $columnObject = $table->getColumn($colName); 688 if ($columnObject) { 689 $alias = $tableAlias; 690 break; 691 } 692 } 693 694 return [$alias, $colName]; 695 } 696 } 697 698 /** 699 * Put's quotes around the table name and checks for injections 700 * 701 * @param string $tableName 702 * @param string 703 * @throws Exception 704 */ 705 protected function quoteTableName($tableName) { 706 return Utils::quoteTableName($tableName); 707 } 708 709 /** 710 * Quotes a column name for use in a query. 711 * If the column name contains prefix, the prefix will also be properly quoted. 712 * If the column name is already quoted or contains '(', '[[' or '{{', 713 * then this method will do nothing. 714 * 715 * @param string $columnName column name 716 * @param string the properly quoted column name 717 */ 718 protected function quoteColumnName($columnName) { 719 return $this->quoteTableName($columnName); 720 } 721 722 /** 723 * Splits table and column on the . separator and quotes them both. 724 * 725 * @param string $columnName 726 * @param string 727 */ 728 protected function quoteTableAndColumnName($columnName) { 729 730 $parts = $this->splitTableAndColumn($columnName); 731 732 if (isset($parts[0])) { 733 return $this->quoteTableName($parts[0]) . '.' . $this->quoteColumnName($parts[1]); 734 } else { 735 return $this->quoteColumnName($parts[1]); 736 } 737 } 738 739 private function buildInValues($columnParts, $values) { 740 741 if (empty($values)) { 742 throw new \Exception("IN condition can not be empty!"); 743 } 744 745 $str = "("; 746 747 foreach ($values as $value) { 748 $paramTag = $this->getParamTag(); 749 $this->addBuildBindParameter($paramTag, $value, $columnParts[0], $columnParts[1]); 750 751 $str .= $paramTag . ', '; 752 } 753 754 $str = rtrim($str, ', ') . ")"; 755 756 return $str; 757 } 758 759 private function buildOrderBy($forUnion = false) { 760 $oBy = $forUnion ? $this->query->getUnionOrderBy() : $this->query->getOrderBy(); 761 if (empty($oBy)) { 762 return ''; 763 } 764 765 $orderBy = "ORDER BY "; 766 767 foreach ($oBy as $column => $direction) { 768 769 if ($direction instanceof Expression) { 770 $orderBy .= $direction . ', '; 771 } else { 772 $direction = strtoupper($direction) === 'DESC' ? 'DESC' : 'ASC'; 773 $orderBy .= $this->quoteTableAndColumnName($column) . ' ' . $direction . ', '; 774 } 775 } 776 777 return trim($orderBy, ' ,'); 778 } 779 780 private function buildHaving() { 781 782 $h = $this->query->getHaving(); 783 if (empty($h)) { 784 return ''; 785 } 786 787 return "HAVING" . $this->buildWhere($h); 788 } 789 790 private function addBuildBindParameter($paramTag, $value, $tableAlias, $columnName) { 791 792 $columnObj = $this->findColumn($tableAlias, $columnName); 793 794 $this->buildBindParameters[] = [ 795 'paramTag' => $paramTag, 796 'value' => $columnObj->castToDb($value), 797 'pdoType' => $columnObj->pdoType 798 ]; 799 } 800 801 /** 802 * Private function to get the current parameter prefix. 803 * 804 * @param string The next available parameter prefix. 805 */ 806 private function getParamTag() { 807 self::$paramCount++; 808 return self::$paramPrefix . self::$paramCount; 809 } 810 811 private function join($config, $prefix) { 812 $join = ""; 813 814 if ($config['src'] instanceof \go\core\db\Query) { 815 $builder = new QueryBuilder($this->conn); 816 $builder->aliasMap = $this->aliasMap; 817 818 $build = $builder->buildSelect($config['src'], $prefix . "\t"); 819 $joinTableName = "(\n" . $prefix . "\t" . $build['sql'] . "\n" . $prefix . ')'; 820 821 $this->buildBindParameters = array_merge($build['params']); 822 } else { 823 $this->aliasMap[$config['joinTableAlias']] = Table::getInstance($config['src'], $this->query->getDbConnection()); 824 $joinTableName = '`' . $config['src'] . '`'; 825 } 826 827 $join .= $config['type'] . ' JOIN ' . $joinTableName . ' '; 828 829 if (!empty($config['joinTableAlias'])) { 830 $join .= '`' . $config['joinTableAlias'] . '` '; 831 } 832 833 834 //import new params 835 $this->buildBindParameters = array_merge($this->buildBindParameters, $config['on']->getBindParameters()); 836 $join .= 'ON ' . $this->buildWhere($config['on']->getWhere(), $prefix); 837 838 return $join; 839 } 840 841} 842