mirror of
https://github.com/Lastorder-DC/rhymix.git
synced 2026-01-06 10:11:38 +09:00
More work on the query parser
This commit is contained in:
parent
92ff69591f
commit
6eca8736c1
6 changed files with 422 additions and 76 deletions
18
common/framework/exceptions/queryerror.php
Normal file
18
common/framework/exceptions/queryerror.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace Rhymix\Framework\Exceptions;
|
||||
|
||||
/**
|
||||
* The Query Error exception class.
|
||||
*/
|
||||
class QueryError extends \Rhymix\Framework\Exception
|
||||
{
|
||||
public function __construct($message = '', $code = 0, $previous = null)
|
||||
{
|
||||
if ($message === '')
|
||||
{
|
||||
$message = 'Query Error';
|
||||
}
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Rhymix\Framework\Parsers\DBQuery;
|
||||
|
||||
/**
|
||||
* Expression class.
|
||||
*/
|
||||
class Expression
|
||||
{
|
||||
/**
|
||||
* Instance properties.
|
||||
*/
|
||||
public $type;
|
||||
public $value;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $type
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function __construct(string $type, $value = null)
|
||||
{
|
||||
$this->type = $type;
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the string representation of this expression.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
switch ($this->type)
|
||||
{
|
||||
case 'var':
|
||||
return ':' . $this->value;
|
||||
case 'null':
|
||||
return 'NULL';
|
||||
case 'string':
|
||||
return;
|
||||
case 'int':
|
||||
return $this->value;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,4 +18,215 @@ class Query
|
|||
public $conditions = array();
|
||||
public $groupby = null;
|
||||
public $navigation = null;
|
||||
|
||||
/**
|
||||
* Attributes for query generation.
|
||||
*/
|
||||
protected $_prefix = '';
|
||||
protected $_args = array();
|
||||
protected $_column_list = array();
|
||||
protected $_params = array();
|
||||
protected $_temp_num = 0;
|
||||
|
||||
/**
|
||||
* Generate the query string for this query.
|
||||
*
|
||||
* @param string $prefix
|
||||
* @param array $args
|
||||
* @param array $column_list
|
||||
* @return string
|
||||
*/
|
||||
public function getQueryString(string $prefix = '', array $args, array $column_list = []): string
|
||||
{
|
||||
// Save the query information.
|
||||
$this->_prefix = $prefix;
|
||||
$this->_args = $args;
|
||||
$this->_column_list = $column_list;
|
||||
$this->_temp_num = 0;
|
||||
|
||||
// Call different internal methods depending on the query type.
|
||||
switch ($this->type)
|
||||
{
|
||||
case 'SELECT':
|
||||
return $this->_getSelectQueryString();
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query parameters to use with the query string generated above.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getQueryParams()
|
||||
{
|
||||
return $this->_params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a SELECT query string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function _getSelectQueryString(): string
|
||||
{
|
||||
// Initialize the query string.
|
||||
$result = 'SELECT ';
|
||||
|
||||
// Compose the column list.
|
||||
$columns = array();
|
||||
if ($this->_column_list)
|
||||
{
|
||||
$result .= implode(', ', array_map(function($str) {
|
||||
return '`' . $str . '`';
|
||||
}, $this->_column_list));
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach ($this->columns as $column)
|
||||
{
|
||||
if ($column instanceof self)
|
||||
{
|
||||
$subquery = $column->getQueryString($this->_prefix, $this->_args);
|
||||
foreach ($column->getQueryParams() as $key => $val)
|
||||
{
|
||||
$this->_params[$key] = $val;
|
||||
}
|
||||
$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)) : '');
|
||||
}
|
||||
else
|
||||
{
|
||||
$columns[] = self::quoteName($column->name) . ($column->alias ? (' AS ' . self::quoteName($column->alias)) : '');
|
||||
}
|
||||
}
|
||||
$result .= implode(', ', $columns);
|
||||
}
|
||||
|
||||
// Compose the table list.
|
||||
$tables = array();
|
||||
foreach ($this->tables as $table)
|
||||
{
|
||||
if ($table instanceof self)
|
||||
{
|
||||
$subquery = $table->getQueryString($this->_prefix, $this->_args);
|
||||
foreach ($table->getQueryParams() as $key => $val)
|
||||
{
|
||||
$this->_params[$key] = $val;
|
||||
}
|
||||
$tables[] = sprintf('(%s) AS `%s`', $subquery, $table->alias);
|
||||
}
|
||||
else
|
||||
{
|
||||
$tabledef = self::quoteName($table->name) . ($table->alias ? (' AS `' . $table->alias . '`') : '');
|
||||
if ($table->join_type)
|
||||
{
|
||||
$tabledef = $table->join_type . ' ' . $tabledef;
|
||||
$join_where = $this->_arrangeConditions($table->join_conditions);
|
||||
if ($join_where !== '')
|
||||
{
|
||||
$tabledef = $tabledef . ' ON ' . $join_where;
|
||||
}
|
||||
}
|
||||
$tables[] = $tabledef;
|
||||
}
|
||||
}
|
||||
$result .= ' FROM ' . implode(', ', $tables);
|
||||
|
||||
// Compose the conditions.
|
||||
if (count($this->conditions))
|
||||
{
|
||||
$where = $this->_arrangeConditions($this->conditions);
|
||||
if ($where !== '')
|
||||
{
|
||||
$result .= ' WHERE ' . $where;
|
||||
}
|
||||
}
|
||||
|
||||
// Compose the GROUP BY clause.
|
||||
|
||||
// Compose the LIMIT clause.
|
||||
|
||||
// Return the final query string.
|
||||
return $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)
|
||||
{
|
||||
// Subquery
|
||||
if ($condition instanceof self)
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
// Condition group
|
||||
elseif ($condition instanceof ConditionGroup)
|
||||
{
|
||||
$condition_string = $this->_arrangeConditions($condition->conditions);
|
||||
if ($condition_string === '')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
$result .= ($result === '' ? '' : (' ' . $condition->pipe . ' ')) . '(' . $condition_string . ')';
|
||||
}
|
||||
|
||||
// Simple condition
|
||||
else
|
||||
{
|
||||
$condition_string = $this->_parseCondition($condition);
|
||||
if ($condition_string === '')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
$result .= ($result === '' ? '' : (' ' . $condition->pipe . ' ')) . $condition_string;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the WHERE clause.
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate each condition in a WHERE clause.
|
||||
*
|
||||
* @param object $condition
|
||||
* @return string
|
||||
*/
|
||||
protected function _parseCondition(Condition $condition): string
|
||||
{
|
||||
list($where, $params) = $condition->getQueryStringAndParams($this->_args);
|
||||
foreach ($params as $key => $val)
|
||||
{
|
||||
$this->_params[$key] = $val;
|
||||
}
|
||||
return $where;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quote a column name.
|
||||
*/
|
||||
public static function quoteName($column_name): string
|
||||
{
|
||||
$columns = explode('.', $column_name);
|
||||
$columns = array_map(function($str) {
|
||||
return $str === '*' ? $str : ('`' . $str . '`');
|
||||
}, $columns);
|
||||
return implode('.', $columns);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,63 +14,229 @@ class VariableBase
|
|||
public $default;
|
||||
|
||||
/**
|
||||
* Get the value of this variable.
|
||||
* Convert an operator into real SQL.
|
||||
*
|
||||
* @return Expression
|
||||
* @param array $args
|
||||
* @return array
|
||||
*/
|
||||
public function getValue(): Expression
|
||||
public function getQueryStringAndParams(array $args): array
|
||||
{
|
||||
if ($this->var)
|
||||
// Return if this method is called on an invalid child class.
|
||||
if (!isset($this->column) || !isset($this->operation))
|
||||
{
|
||||
return new Expression('var', $this->var);
|
||||
throw new \Rhymix\Framework\Exceptions\QueryError('Invalid invocation of getQueryStringAndParams()');
|
||||
}
|
||||
|
||||
// Process the variable or default value.
|
||||
if ($this->var && isset($args[$this->var]) && !empty($args[$this->var]))
|
||||
{
|
||||
$this->filterValue($args[$this->var]);
|
||||
$value = $args[$this->var];
|
||||
}
|
||||
elseif ($this->default !== null)
|
||||
{
|
||||
$value = $this->getDefaultValue();
|
||||
}
|
||||
elseif ($this->not_null)
|
||||
{
|
||||
throw new \Rhymix\Framework\Exceptions\QueryError('Variable ' . $this->var . ' for column ' . $this->column . ' is not set');
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->getDefaultValue();
|
||||
return ['', []];
|
||||
}
|
||||
|
||||
// Quote the column name.
|
||||
$column = Query::quoteName($this->column);
|
||||
$where = '';
|
||||
$params = array();
|
||||
|
||||
// Prepare the target value.
|
||||
$list_ops = array('in' => true, 'notin' => true, 'not_in' => true, 'between' => true);
|
||||
if (isset($list_ops[$this->operation]) && !is_array($value) && $value !== '')
|
||||
{
|
||||
$value = explode(',', preg_replace('/[\s\']/', '', $value));
|
||||
}
|
||||
|
||||
// Apply the operator.
|
||||
switch ($this->operation)
|
||||
{
|
||||
case 'equal':
|
||||
$where = sprintf('%s = ?', $column);
|
||||
$params[] = $value;
|
||||
break;
|
||||
case 'like':
|
||||
$where = sprintf('%s LIKE ?', $column);
|
||||
$params[] = '%' . $value . '%';
|
||||
break;
|
||||
case 'like_prefix':
|
||||
case 'like_head':
|
||||
$where = sprintf('%s LIKE ?', $column);
|
||||
$params[] = $value . '%';
|
||||
break;
|
||||
case 'like_suffix':
|
||||
case 'like_tail':
|
||||
$where = sprintf('%s LIKE ?', $column);
|
||||
$params[] = '%' . $value;
|
||||
break;
|
||||
case 'notlike':
|
||||
$where = sprintf('%s NOT LIKE ?', $column);
|
||||
$params[] = '%' . $value . '%';
|
||||
break;
|
||||
case 'notlike_prefix':
|
||||
case 'notlike_head':
|
||||
$where = sprintf('%s NOT LIKE ?', $column);
|
||||
$params[] = $value . '%';
|
||||
break;
|
||||
case 'notlike_suffix':
|
||||
case 'notlike_tail':
|
||||
$where = sprintf('%s NOT LIKE ?', $column);
|
||||
$params[] = '%' . $value;
|
||||
break;
|
||||
case 'in':
|
||||
$count = count($value);
|
||||
$placeholders = implode(', ', array_fill(0, $count, '?'));
|
||||
$where = sprintf('%s IN (%s)', $column, $placeholders);
|
||||
foreach ($value as $item)
|
||||
{
|
||||
$params[] = $item;
|
||||
}
|
||||
break;
|
||||
case 'notin':
|
||||
case 'not_in':
|
||||
$count = count($value);
|
||||
$placeholders = implode(', ', array_fill(0, $count, '?'));
|
||||
$where = sprintf('%s NOT IN (%s)', $column, $placeholders);
|
||||
foreach ($value as $item)
|
||||
{
|
||||
$params[] = $item;
|
||||
}
|
||||
break;
|
||||
case 'between':
|
||||
$where = sprintf('%s BETWEEN ? AND ?', $column);
|
||||
foreach ($value as $item)
|
||||
{
|
||||
$params[] = $item;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Return the complete condition and parameters.
|
||||
return [$where, $params];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default value of this variable.
|
||||
*
|
||||
* @return Expression
|
||||
* @return mixed
|
||||
*/
|
||||
public function getDefaultValue(): Expression
|
||||
public function getDefaultValue()
|
||||
{
|
||||
// If the default value is not set, return null.
|
||||
$val = $this->default;
|
||||
if ($val === null)
|
||||
// If the default value is a column name, escape it.
|
||||
if (preg_match('/^[a-z0-9_]+(?:\.[a-z0-9_]+)+$/', $this->default))
|
||||
{
|
||||
return new Expression('null');
|
||||
return Query::quoteName($this->default);
|
||||
}
|
||||
elseif (isset($this->column) && preg_match('/_srl$/', $this->column) && !ctype_digit($this->default))
|
||||
{
|
||||
return Query::quoteName($this->default);
|
||||
}
|
||||
|
||||
// If the default value is a function shortcut, return an appropriate value.
|
||||
switch ($val)
|
||||
switch ($this->default)
|
||||
{
|
||||
case 'ipaddress()':
|
||||
return new Expression('string', \RX_CLIENT_IP);
|
||||
return "'" . \RX_CLIENT_IP . "'";
|
||||
case 'unixtime()':
|
||||
return new Expression('string', time());
|
||||
return time();
|
||||
case 'curdate()':
|
||||
case 'date()':
|
||||
return new Expression('string', date('YmdHis'));
|
||||
return "'" . date('YmdHis') . "'";
|
||||
case 'sequence()':
|
||||
return new Expression('int', getNextSequence());
|
||||
return getNextSequence();
|
||||
}
|
||||
|
||||
// If the default value is a calculation based on the current value, return a query string.
|
||||
if (isset($this->column) && preg_match('/^(plus|minus|multiply)\(([0-9]+)\)$/', $val, $matches))
|
||||
if (isset($this->column) && preg_match('/^(plus|minus|multiply)\(([0-9]+)\)$/', $this->default, $matches))
|
||||
{
|
||||
|
||||
switch ($matches[1])
|
||||
{
|
||||
case 'plus':
|
||||
return sprintf('%s + %d', Query::quoteName($this->column), $matches[2]);
|
||||
case 'minus':
|
||||
return sprintf('%s - %d', Query::quoteName($this->column), $matches[2]);
|
||||
case 'multiply':
|
||||
return sprintf('%s * %d', Query::quoteName($this->column), $matches[2]);
|
||||
}
|
||||
}
|
||||
|
||||
// If the default value is a column name, return the column name.20
|
||||
if (\Rhymix\Framework\Parsers\DBQueryParser::isValidColumnName($val))
|
||||
// Otherwise, just return the literal value.
|
||||
return $this->default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter a value.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
*/
|
||||
public function filterValue($value)
|
||||
{
|
||||
// Apply filters.
|
||||
switch (isset($this->filter) ? $this->filter : '')
|
||||
{
|
||||
|
||||
case 'email':
|
||||
case 'email_address':
|
||||
if (!preg_match('/^[\w-]+((?:\.|\+|\~)[\w-]+)*@[\w-]+(\.[\w-]+)+$/', $value))
|
||||
{
|
||||
throw new \Rhymix\Framework\Exceptions\QueryError('Variable ' . $this->var . ' for column ' . $this->column . ' must contain a valid e-mail address');
|
||||
}
|
||||
break;
|
||||
case 'homepage':
|
||||
case 'url':
|
||||
if (!preg_match('/^(http|https)+(:\/\/)+[0-9a-z_-]+\.[^ ]+$/i', $value))
|
||||
{
|
||||
throw new \Rhymix\Framework\Exceptions\QueryError('Variable ' . $this->var . ' for column ' . $this->column . ' must contain a valid URL');
|
||||
}
|
||||
break;
|
||||
case 'userid':
|
||||
case 'user_id':
|
||||
if (!preg_match('/^[a-zA-Z]+([_0-9a-zA-Z]+)*$/', $value))
|
||||
{
|
||||
throw new \Rhymix\Framework\Exceptions\QueryError('Variable ' . $this->var . ' for column ' . $this->column . ' must contain a valid user ID');
|
||||
}
|
||||
break;
|
||||
case 'number':
|
||||
case 'numbers':
|
||||
if (!preg_match('/^(-?)[0-9]+(,\-?[0-9]+)*$/', is_array($value) ? implode(',', $value) : $value))
|
||||
{
|
||||
throw new \Rhymix\Framework\Exceptions\QueryError('Variable ' . $this->var . ' for column ' . $this->column . ' must contain a valid number');
|
||||
}
|
||||
break;
|
||||
case 'alpha':
|
||||
if (!ctype_alpha($value))
|
||||
{
|
||||
throw new \Rhymix\Framework\Exceptions\QueryError('Variable ' . $this->var . ' for column ' . $this->column . ' must contain only alphabets');
|
||||
}
|
||||
break;
|
||||
case 'alnum':
|
||||
case 'alpha_number':
|
||||
if (!ctype_alnum($value))
|
||||
{
|
||||
throw new \Rhymix\Framework\Exceptions\QueryError('Variable ' . $this->var . ' for column ' . $this->column . ' must contain only alphanumeric characters');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Otherwise, return the literal value.
|
||||
return new Expression('string', $val);
|
||||
// Check minimum and maximum lengths.
|
||||
$length = iconv_strlen($value, 'UTF-8');
|
||||
if (isset($this->minlength) && $this->minlength > 0 && $length < $this->minlength)
|
||||
{
|
||||
throw new \Rhymix\Framework\Exceptions\QueryError('Variable ' . $this->var . ' for column ' . $this->column . ' must contain no less than ' . $this->minlength . ' characters');
|
||||
}
|
||||
if (isset($this->maxlength) && $this->maxlength > 0 && $length > $this->maxlength)
|
||||
{
|
||||
throw new \Rhymix\Framework\Exceptions\QueryError('Variable ' . $this->var . ' for column ' . $this->column . ' must contain no more than ' . $this->minlength . ' characters');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,11 +45,11 @@ class DBQueryParser
|
|||
{
|
||||
$query->name = $query->alias;
|
||||
}
|
||||
$query->type = strtoupper($xml['action']) ?: null;
|
||||
$query->type = strtoupper($xml['action']) ?: 'SELECT';
|
||||
|
||||
// Load attributes that only apply to subqueries in the <conditions> block.
|
||||
$query->operation = trim($xml['operation']) ?: null;
|
||||
$query->column = trim($xml['column']) ?: null;
|
||||
$query->column = preg_replace('/[^a-z0-9_\.]/i', '', $xml['column']) ?: null;
|
||||
$query->pipe = strtoupper($xml['pipe']) ?: 'AND';
|
||||
|
||||
// Load tables.
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class Table
|
|||
* @param string $engine
|
||||
* @return string
|
||||
*/
|
||||
public function getCreateQuery(string $prefix = '', string $charset = 'utf8mb4', string $engine = 'innodb')
|
||||
public function getCreateQuery(string $prefix = '', string $charset = 'utf8mb4', string $engine = 'innodb'): string
|
||||
{
|
||||
// Initialize the query.
|
||||
$result = 'CREATE TABLE `' . $prefix . $this->name . '` (';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue