Cherry-picked udan11/sql-parser@e5b1cd830d. Signed-off-by: Dan Ungureanu <udan1107@gmail.com>
344 lines
11 KiB
PHP
344 lines
11 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Parses a list of options.
|
|
*
|
|
* @package SqlParser
|
|
* @subpackage Components
|
|
*/
|
|
namespace SqlParser\Components;
|
|
|
|
use SqlParser\Component;
|
|
use SqlParser\Parser;
|
|
use SqlParser\Token;
|
|
use SqlParser\TokensList;
|
|
|
|
/**
|
|
* Parses a list of options.
|
|
*
|
|
* @category Components
|
|
* @package SqlParser
|
|
* @subpackage Components
|
|
* @author Dan Ungureanu <udan1107@gmail.com>
|
|
* @license http://opensource.org/licenses/GPL-2.0 GNU Public License
|
|
*/
|
|
class OptionsArray extends Component
|
|
{
|
|
|
|
/**
|
|
* ArrayObj of selected options.
|
|
*
|
|
* @var array
|
|
*/
|
|
public $options = array();
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @param array $options The array of options. Options that have a value
|
|
* must be an array with at least two keys `name` and
|
|
* `expr` or `value`.
|
|
*/
|
|
public function __construct(array $options = array())
|
|
{
|
|
$this->options = $options;
|
|
}
|
|
|
|
/**
|
|
* @param Parser $parser The parser that serves as context.
|
|
* @param TokensList $list The list of tokens that are being parsed.
|
|
* @param array $options Parameters for parsing.
|
|
*
|
|
* @return OptionsArray
|
|
*/
|
|
public static function parse(Parser $parser, TokensList $list, array $options = array())
|
|
{
|
|
$ret = new OptionsArray();
|
|
|
|
/**
|
|
* The ID that will be assigned to duplicate options.
|
|
*
|
|
* @var int $lastAssignedId
|
|
*/
|
|
$lastAssignedId = count($options) + 1;
|
|
|
|
/**
|
|
* The option that was processed last time.
|
|
*
|
|
* @var array $lastOption
|
|
*/
|
|
$lastOption = null;
|
|
|
|
/**
|
|
* The index of the option that was processed last time.
|
|
*
|
|
* @var int $lastOptionId
|
|
*/
|
|
$lastOptionId = 0;
|
|
|
|
/**
|
|
* Counts brackets.
|
|
*
|
|
* @var int $brackets
|
|
*/
|
|
$brackets = 0;
|
|
|
|
/**
|
|
* The state of the parser.
|
|
*
|
|
* Below are the states of the parser.
|
|
*
|
|
* 0 ---------------------[ option ]----------------------> 1
|
|
*
|
|
* 1 -------------------[ = (optional) ]------------------> 2
|
|
*
|
|
* 2 ----------------------[ value ]----------------------> 0
|
|
*
|
|
* @var int $state
|
|
*/
|
|
$state = 0;
|
|
|
|
for (; $list->idx < $list->count; ++$list->idx) {
|
|
/**
|
|
* Token parsed at this moment.
|
|
*
|
|
* @var Token $token
|
|
*/
|
|
$token = $list->tokens[$list->idx];
|
|
|
|
// End of statement.
|
|
if ($token->type === Token::TYPE_DELIMITER) {
|
|
break;
|
|
}
|
|
|
|
// Skipping comments.
|
|
if ($token->type === Token::TYPE_COMMENT) {
|
|
continue;
|
|
}
|
|
|
|
// Skipping whitespace if not parsing value.
|
|
if (($token->type === Token::TYPE_WHITESPACE) && ($brackets === 0)) {
|
|
continue;
|
|
}
|
|
|
|
if ($lastOption === null) {
|
|
$upper = strtoupper($token->token);
|
|
if (isset($options[$upper])) {
|
|
$lastOption = $options[$upper];
|
|
$lastOptionId = is_array($lastOption) ?
|
|
$lastOption[0] : $lastOption;
|
|
$state = 0;
|
|
|
|
// Checking for option conflicts.
|
|
// For example, in `SELECT` statements the keywords `ALL`
|
|
// and `DISTINCT` conflict and if used together, they
|
|
// produce an invalid query.
|
|
//
|
|
// Usually, tokens can be identified in the array by the
|
|
// option ID, but if conflicts occur, a generated option ID
|
|
// is used.
|
|
//
|
|
// The first pseudo duplicate ID is the maximum value of the
|
|
// real options (e.g. if there are 5 options, the first
|
|
// fake ID is 6).
|
|
if (isset($ret->options[$lastOptionId])) {
|
|
$parser->error(
|
|
sprintf(
|
|
__('This option conflicts with "%1$s".'),
|
|
$ret->options[$lastOptionId]
|
|
),
|
|
$token
|
|
);
|
|
$lastOptionId = $lastAssignedId++;
|
|
}
|
|
} else {
|
|
// There is no option to be processed.
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($state === 0) {
|
|
if (!is_array($lastOption)) {
|
|
// This is a just keyword option without any value.
|
|
// This is the beginning and the end of it.
|
|
$ret->options[$lastOptionId] = $token->value;
|
|
$lastOption = null;
|
|
$state = 0;
|
|
} elseif (($lastOption[1] === 'var') || ($lastOption[1] === 'var=')) {
|
|
// This is a keyword that is followed by a value.
|
|
// This is only the beginning. The value is parsed in state
|
|
// 1 and 2. State 1 is used to skip the first equals sign
|
|
// and state 2 to parse the actual value.
|
|
$ret->options[$lastOptionId] = array(
|
|
// @var string The name of the option.
|
|
'name' => $token->value,
|
|
// @var bool Whether it contains an equal sign.
|
|
// This is used by the builder to rebuild it.
|
|
'equals' => $lastOption[1] === 'var=',
|
|
// @var string Raw value.
|
|
'expr' => '',
|
|
// @var string Processed value.
|
|
'value' => '',
|
|
);
|
|
$state = 1;
|
|
} elseif ($lastOption[1] === 'expr') {
|
|
// This is a keyword that is followed by an expression.
|
|
// The expression is used by the specialized parser.
|
|
|
|
// Skipping this option in order to parse the expression.
|
|
++$list->idx;
|
|
$ret->options[$lastOptionId] = array(
|
|
// @var string The name of the option.
|
|
'name' => $token->value,
|
|
// @var Expression The parsed expression.
|
|
'expr' => null,
|
|
);
|
|
$state = 1;
|
|
}
|
|
} elseif ($state === 1) {
|
|
$state = 2;
|
|
if ($token->token === '=') {
|
|
$ret->options[$lastOptionId]['equals'] = true;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// This is outside the `elseif` group above because the change might
|
|
// change this iteration.
|
|
if ($state === 2) {
|
|
if ($lastOption[1] === 'expr') {
|
|
$ret->options[$lastOptionId]['expr'] = Expression::parse(
|
|
$parser,
|
|
$list,
|
|
empty($lastOption[2]) ? array() : $lastOption[2]
|
|
);
|
|
$ret->options[$lastOptionId]['value']
|
|
= $ret->options[$lastOptionId]['expr']->expr;
|
|
$lastOption = null;
|
|
$state = 0;
|
|
} else {
|
|
if ($token->token === '(') {
|
|
++$brackets;
|
|
} elseif ($token->token === ')') {
|
|
--$brackets;
|
|
}
|
|
|
|
$ret->options[$lastOptionId]['expr'] .= $token->token;
|
|
|
|
if (!((($token->token === '(') && ($brackets === 1))
|
|
|| (($token->token === ')') && ($brackets === 0)))
|
|
) {
|
|
// First pair of brackets is being skipped.
|
|
$ret->options[$lastOptionId]['value'] .= $token->value;
|
|
}
|
|
|
|
// Checking if we finished parsing.
|
|
if ($brackets === 0) {
|
|
$lastOption = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (empty($options['_UNSORTED'])) {
|
|
ksort($ret->options);
|
|
}
|
|
|
|
--$list->idx;
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* @param OptionsArray $component The component to be built.
|
|
* @param array $options Parameters for building.
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function build($component, array $options = array())
|
|
{
|
|
if (empty($component->options)) {
|
|
return '';
|
|
}
|
|
|
|
$options = array();
|
|
foreach ($component->options as $option) {
|
|
if (!is_array($option)) {
|
|
$options[] = $option;
|
|
} else {
|
|
$options[] = $option['name']
|
|
. (!empty($option['equals']) ? '=' : ' ')
|
|
. (!empty($option['expr']) ? $option['expr'] : $option['value']);
|
|
}
|
|
}
|
|
return implode(' ', $options);
|
|
}
|
|
|
|
/**
|
|
* Checks if it has the specified option and returns it value or true.
|
|
*
|
|
* @param string $key The key to be checked.
|
|
* @param bool $getExpr Gets the expression instead of the value.
|
|
* The value is the processed form of the expression.
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function has($key, $getExpr = false)
|
|
{
|
|
foreach ($this->options as $option) {
|
|
if ($key === $option) {
|
|
return true;
|
|
} elseif ((is_array($option)) && ($key === $option['name'])) {
|
|
return $getExpr ? $option['expr'] : $option['value'];
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Removes the option from the array.
|
|
*
|
|
* @param string $key The key to be removed.
|
|
*
|
|
* @return bool Whether the key was found and deleted or not.
|
|
*/
|
|
public function remove($key)
|
|
{
|
|
foreach ($this->options as $idx => $option) {
|
|
if (($key === $option)
|
|
|| ((is_array($option)) && ($key === $option['name']))
|
|
) {
|
|
unset($this->options[$idx]);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Merges the specified options with these ones. Values with same ID will be
|
|
* replaced.
|
|
*
|
|
* @param array|OptionsArray $options The options to be merged.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function merge($options)
|
|
{
|
|
if (is_array($options)) {
|
|
$this->options = array_merge_recursive($this->options, $options);
|
|
} elseif ($options instanceof OptionsArray) {
|
|
$this->options = array_merge_recursive($this->options, $options->options);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks tf there are no options set.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function isEmpty()
|
|
{
|
|
return empty($this->options);
|
|
}
|
|
}
|