diff --git a/README.rst b/README.rst index b65ee46..f78eb2e 100644 --- a/README.rst +++ b/README.rst @@ -100,6 +100,73 @@ You can utilize the CompilerRuntime in ``JmesPath\search()`` by setting the ``JP_PHP_COMPILE`` environment variable to "on" or to a directory on disk used to store cached expressions. +Custom functions +---------------- + +The JMESPath language has numerous +`built-in functions +`__, but it is +also possible to add your own custom functions. Keep in mind that +custom function support in jmespath.php is experimental and the API may +change based on feedback. + +**If you have a custom function that you've found useful, consider submitting +it to jmespath.site and propose that it be added to the JMESPath language.** +You can submit proposals +`here `__. + +To create custom functions: + +* Create any `callable `_ + structure (loose function or class with functions) that implement your logic. +* Call ``FnDispatcher::registerCustomFunction()`` to register your function. + Be aware that these ``registerCustomFunction()`` calls must be in a global place if you want + to have your functions always available. + +Here is an example with a class instance: + +.. code-block:: php + + // Create a class that contains your function + class CustomFunctionHandler + { + public function double($args) + { + return $args[0] * 2; + } + } + FnDispatcher::registerCustomFunction('myFunction', [new CustomFunctionHandler(), 'double']) + +An example with a runtime function: + +.. code-block:: php + + $callbackFunction = function ($args) { + return $args[0]; + }; + FnDispatcher::registerCustomFunction('myFunction', $callbackFunction); + +As you can see, you can use all the possible ``callable`` structures as defined in the PHP documentation. +All those examples will lead to a function ``myFunction()`` that can be used in your expressions. + +Type specification +~~~~~~~~~~~~~~~~~~ + +The ``FnDispatcher::registerCustomFunction()`` function accepts an +optional third parameter that allows you to pass an array of type specifications +for your custom function. If you pass this, the types (and count) of the passed +parameters in the expression will be validated before your ``callable`` is executed. + +Example: + +.. code-block:: php + + FnDispatcher::registerCustomFunction('myFunction', $callbackFunction, [['number'], ['string']]); + +Defines that your function expects exactly 2 parameters, the first being a ``number`` and +the second being a ``string``. If anything else is passed in the call to your function, +a ``\RuntimeException`` will be thrown. + Testing ======= diff --git a/src/FnDispatcher.php b/src/FnDispatcher.php index 2b1eaa1..df20180 100644 --- a/src/FnDispatcher.php +++ b/src/FnDispatcher.php @@ -9,6 +9,17 @@ */ class FnDispatcher { + + /** + * @var FnDispatcher singleton instance + */ + private static $instance = null; + + /** + * @var array custom type map + */ + private $customFunctions = []; + /** * Gets a cached instance of the default function implementations. * @@ -16,12 +27,42 @@ class FnDispatcher */ public static function getInstance() { - static $instance = null; - if (!$instance) { - $instance = new self(); + if (!self::$instance) { + self::$instance = new self(); } - return $instance; + return self::$instance; + } + + /** + * Registers a custom function using a user defined callback + * + * @param string $name name of your custom function + * @param callable $callable callable spec, see http://php.net/manual/en/language.types.callable.php + * @param array $types optional spec of expected function parameters + * + * @return void + */ + public static function registerCustomFunction($name, $callable, $types = []) + { + self::getInstance()->registerCustomFn($name, $callable, $types); + } + + /** + * Instance-bound register function, allowing for more isolated testing + * + * @param string $name name of your custom function + * @param callable $callable callable spec, see http://php.net/manual/en/language.types.callable.php + * @param array $types optional spec of expected function parameters + * + * @return void + */ + public function registerCustomFn($name, $callable, $types = []) + { + $this->customFunctions[$name] = array( + 'callable' => $callable, + 'types' => $types + ); } /** @@ -392,10 +433,24 @@ private function wrapExpression($from, callable $expr, array $types) }; } - /** @internal Pass function name validation off to runtime */ + /** @internal Pass function name validation off to runtime (if not defined in custom functions) */ public function __call($name, $args) { $name = str_replace('fn_', '', $name); + + if ( + isset($this->customFunctions[$name]['callable']) && + is_callable($this->customFunctions[$name]['callable']) + ) { + + // is there type validation? + if (!empty($this->customFunctions[$name]['types'])) { + $this->validate($name, $args[0], $this->customFunctions[$name]['types']); + } + + return call_user_func_array($this->customFunctions[$name]['callable'], [$args[0], $this]); + } + throw new \RuntimeException("Call to undefined function {$name}"); } } diff --git a/tests/FnDispatcherTest.php b/tests/FnDispatcherTest.php index 7252b3e..7fbdf16 100644 --- a/tests/FnDispatcherTest.php +++ b/tests/FnDispatcherTest.php @@ -17,6 +17,28 @@ public function testConvertsToString() $this->assertEquals('foo', $fn('to_string', [new _TestStringClass()])); $this->assertEquals('"foo"', $fn('to_string', [new _TestJsonStringClass()])); } + + public function testCustomFunctions() + { + $callable = new _TestCustomFunctionCallable(); + + $fn = new FnDispatcher(); + $fn->registerCustomFn('double', [$callable, 'double']); + $fn->registerCustomFn('testSuffix', [$callable, 'testSuffix']); + $fn->registerCustomFn('testTypeValidation', [$callable, 'testTypeValidation'], [['number'], ['number']]); + + $this->assertEquals(4, $fn('double', [2])); + $this->assertEquals('someStringTest', $fn('testSuffix', ['someString'])); + + // check type validation + try { + $this->assertEquals(2, $fn('testTypeValidation', [1, '1'])); + } catch (\Exception $e) { + $this->assertInstanceOf('\RuntimeException', $e); + } + + $this->assertEquals(4, $fn('testTypeValidation', [2, 2])); + } } class _TestStringClass @@ -39,3 +61,21 @@ public function jsonSerialize() return 'foo'; } } + +class _TestCustomFunctionCallable +{ + public function double($args) + { + return $args[0] * 2; + } + + public function testSuffix($args) + { + return $args[0].'Test'; + } + + public function testTypeValidation($args) + { + return $args[0] + $args[1]; + } +}