Current File : //home/tradevaly/www/phpmy/vendor/phpmyadmin/sql-parser/src/Components/OptionsArray.php |
<?php
/**
* Parses a list of options.
*/
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Components;
use PhpMyAdmin\SqlParser\Component;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use PhpMyAdmin\SqlParser\Translator;
use function array_merge_recursive;
use function count;
use function implode;
use function is_array;
use function ksort;
use function sprintf;
use function strcasecmp;
use function strtoupper;
/**
* Parses a list of options.
*
* @final
*/
class OptionsArray extends Component
{
/**
* ArrayObj of selected options.
*
* @var array
*/
public $options = [];
/**
* @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 = [])
{
$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 = [])
{
$ret = new static();
/**
* The ID that will be assigned to duplicate options.
*
* @var int
*/
$lastAssignedId = count($options) + 1;
/**
* The option that was processed last time.
*
* @var array
*/
$lastOption = null;
/**
* The index of the option that was processed last time.
*
* @var int
*/
$lastOptionId = 0;
/**
* Counts brackets.
*
* @var int
*/
$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 = 0;
for (; $list->idx < $list->count; ++$list->idx) {
/**
* Token parsed at this moment.
*
* @var 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])) {
// There is no option to be processed.
break;
}
$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(
Translator::gettext('This option conflicts with "%1$s".'),
is_array($ret->options[$lastOptionId])
? $ret->options[$lastOptionId]['name']
: $ret->options[$lastOptionId]
),
$token
);
$lastOptionId = $lastAssignedId++;
}
}
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] = [
// @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' || $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] = [
// @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] === 'expr=',
// @var Expression The parsed expression.
'expr' => '',
];
$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) {
continue;
}
if ($lastOption[1] === 'expr' || $lastOption[1] === 'expr=') {
$ret->options[$lastOptionId]['expr'] = Expression::parse(
$parser,
$list,
empty($lastOption[2]) ? [] : $lastOption[2]
);
if ($ret->options[$lastOptionId]['expr'] !== null) {
$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;
}
}
}
/*
* We reached the end of statement without getting a value
* for an option for which a value was required
*/
if (
$state === 1
&& $lastOption
&& ($lastOption[1] === 'expr'
|| $lastOption[1] === 'var'
|| $lastOption[1] === 'var='
|| $lastOption[1] === 'expr=')
) {
$parser->error(
sprintf(
'Value/Expression for the option %1$s was expected.',
$ret->options[$lastOptionId]['name']
),
$list->tokens[$list->idx - 1]
);
}
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 = [])
{
if (empty($component->options)) {
return '';
}
$options = [];
foreach ($component->options as $option) {
if (! is_array($option)) {
$options[] = $option;
} else {
$options[] = $option['name']
. (! empty($option['equals']) && $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 (is_array($option)) {
if (! strcasecmp($key, $option['name'])) {
return $getExpr ? $option['expr'] : $option['value'];
}
} elseif (! strcasecmp($key, $option)) {
return true;
}
}
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 (is_array($option)) {
if (! strcasecmp($key, $option['name'])) {
unset($this->options[$idx]);
return true;
}
} elseif (! strcasecmp($key, $option)) {
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
*/
public function merge($options)
{
if (is_array($options)) {
$this->options = array_merge_recursive($this->options, $options);
} elseif ($options instanceof self) {
$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);
}
}