mirror of
https://github.com/espocrm/espocrm.git
synced 2026-06-28 06:56:05 +00:00
formula func interface
This commit is contained in:
183
application/Espo/Core/Formula/EvaluatedArgumentList.php
Normal file
183
application/Espo/Core/Formula/EvaluatedArgumentList.php
Normal file
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Formula;
|
||||
|
||||
use BadMethodCallException;
|
||||
use OutOfBoundsException;
|
||||
use Iterator;
|
||||
use Countable;
|
||||
use ArrayAccess;
|
||||
use SeekableIterator;
|
||||
|
||||
/**
|
||||
* A list of evaluated function arguments.
|
||||
*
|
||||
* @implements ArrayAccess<int, mixed>
|
||||
* @implements Iterator<mixed>
|
||||
* @implements SeekableIterator<int, mixed>
|
||||
*/
|
||||
class EvaluatedArgumentList implements Iterator, Countable, ArrayAccess, SeekableIterator
|
||||
{
|
||||
private int $position = 0;
|
||||
|
||||
/**
|
||||
* @param mixed[] $dataList
|
||||
*/
|
||||
public function __construct(private array $dataList)
|
||||
{}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
private function getLastValidKey()
|
||||
{
|
||||
$keys = array_keys($this->dataList);
|
||||
|
||||
$i = end($keys);
|
||||
|
||||
while ($i > 0) {
|
||||
if (array_key_exists($i, $this->dataList)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$i--;
|
||||
}
|
||||
|
||||
return $i;
|
||||
}
|
||||
|
||||
public function rewind(): void
|
||||
{
|
||||
$this->position = 0;
|
||||
|
||||
while (!$this->valid() && $this->position <= $this->getLastValidKey()) {
|
||||
$this->position ++;
|
||||
}
|
||||
}
|
||||
|
||||
private function getArgumentByIndex(int $index): mixed
|
||||
{
|
||||
return $this->dataList[$index];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function current()
|
||||
{
|
||||
return $this->getArgumentByIndex($this->position);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function key()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function next(): void
|
||||
{
|
||||
do {
|
||||
$this->position ++;
|
||||
$next = false;
|
||||
|
||||
if (
|
||||
!$this->valid() &&
|
||||
$this->position <= $this->getLastValidKey()
|
||||
) {
|
||||
$next = true;
|
||||
}
|
||||
} while ($next);
|
||||
}
|
||||
|
||||
public function valid(): bool
|
||||
{
|
||||
return array_key_exists($this->position, $this->dataList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
*/
|
||||
public function offsetExists($offset): bool
|
||||
{
|
||||
return array_key_exists($offset, $this->dataList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
* @return mixed
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function offsetGet($offset)
|
||||
{
|
||||
if (!$this->offsetExists($offset)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->getArgumentByIndex($offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function offsetSet($offset, $value): void
|
||||
{
|
||||
throw new BadMethodCallException('Setting is not allowed.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
*/
|
||||
public function offsetUnset($offset): void
|
||||
{
|
||||
throw new BadMethodCallException('Unsetting is not allowed.');
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->dataList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $offset
|
||||
*/
|
||||
public function seek($offset): void
|
||||
{
|
||||
$this->position = $offset;
|
||||
|
||||
if (!$this->valid()) {
|
||||
throw new OutOfBoundsException("Invalid seek offset ($offset).");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,10 @@
|
||||
namespace Espo\Core\Formula;
|
||||
|
||||
use Espo\Core\Formula\Exceptions\Error;
|
||||
use Espo\Core\Formula\Exceptions\ExecutionException;
|
||||
use Espo\Core\Formula\Exceptions\SyntaxError;
|
||||
use Espo\Core\Formula\Functions\Base as DeprecatedBaseFunction;
|
||||
use Espo\Core\Formula\Functions\BaseFunction;
|
||||
use Espo\Core\Formula\Parser\Ast\Attribute;
|
||||
use Espo\Core\Formula\Parser\Ast\Node;
|
||||
use Espo\Core\Formula\Parser\Ast\Value;
|
||||
@@ -45,32 +48,31 @@ use stdClass;
|
||||
*/
|
||||
class Evaluator
|
||||
{
|
||||
/** @var array<string, class-string> */
|
||||
private $functionClassNameMap;
|
||||
private Parser $parser;
|
||||
private AttributeFetcher $attributeFetcher;
|
||||
private InjectableFactory $injectableFactory;
|
||||
/** @var array<string, (Node|Value|Attribute|Variable)> */
|
||||
private $parsedHash;
|
||||
|
||||
/**
|
||||
* @param array<string, class-string> $functionClassNameMap
|
||||
* @param array<string, class-string<BaseFunction|Func|DeprecatedBaseFunction>> $functionClassNameMap
|
||||
*/
|
||||
public function __construct(InjectableFactory $injectableFactory, array $functionClassNameMap = [])
|
||||
{
|
||||
public function __construct(
|
||||
private InjectableFactory $injectableFactory,
|
||||
private array $functionClassNameMap = []
|
||||
) {
|
||||
$this->attributeFetcher = $injectableFactory->create(AttributeFetcher::class);
|
||||
$this->injectableFactory = $injectableFactory;
|
||||
$this->functionClassNameMap = $functionClassNameMap;
|
||||
$this->parser = new Parser();
|
||||
$this->parsedHash = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
* Process expression.
|
||||
*
|
||||
* @throws SyntaxError
|
||||
* @throws Error
|
||||
* @throws ExecutionException
|
||||
*/
|
||||
public function process(string $expression, ?Entity $entity = null, ?stdClass $variables = null)
|
||||
public function process(string $expression, ?Entity $entity = null, ?stdClass $variables = null): mixed
|
||||
{
|
||||
$processor = new Processor(
|
||||
$this->injectableFactory,
|
||||
@@ -81,9 +83,7 @@ class Evaluator
|
||||
);
|
||||
|
||||
$item = $this->getParsedExpression($expression);
|
||||
|
||||
$result = $processor->process($item);
|
||||
|
||||
$this->attributeFetcher->resetRuntimeCache();
|
||||
|
||||
return $result;
|
||||
|
||||
@@ -29,6 +29,34 @@
|
||||
|
||||
namespace Espo\Core\Formula\Exceptions;
|
||||
|
||||
/**
|
||||
* A bad argument type.
|
||||
*/
|
||||
class BadArgumentType extends Error
|
||||
{
|
||||
private ?int $position = null;
|
||||
private ?string $type = null;
|
||||
|
||||
/**
|
||||
* Create.
|
||||
*
|
||||
* @param int $position An argument position.
|
||||
* @param string $type A required argument type.
|
||||
*/
|
||||
public static function create(int $position, string $type): self
|
||||
{
|
||||
$obj = new self();
|
||||
$obj->position = $position;
|
||||
$obj->type = $type;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function getLogMessage(): string
|
||||
{
|
||||
$position = (string) ($this->position ?? '?');
|
||||
$type = $this->type ?? '?';
|
||||
|
||||
return "Bad argument type on position {$position}, must be {$type}.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,30 @@
|
||||
|
||||
namespace Espo\Core\Formula\Exceptions;
|
||||
|
||||
/**
|
||||
* A bad argument value.
|
||||
*/
|
||||
class BadArgumentValue extends Error
|
||||
{
|
||||
private ?int $position = null;
|
||||
|
||||
/**
|
||||
* Create.
|
||||
*
|
||||
* @param int $position An argument position.
|
||||
*/
|
||||
public static function create(int $position): self
|
||||
{
|
||||
$obj = new self();
|
||||
$obj->position = $position;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function getLogMessage(): string
|
||||
{
|
||||
$position = (string) ($this->position ?? '?');
|
||||
|
||||
return "Bad argument value on position {$position}.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,30 @@
|
||||
|
||||
namespace Espo\Core\Formula\Exceptions;
|
||||
|
||||
/**
|
||||
* Too few function arguments passsed.
|
||||
*/
|
||||
class TooFewArguments extends Error
|
||||
{
|
||||
private ?int $number = null;
|
||||
|
||||
/**
|
||||
* Create.
|
||||
*
|
||||
* @param int $number A required number of arguments.
|
||||
*/
|
||||
public static function create(int $number): self
|
||||
{
|
||||
$obj = new self();
|
||||
$obj->number = $number;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function getLogMessage(): string
|
||||
{
|
||||
$number = (string) ($this->number ?? '?');
|
||||
|
||||
return "Too few arguments passed, must be {$number}.";
|
||||
}
|
||||
}
|
||||
|
||||
46
application/Espo/Core/Formula/Func.php
Normal file
46
application/Espo/Core/Formula/Func.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Formula;
|
||||
|
||||
use Espo\Core\Formula\Exceptions\Error;
|
||||
|
||||
/**
|
||||
* A function.
|
||||
*/
|
||||
interface Func
|
||||
{
|
||||
/**
|
||||
* Process.
|
||||
*
|
||||
* @param EvaluatedArgumentList $arguments
|
||||
* @throws Error
|
||||
*/
|
||||
public function process(EvaluatedArgumentList $arguments): mixed;
|
||||
}
|
||||
@@ -36,36 +36,30 @@ use Espo\Core\Formula\Functions\BaseFunction;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Core\InjectableFactory;
|
||||
|
||||
use ReflectionClass;
|
||||
use stdClass;
|
||||
|
||||
class FunctionFactory
|
||||
{
|
||||
private Processor $processor;
|
||||
private InjectableFactory $injectableFactory;
|
||||
private AttributeFetcher $attributeFetcher;
|
||||
/** @var array<string, class-string> */
|
||||
/** @var array<string, class-string<BaseFunction|Func|Base>> */
|
||||
private $classNameMap;
|
||||
|
||||
/**
|
||||
* @param array<string,class-string> $classNameMap
|
||||
* @param array<string, class-string<BaseFunction|Func|Base>> $classNameMap
|
||||
*/
|
||||
public function __construct(
|
||||
Processor $processor,
|
||||
InjectableFactory $injectableFactory,
|
||||
AttributeFetcher $attributeFetcher,
|
||||
private Processor $processor,
|
||||
private InjectableFactory $injectableFactory,
|
||||
private AttributeFetcher $attributeFetcher,
|
||||
?array $classNameMap = null
|
||||
) {
|
||||
$this->processor = $processor;
|
||||
$this->injectableFactory = $injectableFactory;
|
||||
$this->attributeFetcher = $attributeFetcher;
|
||||
$this->classNameMap = $classNameMap ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BaseFunction|Base
|
||||
* @throws UnknownFunction
|
||||
*/
|
||||
public function create(string $name, ?Entity $entity = null, ?stdClass $variables = null): object
|
||||
public function create(string $name, ?Entity $entity = null, ?stdClass $variables = null): Func|BaseFunction|Base
|
||||
{
|
||||
if ($this->classNameMap && array_key_exists($name, $this->classNameMap)) {
|
||||
$className = $this->classNameMap[$name];
|
||||
@@ -83,7 +77,7 @@ class FunctionFactory
|
||||
|
||||
$typeName = implode('\\', $arr);
|
||||
|
||||
/** @var class-string<BaseFunction|Base> $className */
|
||||
/** @var class-string<Func|BaseFunction|Base> $className */
|
||||
$className = 'Espo\\Core\\Formula\\Functions\\' . $typeName . 'Type';
|
||||
}
|
||||
|
||||
@@ -91,6 +85,10 @@ class FunctionFactory
|
||||
throw new UnknownFunction("Unknown function: " . $name);
|
||||
}
|
||||
|
||||
if ((new ReflectionClass($className))->implementsInterface(Func::class)) {
|
||||
return $this->injectableFactory->create($className);
|
||||
}
|
||||
|
||||
$object = $this->injectableFactory->createWith($className, [
|
||||
'name' => $name,
|
||||
'processor' => $this->processor,
|
||||
|
||||
@@ -163,7 +163,6 @@ abstract class BaseFunction
|
||||
* @throws Error
|
||||
*/
|
||||
protected function throwError(?string $msg = null)
|
||||
|
||||
{
|
||||
$string = 'function: ' . $this->name;
|
||||
|
||||
|
||||
@@ -29,23 +29,20 @@
|
||||
|
||||
namespace Espo\Core\Formula\Functions\StringGroup;
|
||||
|
||||
use Espo\Core\Formula\{
|
||||
Functions\BaseFunction,
|
||||
ArgumentList,
|
||||
};
|
||||
use Espo\Core\Formula\EvaluatedArgumentList;
|
||||
use Espo\Core\Formula\Exceptions\TooFewArguments;
|
||||
use Espo\Core\Formula\Func;
|
||||
|
||||
class ContainsType extends BaseFunction
|
||||
class ContainsType implements Func
|
||||
{
|
||||
public function process(ArgumentList $args)
|
||||
public function process(EvaluatedArgumentList $arguments): bool
|
||||
{
|
||||
$args = $this->evaluate($args);
|
||||
|
||||
if (count($args) < 2) {
|
||||
$this->throwTooFewArguments();
|
||||
if (count($arguments) < 2) {
|
||||
throw TooFewArguments::create(2);
|
||||
}
|
||||
|
||||
$string = $args[0];
|
||||
$needle = $args[1];
|
||||
$string = $arguments[0];
|
||||
$needle = $arguments[1];
|
||||
|
||||
if (!is_string($string)) {
|
||||
return false;
|
||||
|
||||
@@ -29,18 +29,13 @@
|
||||
|
||||
namespace Espo\Core\Formula\Functions\UtilGroup;
|
||||
|
||||
use Espo\Core\Formula\{
|
||||
ArgumentList,
|
||||
};
|
||||
|
||||
use Espo\Core\Formula\EvaluatedArgumentList;
|
||||
use Espo\Core\Formula\Func;
|
||||
use Espo\Core\Utils\Util;
|
||||
|
||||
class GenerateIdType
|
||||
class GenerateIdType implements Func
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function process(ArgumentList $args)
|
||||
public function process(EvaluatedArgumentList $arguments): string
|
||||
{
|
||||
return Util::generateId();
|
||||
}
|
||||
|
||||
@@ -29,16 +29,20 @@
|
||||
|
||||
namespace Espo\Core\Formula;
|
||||
|
||||
use Espo\Core\Formula\Exceptions\BadArgumentType;
|
||||
use Espo\Core\Formula\Exceptions\BadArgumentValue;
|
||||
use Espo\Core\Formula\Exceptions\ExecutionException;
|
||||
use Espo\Core\Formula\Exceptions\TooFewArguments;
|
||||
use Espo\Core\Formula\Functions\Base as DeprecatedBaseFunction;
|
||||
use Espo\Core\Formula\Functions\BaseFunction;
|
||||
use Espo\Core\Formula\Parser\Ast\Attribute;
|
||||
use Espo\Core\Formula\Parser\Ast\Node;
|
||||
use Espo\Core\Formula\Parser\Ast\Value;
|
||||
use Espo\Core\Formula\Parser\Ast\Variable;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use Espo\Core\Formula\Exceptions\Error;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Formula\Functions\Base as DeprecatedBaseFunction;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use stdClass;
|
||||
@@ -49,17 +53,16 @@ use stdClass;
|
||||
class Processor
|
||||
{
|
||||
private FunctionFactory $functionFactory;
|
||||
private ?Entity $entity;
|
||||
private stdClass $variables;
|
||||
|
||||
/**
|
||||
* @param ?array<string, class-string> $functionClassNameMap
|
||||
* @param ?array<string, class-string<BaseFunction|Func|DeprecatedBaseFunction>> $functionClassNameMap
|
||||
*/
|
||||
public function __construct(
|
||||
InjectableFactory $injectableFactory,
|
||||
AttributeFetcher $attributeFetcher,
|
||||
?array $functionClassNameMap = null,
|
||||
?Entity $entity = null,
|
||||
private ?Entity $entity = null,
|
||||
?stdClass $variables = null
|
||||
) {
|
||||
$this->functionFactory = new FunctionFactory(
|
||||
@@ -69,7 +72,6 @@ class Processor
|
||||
$functionClassNameMap
|
||||
);
|
||||
|
||||
$this->entity = $entity;
|
||||
$this->variables = $variables ?? (object) [];
|
||||
}
|
||||
|
||||
@@ -80,7 +82,7 @@ class Processor
|
||||
* @throws Error
|
||||
* @throws ExecutionException
|
||||
*/
|
||||
public function process(Evaluatable $item)
|
||||
public function process(Evaluatable $item): mixed
|
||||
{
|
||||
if ($item instanceof ArgumentList) {
|
||||
return $this->processList($item);
|
||||
@@ -92,7 +94,21 @@ class Processor
|
||||
|
||||
$function = $this->functionFactory->create($item->getType(), $this->entity, $this->variables);
|
||||
|
||||
/** @deprecated */
|
||||
if ($function instanceof Func) {
|
||||
$evaluatedArguments = array_map(function ($item) {
|
||||
return $this->process($item);
|
||||
}, iterator_to_array($item->getArgumentList()));
|
||||
|
||||
try {
|
||||
return $function->process(new EvaluatedArgumentList($evaluatedArguments));
|
||||
}
|
||||
catch (TooFewArguments|BadArgumentType|BadArgumentValue $e) {
|
||||
$message = sprintf('Function %s; %s', $item->getType(), $e->getLogMessage());
|
||||
|
||||
throw new Error($message);
|
||||
}
|
||||
}
|
||||
|
||||
if ($function instanceof DeprecatedBaseFunction) {
|
||||
return $function->process(self::dataToStdClass($item->getData()));
|
||||
}
|
||||
@@ -139,6 +155,7 @@ class Processor
|
||||
/**
|
||||
* @return mixed[]
|
||||
* @throws Error
|
||||
* @throws ExecutionException
|
||||
*/
|
||||
private function processList(ArgumentList $args): array
|
||||
{
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
namespace tests\unit\Espo\Core\Formula;
|
||||
|
||||
use Espo\Core\Formula\Evaluator;
|
||||
use Espo\Core\Formula\Exceptions\Error;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Formula\Exceptions\SyntaxError;
|
||||
use Espo\Core\Utils\Log;
|
||||
@@ -1153,4 +1154,31 @@ class EvaluatorTest extends \PHPUnit\Framework\TestCase
|
||||
|
||||
$this->assertEquals(2, $value);
|
||||
}
|
||||
|
||||
public function testFuncInterface1(): void
|
||||
{
|
||||
$expression = "string\\contains(string\\concatenate('test', 'hello'), 'hello')";
|
||||
|
||||
$value = $this->evaluator->process($expression);
|
||||
|
||||
$this->assertEquals(true, $value);
|
||||
}
|
||||
|
||||
public function testFuncInterface2(): void
|
||||
{
|
||||
$expression = "true == string\\contains('test hello', 'hello')";
|
||||
|
||||
$value = $this->evaluator->process($expression);
|
||||
|
||||
$this->assertEquals(true, $value);
|
||||
}
|
||||
|
||||
public function testFuncInterfaceException(): void
|
||||
{
|
||||
$expression = "true == string\\contains('test hello')";
|
||||
|
||||
$this->expectException(Error::class);
|
||||
|
||||
$this->evaluator->process($expression);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user