or section. */ public $alias; public $join_type; public $join_conditions = array(); /** * Attributes for subqueries in the section. */ public $operation; public $column; public $pipe; /** * Attributes used during query string generation. */ protected $_prefix = ''; protected $_args = array(); protected $_column_list = array(); protected $_params = array(); /** * Constants for alias handling. */ const ALIAS_NONE = 0; const ALIAS_SPECIFIED = 1; const ALIAS_ALWAYS = 2; /** * Generate the query string for this query. * * @param string $prefix * @param array $args * @param array $column_list * @param int $count_only * @return string */ public function getQueryString(string $prefix = '', array $args = [], array $column_list = [], int $count_only = 0): string { // Save the query information. $this->_prefix = $prefix; $this->_args = $args; $this->_column_list = $column_list; $this->_params = array(); // Call different internal methods depending on the query type. switch ($this->type) { case 'SELECT': $result = $this->_getSelectQueryString($count_only); break; case 'INSERT': $result = $this->_getInsertQueryString(); break; case 'UPDATE': $result = $this->_getUpdateQueryString(); break; case 'DELETE': $result = $this->_getDeleteQueryString(); break; default: $result = ''; } // Reset state and return the result. $this->_prefix = ''; $this->_args = array(); $this->_column_list = array(); return $result; } /** * Get the query parameters to use with the query string generated above. * * @return array */ public function getQueryParams(): array { return $this->_params; } /** * Check if this query requires pagination. * * @return bool */ public function requiresPagination(): bool { return $this->requires_pagination; } /** * Generate a SELECT query string. * * @param int $count_only * @return string */ protected function _getSelectQueryString(int $count_only = 0): string { // Initialize the query string. $result = 'SELECT'; $has_subquery_columns = false; // Compose the column list. if ($this->_column_list) { $column_list = implode(', ', array_map(function($str) { return self::quoteName($str); }, $this->_column_list)); } else { $columns = array(); foreach ($this->columns as $column) { if ($column->ifvar && empty($this->_args[$column->ifvar])) { continue; } elseif ($column instanceof self) { $has_subquery_columns = true; $subquery_count_only = $count_only ? $count_only + 1 : 0; $subquery = $column->getQueryString($this->_prefix, $this->_args, [], $subquery_count_only); foreach ($column->getQueryParams() as $param) { $this->_params[] = $param; } $columns[] = sprintf('(%s) AS %s', $subquery, self::quoteName($column->alias)); } elseif ($column->is_expression && !$column->is_wildcard) { $columns[] = $column->name . ($column->alias ? (' AS ' . self::quoteName($column->alias)) : ''); } elseif ($column->is_wildcard && $count_only >= 1 && !$this->select_distinct) { $columns[] = '1'; } else { $columns[] = self::quoteName($column->name) . ($column->alias ? (' AS ' . self::quoteName($column->alias)) : ''); } } $column_list = implode(', ', $columns); } // Replace the column list if this is a count-only query. if ($count_only == 1) { $count_wrap = ($this->groupby || $this->select_distinct || $has_subquery_columns || preg_match('/\bDISTINCT\b/i', $column_list)); if ($count_wrap) { $result .= ($this->select_distinct ? ' DISTINCT ' : ' ') . $column_list; } else { $result .= ' COUNT(*) AS `count`'; } } else { $count_wrap = false; $result .= ($this->select_distinct ? ' DISTINCT ' : ' ') . $column_list; } // Compose the FROM clause. if (count($this->tables)) { $tables = $this->_arrangeTables($this->tables, self::ALIAS_ALWAYS); if ($tables !== '') { $result .= ' FROM ' . $tables; } } if (count($this->index_hints)) { $index_hints = $this->_arrangeIndexHints($this->index_hints); if ($index_hints !== '') { $result .= ' ' . $index_hints; } } // Compose the WHERE clause. if (count($this->conditions)) { $where = $this->_arrangeConditions($this->conditions); if ($where !== '') { $result .= ' WHERE ' . $where; } } // Compose the GROUP BY clause. if ($this->groupby && count($this->groupby->columns) && (!$this->groupby->ifvar || !empty($this->_args[$this->groupby->ifvar]))) { $columns = array(); foreach ($this->groupby->columns as $column_name) { if (is_array($column_name)) { list($column_name, $ifvar) = $column_name; if ($ifvar && empty($this->_args[$ifvar])) { continue; } } if (self::isValidColumnName($column_name)) { $columns[] = self::quoteName($column_name); } else { $columns[] = $column_name; } } $result .= ' GROUP BY ' . implode(', ', $columns); } if ($this->groupby && count($this->groupby->having) && (!$this->groupby->ifvar || !empty($this->_args[$this->groupby->ifvar]))) { $having = $this->_arrangeConditions($this->groupby->having); if ($having !== '') { $result .= ' HAVING ' . $having; } } // Compose the ORDER BY clause. if ($this->navigation && count($this->navigation->orderby) && !$count_only) { $order_by = $this->_arrangeOrderBy($this->navigation); if ($order_by !== '') { $result .= ' ORDER BY ' . $order_by; } } // Compose the LIMIT/OFFSET clause. if ($this->navigation && $this->navigation->list_count && !$count_only) { $limit_offset = $this->_arrangeLimitOffset($this->navigation); if ($limit_offset !== '') { $result .= ' LIMIT ' . $limit_offset; } } // Wrap in a subquery if necesary. if ($count_wrap) { $result = 'SELECT COUNT(*) AS `count` FROM (' . $result . ') AS `subquery`'; } // Return the final query string. return $result; } /** * Generate a INSERT query string. * * @return string */ protected function _getInsertQueryString(): string { // Initialize the query string. $result = 'INSERT'; // Compose the INTO clause. if (count($this->tables)) { $tables = $this->_arrangeTables($this->tables, self::ALIAS_NONE); if ($tables !== '') { $result .= ' INTO ' . $tables; } } // Process the SET clause with new values. $columns = array(); foreach ($this->columns as $column) { if ($column->ifvar && empty($this->_args[$column->ifvar])) { continue; } $setval_string = $this->_parseCondition($column); if ($setval_string !== '') { $columns[] = $setval_string; } } $result .= ' SET ' . implode(', ', $columns); // Process the ON DUPLICATE KEY UPDATE (upsert) clause. if ($this->update_duplicate && count($columns)) { $result .= ' ON DUPLICATE KEY UPDATE ' . implode(', ', $columns); $duplicate_params = $this->_params; foreach ($duplicate_params as $param) { $this->_params[] = $param; } } // Return the final query string. return $result; } /** * Generate a UPDATE query string. * * @return string */ protected function _getUpdateQueryString(): string { // Initialize the query string. $result = 'UPDATE '; // Compose the INTO clause. if (count($this->tables)) { $tables = $this->_arrangeTables($this->tables, self::ALIAS_SPECIFIED); if ($tables !== '') { $result .= $tables; } } // Compose the SET clause with updated values. $columns = array(); foreach ($this->columns as $column) { if ($column->ifvar && empty($this->_args[$column->ifvar])) { continue; } $setval_string = $this->_parseCondition($column); if ($setval_string !== '') { $columns[] = $setval_string; } } $result .= ' SET ' . implode(', ', $columns); // Compose the WHERE clause. if (count($this->conditions)) { $where = $this->_arrangeConditions($this->conditions); if ($where !== '') { $result .= ' WHERE ' . $where; } } // Return the final query string. return $result; } /** * Generate a DELETE query string. * * @return string */ protected function _getDeleteQueryString(): string { // Initialize the query string. $result = 'DELETE'; // Compose the FROM clause. if (count($this->tables)) { $tables = $this->_arrangeTables($this->tables, self::ALIAS_NONE); if ($tables !== '') { $result .= ' FROM ' . $tables; } } // Compose the WHERE clause. if (count($this->conditions)) { $where = $this->_arrangeConditions($this->conditions); if ($where !== '') { $result .= ' WHERE ' . $where; } } // Compose the ORDER BY clause. if ($this->navigation && count($this->navigation->orderby)) { $result .= ' ORDER BY ' . $this->_arrangeOrderBy($this->navigation); } // Compose the LIMIT/OFFSET clause. if ($this->navigation && $this->navigation->list_count) { $limit_offset = $this->_arrangeLimitOffset($this->navigation); if ($limit_offset !== '') { $result .= ' LIMIT ' . $limit_offset; } } // Return the final query string. return $result; } /** * Generate a FROM clause from a list of tables. * * @param array $tables * @param int $use_aliases * @return string */ protected function _arrangeTables(array $tables, int $use_aliases = self::ALIAS_SPECIFIED): string { // Initialize the result. $result = array(); // Process each table definition. foreach ($tables as $table) { // Skip if ($table->ifvar && empty($this->_args[$table->ifvar])) { continue; } // Subquery if ($table instanceof self) { $tabledef = '(' . $table->getQueryString($this->_prefix, $this->_args) . ')'; $table->alias = $table->alias ?: $table->name; if ($table->alias) { $tabledef .= ' AS `' . $table->alias . '`'; } foreach ($table->getQueryParams() as $param) { $this->_params[] = $param; } } // Regular table else { $tabledef = self::quoteName($this->_prefix . $table->name); if ($use_aliases === self::ALIAS_ALWAYS) { $table->alias = $table->alias ?: $table->name; } if ($use_aliases !== self::ALIAS_NONE && $table->alias && $table->alias !== ($this->_prefix . $table->name)) { $tabledef .= ' AS `' . $table->alias . '`'; } } // Add join conditions if ($table->join_type) { $join_where = $this->_arrangeConditions($table->join_conditions); if ($join_where !== '') { $tabledef = $tabledef . ' ON ' . $join_where; } $result[] = ' ' . $table->join_type . ' ' . $tabledef; } else { $result[] = (count($result) ? ', ' : '') . $tabledef; } } // Combine the result and return as a string. return implode('', $result); } /** * Generate index hints. * * @param array $index_hints * @return string */ protected function _arrangeIndexHints(array $index_hints): string { // Initialize the index list by type. $index_list = []; // Group each index hint by type. foreach ($index_hints as $index_hint) { // Skip if ($index_hint->ifvar && empty($this->_args[$index_hint->ifvar])) { continue; } if (!count($index_hint->target_db) || isset($index_hint->target_db['mysql'])) { $key = $index_hint->hint_type ?: 'USE'; $index_list[$key] = $index_list[$key] ?? []; if ($index_hint->var && isset($this->_args[$index_hint->var])) { $index_list[$key][] = self::quoteName($this->_args[$index_hint->var]); } elseif ($index_hint->index_name) { $index_list[$key][] = self::quoteName($index_hint->index_name); } } } // Generate a list of indexes for each group. $result = []; foreach ($index_list as $key => $val) { if (count($val)) { $result[] = sprintf('%s INDEX (%s)', $key, implode(', ', $val)); } } return implode(' ', $result); } /** * Generate a WHERE clause from a list of conditions. * * @param array $conditions * @return string */ protected function _arrangeConditions(array $conditions): string { // Initialize the result. $result = ''; // Process each condition. foreach ($conditions as $condition) { // Skip if ($condition->ifvar && empty($this->_args[$condition->ifvar])) { continue; } // Subquery if ($condition instanceof self) { $condition_string = $this->_parseCondition($condition); if ($condition_string !== '') { $result .= ($result === '' ? '' : (' ' . $condition->pipe . ' ')) . $condition_string; } } // Condition group elseif ($condition instanceof ConditionGroup) { $condition_string = $this->_arrangeConditions($condition->conditions); if ($condition_string !== '') { $result .= ($result === '' ? '' : (' ' . $condition->pipe . ' ')) . '(' . $condition_string . ')'; } elseif ($condition->not_null) { throw new \Rhymix\Framework\Exceptions\QueryError('Condition group marked as NOT NULL must contain at least one valid condition'); } } // Simple condition else { $condition_string = $this->_parseCondition($condition); if ($condition_string !== '') { $result .= ($result === '' ? '' : (' ' . $condition->pipe . ' ')) . $condition_string; } } } // Return the WHERE clause. return $result; } /** * Generate a ORDER BY clause from navigation settings. * * @param Navigation $navigation * @return string */ protected function _arrangeOrderBy(Navigation $navigation): string { // Initialize the result. $result = array(); // Process each column definition. foreach ($navigation->orderby as $orderby) { // Skip if ($orderby->ifvar && empty($this->_args[$orderby->ifvar])) { continue; } // Get the name of the column or expression to order by. $column_name = ''; list($column_name, $is_expression, $is_default_value) = $orderby->getValue($this->_args); if (!$column_name) { continue; } if (!$is_expression && self::isValidColumnName($column_name)) { $column_name = self::quoteName($column_name); } elseif (!$is_default_value) { continue; } // Get the ordering (ASC or DESC). if (preg_match('/^(ASC|DESC)$/i', $orderby->order_var ?: '', $matches)) { $column_order = strtoupper($matches[1]); } elseif (isset($this->_args[$orderby->order_var])) { $column_order = preg_replace('/[^A-Z]/', '', strtoupper($this->_args[$orderby->order_var])); } else { $column_order = preg_replace('/[^A-Z]/', '', strtoupper($orderby->order_default)); } $result[] = $column_name . ' ' . $column_order; } // Return the ORDER BY clause. return implode(', ', $result); } /** * Generate a LIMIT/OFFSET clause from navigation settings. * * @param Navigation $navigation * @return string */ protected function _arrangeLimitOffset(Navigation $navigation): string { $list_count = 0; $page = 0; $offset = 0; // Get the list count. if (!$navigation->list_count->ifvar || !empty($this->_args[$navigation->list_count->ifvar])) { $list_count = $navigation->list_count->getValue($this->_args)[0]; } if ($list_count <= 0) { return ''; } // Get the offset from the page or offset variable. if ($navigation->page) { if (!$navigation->page->ifvar || !empty($this->_args[$navigation->page->ifvar])) { $page = $navigation->page->getValue($this->_args)[0]; } } if ($navigation->offset) { if (!$navigation->offset->ifvar || !empty($this->_args[$navigation->offset->ifvar])) { $offset = $navigation->offset->getValue($this->_args)[0]; } } // If page is available, set the offset and require pagination for this query. if ($page > 0) { $offset = $list_count * ($page - 1); if ($this->type === 'SELECT') { $this->requires_pagination = true; } } else { $page = 1; } // Return the LIMIT/OFFSET clause. return ($offset > 0 ? (intval($offset) . ', ') : '') . intval($list_count); } /** * Generate each condition in a WHERE clause. * * @param VariableBase $condition * @return string */ protected function _parseCondition(VariableBase $condition): string { list($where, $params) = $condition->getQueryStringAndParams($this->_args, $this->_prefix); foreach ($params as $param) { $this->_params[] = $param; } return $where; } /** * Quote a column name. * * @param string $column_name * @return string */ public static function quoteName(string $column_name): string { $exceptions = ['*' => true, 'DISTINCT' => true, 'distinct' => true]; return preg_replace_callback('/[a-z][a-z0-9_.*]*(?!\\()\b/i', function($m) use($exceptions) { $columns = explode('.', $m[0]); $columns = array_map(function($str) use($exceptions) { return isset($exceptions[$str]) ? $str : ('`' . $str . '`'); }, $columns); return implode('.', $columns); }, $column_name); } /** * Check if a column name is valid. * * @param string $column_name * @return bool */ public static function isValidColumnName(string $column_name): bool { return preg_match('/^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)*$/i', $column_name) ? true : false; } /** * Check if a variable is considered valid for XE compatibility. * * @param mixed $var * @param bool $allow_empty_string * @return bool */ public static function isValidVariable($var, bool $allow_empty_string = true): bool { if ($var === null || ($var === '' && !$allow_empty_string)) { return false; } if (is_array($var)) { $count = count($var); if ($count === 0 || ($count === 1 && reset($var) === '')) { return false; } } return true; } }