mirror of
https://github.com/espocrm/espocrm.git
synced 2026-06-28 06:56:05 +00:00
formula: nested key access
This commit is contained in:
@@ -45,7 +45,7 @@ class VariableGetValueByKeyType extends BaseFunction
|
||||
}
|
||||
|
||||
$name = $this->evaluate($args[0]);
|
||||
$key = $this->evaluate($args[1]);
|
||||
$keys = $this->evaluate($args[1]);
|
||||
|
||||
if (!is_string($name)) {
|
||||
$this->throwBadArgumentValue(1, 'string');
|
||||
@@ -57,13 +57,21 @@ class VariableGetValueByKeyType extends BaseFunction
|
||||
|
||||
$reference =& $this->getVariables()->$name;
|
||||
|
||||
return $this->getByKey($reference, $key);
|
||||
$value = null;
|
||||
|
||||
foreach ($keys as $key) {
|
||||
$value =& $this->getByKey($reference, $key);
|
||||
|
||||
$reference =& $value;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
private function getByKey(mixed &$reference, mixed $key): mixed
|
||||
private function &getByKey(mixed &$reference, mixed $key): mixed
|
||||
{
|
||||
if (!is_array($reference) && !$reference instanceof stdClass) {
|
||||
throw new Error("Cannot access by key of variable that is non-array and non-object.");
|
||||
@@ -86,7 +94,10 @@ class VariableGetValueByKeyType extends BaseFunction
|
||||
throw new Error("Cannot get array item value by non-existent key.");
|
||||
}
|
||||
|
||||
return $reference[$key];
|
||||
/** @noinspection PhpUnnecessaryLocalVariableInspection */
|
||||
$value =& $reference[$key];
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (!is_string($key)) {
|
||||
@@ -101,6 +112,9 @@ class VariableGetValueByKeyType extends BaseFunction
|
||||
throw new Error("Cannot get object item value by non-existent key.");
|
||||
}
|
||||
|
||||
return $reference->$key;
|
||||
/** @noinspection PhpUnnecessaryLocalVariableInspection */
|
||||
$value =& $reference->$key;
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ class VariableSetKeyValueType extends BaseFunction
|
||||
}
|
||||
|
||||
$name = $this->evaluate($args[0]);
|
||||
$key = $this->evaluate($args[1]);
|
||||
$keys = $this->evaluate($args[1]);
|
||||
$value = $this->evaluate($args[2]);
|
||||
|
||||
if (!is_string($name)) {
|
||||
@@ -58,7 +58,17 @@ class VariableSetKeyValueType extends BaseFunction
|
||||
|
||||
$reference =& $this->getVariables()->$name;
|
||||
|
||||
$this->setByKey($reference, $key, $value);
|
||||
foreach ($keys as $i => $key) {
|
||||
if ($i === count($keys) - 1) {
|
||||
$this->setByKey($reference, $key, $value);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$referenceValue =& $this->getByKey($reference, $key);
|
||||
|
||||
$reference =& $referenceValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,4 +108,54 @@ class VariableSetKeyValueType extends BaseFunction
|
||||
|
||||
$reference->$key = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
private function &getByKey(mixed &$reference, mixed $key): mixed
|
||||
{
|
||||
if (!is_array($reference) && !$reference instanceof stdClass) {
|
||||
throw new Error("Cannot access by key of variable that is non-array and non-object.");
|
||||
}
|
||||
|
||||
if (is_array($reference)) {
|
||||
if (!is_int($key)) {
|
||||
throw new Error("Cannot get array item value by non-integer key.");
|
||||
}
|
||||
|
||||
if ($key < 0) {
|
||||
throw new Error("Cannot get array item value by key that is less than zero.");
|
||||
}
|
||||
|
||||
if ($key > count($reference) - 1) {
|
||||
throw new Error("Cannot get array item value by key that is out of array end.");
|
||||
}
|
||||
|
||||
if (!array_key_exists($key, $reference)) {
|
||||
throw new Error("Cannot get array item value by non-existent key.");
|
||||
}
|
||||
|
||||
/** @noinspection PhpUnnecessaryLocalVariableInspection */
|
||||
$value =& $reference[$key];
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (!is_string($key)) {
|
||||
throw new Error("Cannot get object item value by non-string key.");
|
||||
}
|
||||
|
||||
if ($key === '') {
|
||||
throw new Error("Cannot get object item value by empty string key.");
|
||||
}
|
||||
|
||||
if (!property_exists($reference, $key)) {
|
||||
throw new Error("Cannot get object item value by non-existent key.");
|
||||
}
|
||||
|
||||
/** @noinspection PhpUnnecessaryLocalVariableInspection */
|
||||
$value =& $reference->$key;
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1361,23 +1361,21 @@ class Parser
|
||||
|
||||
$isArrayAppend = false;
|
||||
$isKeyValue = false;
|
||||
$keyExpression = '';
|
||||
$keyPath = [];
|
||||
|
||||
if (str_ends_with($firstPart, '[]')) {
|
||||
$variable = substr($firstPart, 1, -2);
|
||||
|
||||
$isArrayAppend = true;
|
||||
} else if (str_ends_with($firstPart, ']') && str_contains($firstPart, '[')) {
|
||||
$bracketPosition = strpos($firstPart, '[');
|
||||
$bracketPosition = strpos($firstPart, '[') ?: 0;
|
||||
|
||||
$variable = substr($firstPart, 1, $bracketPosition - 1);
|
||||
$keyExpression = trim(substr($firstPart, $bracketPosition + 1, -1));
|
||||
|
||||
$keyPart = trim(substr($firstPart, $bracketPosition));
|
||||
$keyPath = array_map(fn ($it) => $this->split($it), $this->splitKeys($keyPart));
|
||||
|
||||
$isKeyValue = true;
|
||||
|
||||
if ($keyExpression === '') {
|
||||
throw new SyntaxError("No expression inside brackets.");
|
||||
}
|
||||
}
|
||||
|
||||
if ($variable === '' || !preg_match($this->variableNameRegExp, $variable)) {
|
||||
@@ -1394,7 +1392,7 @@ class Parser
|
||||
if ($isKeyValue) {
|
||||
return new Node('variableSetKeyValue', [
|
||||
new Value($variable),
|
||||
$this->split($keyExpression),
|
||||
new Node('list', $keyPath),
|
||||
$this->split($secondPart)
|
||||
]);
|
||||
}
|
||||
@@ -1415,7 +1413,7 @@ class Parser
|
||||
$isIncrement = false;
|
||||
$isDecrement = false;
|
||||
$isKeyValue = false;
|
||||
$keyExpression = '';
|
||||
$keyPath = [];
|
||||
|
||||
if (str_ends_with($expression, '++')) {
|
||||
$isIncrement = true;
|
||||
@@ -1428,16 +1426,12 @@ class Parser
|
||||
|
||||
$value = rtrim(substr($value, 0, -2));
|
||||
} else if (str_ends_with($expression, ']') && str_contains($expression, '[')) {
|
||||
$bracketPosition = strpos($expression, '[');
|
||||
|
||||
$bracketPosition = strpos($expression, '[') ?: 0;
|
||||
$value = substr($expression, 1, $bracketPosition - 1);
|
||||
$keyExpression = trim(substr($expression, $bracketPosition + 1, -1));
|
||||
$keyPart = trim(substr($expression, $bracketPosition));
|
||||
$keyPath = array_map(fn ($it) => $this->split($it), $this->splitKeys($keyPart));
|
||||
|
||||
$isKeyValue = true;
|
||||
|
||||
if ($keyExpression === '') {
|
||||
throw new SyntaxError("No expression inside brackets.");
|
||||
}
|
||||
}
|
||||
|
||||
if ($value === '' || !preg_match($this->variableNameRegExp, $value)) {
|
||||
@@ -1459,10 +1453,83 @@ class Parser
|
||||
if ($isKeyValue) {
|
||||
return new Node('variableGetValueByKey', [
|
||||
new Value($value),
|
||||
$this->split($keyExpression),
|
||||
new Node('list', $keyPath),
|
||||
]);
|
||||
}
|
||||
|
||||
return new Variable($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
* @throws SyntaxError
|
||||
*/
|
||||
private function splitKeys(string $expression): array
|
||||
{
|
||||
$modifiedExpression = '';
|
||||
|
||||
$this->processString($expression, $modifiedExpression, $statementList, true);
|
||||
|
||||
$expressionLength = strlen($modifiedExpression);
|
||||
|
||||
$parenthesisCounter = 0;
|
||||
$bracketCounter = 0;
|
||||
|
||||
$output = [];
|
||||
|
||||
/** @var array{int, int}[] $indexPairs */
|
||||
$indexPairs = [];
|
||||
|
||||
$startIndex = -1;
|
||||
|
||||
for ($i = 0; $i < $expressionLength; $i++) {
|
||||
$value = $modifiedExpression[$i];
|
||||
|
||||
if ($value === '(') {
|
||||
$parenthesisCounter++;
|
||||
} else if ($value === ')') {
|
||||
$parenthesisCounter--;
|
||||
} else if ($value === '[') {
|
||||
$bracketCounter++;
|
||||
} else if ($value === ']') {
|
||||
$bracketCounter--;
|
||||
}
|
||||
|
||||
if (
|
||||
$value === '[' &&
|
||||
$parenthesisCounter === 0 &&
|
||||
$bracketCounter === 1
|
||||
) {
|
||||
$startIndex = $i;
|
||||
}
|
||||
|
||||
if (
|
||||
$value === ']' &&
|
||||
$parenthesisCounter === 0 &&
|
||||
$bracketCounter === 0
|
||||
) {
|
||||
$indexPairs[] = [$startIndex + 1, $i];
|
||||
|
||||
$startIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($indexPairs as $i => $pair) {
|
||||
if ($i > 0) {
|
||||
if ($indexPairs[$i - 1][1] !== $pair[0] - 2) {
|
||||
throw new SyntaxError("Nested brackets must have no gaps in between.");
|
||||
}
|
||||
}
|
||||
|
||||
$itemExpression = trim(substr($expression, $pair[0], $pair[1] - $pair[0]));
|
||||
|
||||
if ($itemExpression === '') {
|
||||
throw new SyntaxError("No expression inside brackets.");
|
||||
}
|
||||
|
||||
$output[] = $itemExpression;
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1989,4 +1989,33 @@ class EvaluatorTest extends TestCase
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
$this->evaluator->process($expression);
|
||||
}
|
||||
|
||||
public function testArrayGetSetNested(): void
|
||||
{
|
||||
$expression = "
|
||||
\$a = list(list(0, 1));
|
||||
\$a[0][2] = 2;
|
||||
\$a[0][2];
|
||||
";
|
||||
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
$result = $this->evaluator->process($expression);
|
||||
|
||||
$this->assertEquals(2, $result);
|
||||
}
|
||||
|
||||
public function testObjectGetSetNested(): void
|
||||
{
|
||||
$expression = "
|
||||
\$o = object\\create();
|
||||
\$o['a'] = object\\create();
|
||||
\$o['a']['a1'] = '1';
|
||||
\$o['a']['a1'];
|
||||
";
|
||||
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
$result = $this->evaluator->process($expression);
|
||||
|
||||
$this->assertEquals('1', $result);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user