diff --git a/src/ExpressionLanguage/ExpressionExtension.php b/src/ExpressionLanguage/ExpressionExtension.php new file mode 100644 index 0000000000000000000000000000000000000000..4eb5585605975b32d82282fa1933907fc101f219 --- /dev/null +++ b/src/ExpressionLanguage/ExpressionExtension.php @@ -0,0 +1,142 @@ +<?php + +declare(strict_types=1); + +namespace Dbp\Relay\CoreBundle\ExpressionLanguage; + +/** + * This Type gets injected into our expression language variant with the name + * 'relay'. This allows us to add functions/methods with some kind of namespacing, + * instead of polluting the global namespace. + */ +class ExpressionExtension +{ + /** + * @var ExpressionLanguage + */ + private $lang; + + public function __construct(ExpressionLanguage $lang) + { + $this->lang = $lang; + } + + public static function str_starts_with() + { + $args = func_get_args(); + + return call_user_func_array('str_starts_with', $args); + } + + public static function str_ends_with() + { + $args = func_get_args(); + + return call_user_func_array('str_ends_with', $args); + } + + public static function substr() + { + $args = func_get_args(); + + return call_user_func_array('substr', $args); + } + + public static function strpos() + { + $args = func_get_args(); + + return call_user_func_array('strpos', $args); + } + + public static function strlen() + { + $args = func_get_args(); + + return call_user_func_array('strlen', $args); + } + + public static function ceil() + { + $args = func_get_args(); + + return call_user_func_array('ceil', $args); + } + + public static function floor() + { + $args = func_get_args(); + + return call_user_func_array('floor', $args); + } + + public static function round() + { + $args = func_get_args(); + + return call_user_func_array('round', $args); + } + + public static function max() + { + $args = func_get_args(); + + return call_user_func_array('max', $args); + } + + public static function min() + { + $args = func_get_args(); + + return call_user_func_array('min', $args); + } + + public static function count() + { + $args = func_get_args(); + + return call_user_func_array('count', $args); + } + + public static function implode() + { + $args = func_get_args(); + + return call_user_func_array('implode', $args); + } + + public static function explode() + { + $args = func_get_args(); + + return call_user_func_array('explode', $args); + } + + public static function empty($value): bool + { + // empty is not a real function, so call_user_func_array doesn't work + return empty($value); + } + + public function map(iterable $iterable, string $expression): array + { + $transformedResult = []; + foreach ($iterable as $key => $value) { + $transformedResult[$key] = $this->lang->evaluate($expression, ['key' => $key, 'value' => $value, 'relay' => $this]); + } + + return $transformedResult; + } + + public function filter(iterable $iterable, string $expression): array + { + $filteredResult = []; + foreach ($iterable as $key => $value) { + if ($this->lang->evaluate($expression, ['key' => $key, 'value' => $value])) { + $filteredResult[] = $value; + } + } + + return $filteredResult; + } +} diff --git a/src/ExpressionLanguage/ExpressionLanguage.php b/src/ExpressionLanguage/ExpressionLanguage.php index fe726473f7c9104557d2f6d81704d64342d79657..bc29f3184e42f5f177e34a7a09d5ad7f52a59115 100644 --- a/src/ExpressionLanguage/ExpressionLanguage.php +++ b/src/ExpressionLanguage/ExpressionLanguage.php @@ -30,4 +30,15 @@ class ExpressionLanguage extends SymfonyExpressionLanguage parent::__construct($cache, $providers); } + + /** + * @return mixed + */ + public function evaluate($expression, array $values = []) + { + $ext = new ExpressionExtension($this); + $values['relay'] = $ext; + + return parent::evaluate($expression, $values); + } } diff --git a/tests/ExpressionLanguageTest.php b/tests/ExpressionLanguageTest.php index 4b31dfa74e06ac919f768c78e0dd3bcd4c7c48d4..ef09df286b6dd20ba198d3a83ce1e4714a74af2a 100644 --- a/tests/ExpressionLanguageTest.php +++ b/tests/ExpressionLanguageTest.php @@ -25,6 +25,15 @@ class ExpressionLanguageTest extends TestCase $this->assertSame([1, 5, 2, 4], $lang->evaluate('filter([1, 5, 2, 4], "42")')); $this->assertSame([5, 4], $lang->evaluate('filter([1, 5, 2, 4], "value > 2")')); $this->assertSame([2, 4], $lang->evaluate('filter({1: 2, 3: 4}, "true")')); + $this->assertSame([5, 2, 4], $lang->evaluate('filter([0.5, 5, 2, 4], "floor(value)")')); + + $this->assertSame([], $lang->evaluate('relay.filter([], "true")')); + $this->assertSame([], $lang->evaluate('relay.filter([1, 5, 2, 4], "false")')); + $this->assertSame([1, 5, 2, 4], $lang->evaluate('relay.filter([1, 5, 2, 4], "true")')); + $this->assertSame([1, 5, 2, 4], $lang->evaluate('relay.filter([1, 5, 2, 4], "42")')); + $this->assertSame([5, 4], $lang->evaluate('relay.filter([1, 5, 2, 4], "value > 2")')); + $this->assertSame([2, 4], $lang->evaluate('relay.filter({1: 2, 3: 4}, "true")')); + $this->assertSame([5, 2, 4], $lang->evaluate('relay.filter([0.5, 5, 2, 4], "relay.floor(value)")')); } public function testMap() @@ -35,6 +44,14 @@ class ExpressionLanguageTest extends TestCase $this->assertSame([2, 6, 3, 5], $lang->evaluate('map([1, 5, 2, 4], "value + 1")')); $this->assertSame([1 => 3, 3 => 7], $lang->evaluate('map({1: 2, 3: 4}, "key + value")')); $this->assertSame([1 => 42, 3 => 42], $lang->evaluate('map({1: 2, 3: 4}, "42")')); + $this->assertSame([1.0], $lang->evaluate('map([0.5], "ceil(value)")')); + + $this->assertSame([], $lang->evaluate('relay.map([], "true")')); + $this->assertSame([false], $lang->evaluate('relay.map([1], "false")')); + $this->assertSame([2, 6, 3, 5], $lang->evaluate('relay.map([1, 5, 2, 4], "value + 1")')); + $this->assertSame([1 => 3, 3 => 7], $lang->evaluate('relay.map({1: 2, 3: 4}, "key + value")')); + $this->assertSame([1 => 42, 3 => 42], $lang->evaluate('relay.map({1: 2, 3: 4}, "42")')); + $this->assertSame([1.0], $lang->evaluate('relay.map([0.5], "relay.ceil(value)")')); } public function testEmpty() @@ -45,6 +62,12 @@ class ExpressionLanguageTest extends TestCase $this->assertTrue($lang->evaluate('empty("0")')); $this->assertFalse($lang->evaluate('empty(42)')); $this->assertFalse($lang->evaluate('empty([42])')); + + $this->assertTrue($lang->evaluate('relay.empty([])')); + $this->assertTrue($lang->evaluate('relay.empty(0)')); + $this->assertTrue($lang->evaluate('relay.empty("0")')); + $this->assertFalse($lang->evaluate('relay.empty(42)')); + $this->assertFalse($lang->evaluate('relay.empty([42])')); } public function testPhp() @@ -53,6 +76,10 @@ class ExpressionLanguageTest extends TestCase $this->assertSame(2, $lang->evaluate('count([1, 2])')); $this->assertSame('1-2', $lang->evaluate('implode("-", ["1", "2"])')); $this->assertSame(['1', '2'], $lang->evaluate('explode("-", "1-2")')); + + $this->assertSame(2, $lang->evaluate('relay.count([1, 2])')); + $this->assertSame('1-2', $lang->evaluate('relay.implode("-", ["1", "2"])')); + $this->assertSame(['1', '2'], $lang->evaluate('relay.explode("-", "1-2")')); } public function testNumeric() @@ -63,6 +90,12 @@ class ExpressionLanguageTest extends TestCase $this->assertSame(1.0, $lang->evaluate('round(0.5)')); $this->assertSame(42, $lang->evaluate('max([2, 42])')); $this->assertSame(2, $lang->evaluate('min([2, 42])')); + + $this->assertSame(2.0, $lang->evaluate('relay.ceil(1.2)')); + $this->assertSame(1.0, $lang->evaluate('relay.floor(1.9)')); + $this->assertSame(1.0, $lang->evaluate('relay.round(0.5)')); + $this->assertSame(42, $lang->evaluate('relay.max([2, 42])')); + $this->assertSame(2, $lang->evaluate('relay.min([2, 42])')); } public function testString() @@ -75,5 +108,13 @@ class ExpressionLanguageTest extends TestCase $this->assertSame('foo', $lang->evaluate('substr("foobar", 0, 3)')); $this->assertSame(1, $lang->evaluate('strpos("foobar", "oo")')); $this->assertSame(6, $lang->evaluate('strlen("foobar")')); + + $this->assertTrue($lang->evaluate('relay.str_starts_with("foo", "fo")')); + $this->assertFalse($lang->evaluate('relay.str_starts_with("foo", "xo")')); + $this->assertTrue($lang->evaluate('relay.str_ends_with("foo", "oo")')); + $this->assertFalse($lang->evaluate('relay.str_ends_with("foo", "of")')); + $this->assertSame('foo', $lang->evaluate('relay.substr("foobar", 0, 3)')); + $this->assertSame(1, $lang->evaluate('relay.strpos("foobar", "oo")')); + $this->assertSame(6, $lang->evaluate('relay.strlen("foobar")')); } }