From cfecadac0f05264fb3b854acbb3edac594e2022a Mon Sep 17 00:00:00 2001 From: narcoticfresh Date: Fri, 4 Mar 2016 11:07:29 +0100 Subject: [PATCH 1/7] add ability to register custom functions and call them when appropriate --- src/FnDispatcher.php | 65 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/src/FnDispatcher.php b/src/FnDispatcher.php index 2b1eaa1..466ecc1 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}"); } } From 1381ff3b97b9270b06305de876e5d77c2d4768b2 Mon Sep 17 00:00:00 2001 From: narcoticfresh Date: Fri, 4 Mar 2016 11:07:45 +0100 Subject: [PATCH 2/7] add tests for custom functions --- tests/FnDispatcherTest.php | 40 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) 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]; + } +} From 48371fdd7d08683865edea36c311fd75c63a9742 Mon Sep 17 00:00:00 2001 From: narcoticfresh Date: Fri, 4 Mar 2016 11:29:34 +0100 Subject: [PATCH 3/7] add documentation for custom functions --- README.rst | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/README.rst b/README.rst index b65ee46..e230e9e 100644 --- a/README.rst +++ b/README.rst @@ -100,6 +100,72 @@ 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::getInstance()->registerCustomFn()`` to register your function. + Be aware that there ``registerCustomFn`` calls must be in a global place where they + always will be executed. + +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::getInstance()->registerCustomFn('myFunction', [new CustomFunctionHandler(), 'double']) + +An example with a runtime function: + +.. code-block:: php + + $callbackFunction = function ($args) { + return $args[0]; + }; + $fn->registerCustomFn('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::getInstance()->registerCustomFn()`` 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 + $fn->registerCustomFn('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 ======= From 326d611a2660c0ade2e9852a029a04087e995e01 Mon Sep 17 00:00:00 2001 From: narcoticfresh Date: Fri, 4 Mar 2016 12:04:34 +0100 Subject: [PATCH 4/7] fix readme issues --- README.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index e230e9e..78a27ab 100644 --- a/README.rst +++ b/README.rst @@ -120,8 +120,8 @@ To create custom functions: * Create any `callable `_ structure (loose function or class with functions) that implement your logic. * Call ``FnDispatcher::getInstance()->registerCustomFn()`` to register your function. - Be aware that there ``registerCustomFn`` calls must be in a global place where they - always will be executed. + Be aware that these ``registerCustomFn()`` calls must be in a global place if you want + to have your functions always available. Here is an example with a class instance: @@ -150,7 +150,7 @@ As you can see, you can use all the possible ``callable`` structures as defined All those examples will lead to a function ``myFunction()`` that can be used in your expressions. Type specification -^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~ The ``FnDispatcher::getInstance()->registerCustomFn()`` function accepts an optional third parameter that allows you to pass an array of type specifications @@ -160,6 +160,7 @@ parameters in the expression will be validated before your ``callable`` is execu Example: .. code-block:: php + $fn->registerCustomFn('myFunction', $callbackFunction, [['number'], ['string']]); Defines that your function expects exactly 2 parameters, the first being a ``number`` and From 65c2ab4d41c024a9b141e0ae6cf855518b26eb66 Mon Sep 17 00:00:00 2001 From: narcoticfresh Date: Fri, 4 Mar 2016 12:09:28 +0100 Subject: [PATCH 5/7] fix singleton check --- src/FnDispatcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FnDispatcher.php b/src/FnDispatcher.php index 466ecc1..df20180 100644 --- a/src/FnDispatcher.php +++ b/src/FnDispatcher.php @@ -27,7 +27,7 @@ class FnDispatcher */ public static function getInstance() { - if (self::$instance) { + if (!self::$instance) { self::$instance = new self(); } From 092dfa301d39742b4ab40d4234ff84f8a1ae7270 Mon Sep 17 00:00:00 2001 From: narcoticfresh Date: Fri, 4 Mar 2016 12:12:59 +0100 Subject: [PATCH 6/7] change function name in documentation, shall be static function name --- README.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 78a27ab..20e11ad 100644 --- a/README.rst +++ b/README.rst @@ -119,8 +119,8 @@ To create custom functions: * Create any `callable `_ structure (loose function or class with functions) that implement your logic. -* Call ``FnDispatcher::getInstance()->registerCustomFn()`` to register your function. - Be aware that these ``registerCustomFn()`` calls must be in a global place if you want +* Call ``FnDispatcher::getInstance()->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: @@ -135,7 +135,7 @@ Here is an example with a class instance: return $args[0] * 2; } } - FnDispatcher::getInstance()->registerCustomFn('myFunction', [new CustomFunctionHandler(), 'double']) + FnDispatcher::getInstance()->registerCustomFunction('myFunction', [new CustomFunctionHandler(), 'double']) An example with a runtime function: @@ -144,7 +144,7 @@ An example with a runtime function: $callbackFunction = function ($args) { return $args[0]; }; - $fn->registerCustomFn('myFunction', $callbackFunction); + FnDispatcher::getInstance()->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. @@ -152,7 +152,7 @@ All those examples will lead to a function ``myFunction()`` that can be used in Type specification ~~~~~~~~~~~~~~~~~~ -The ``FnDispatcher::getInstance()->registerCustomFn()`` function accepts an +The ``FnDispatcher::getInstance()->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. @@ -161,7 +161,7 @@ Example: .. code-block:: php - $fn->registerCustomFn('myFunction', $callbackFunction, [['number'], ['string']]); + FnDispatcher::getInstance()->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, From bdea722ba978666eb7e3bdbfbaa22544bb48d420 Mon Sep 17 00:00:00 2001 From: narcoticfresh Date: Fri, 4 Mar 2016 12:19:23 +0100 Subject: [PATCH 7/7] change function calls again to reflect actual static function call --- README.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 20e11ad..f78eb2e 100644 --- a/README.rst +++ b/README.rst @@ -119,7 +119,7 @@ To create custom functions: * Create any `callable `_ structure (loose function or class with functions) that implement your logic. -* Call ``FnDispatcher::getInstance()->registerCustomFunction()`` to register your function. +* 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. @@ -135,7 +135,7 @@ Here is an example with a class instance: return $args[0] * 2; } } - FnDispatcher::getInstance()->registerCustomFunction('myFunction', [new CustomFunctionHandler(), 'double']) + FnDispatcher::registerCustomFunction('myFunction', [new CustomFunctionHandler(), 'double']) An example with a runtime function: @@ -144,7 +144,7 @@ An example with a runtime function: $callbackFunction = function ($args) { return $args[0]; }; - FnDispatcher::getInstance()->registerCustomFunction('myFunction', $callbackFunction); + 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. @@ -152,7 +152,7 @@ All those examples will lead to a function ``myFunction()`` that can be used in Type specification ~~~~~~~~~~~~~~~~~~ -The ``FnDispatcher::getInstance()->registerCustomFunction()`` function accepts an +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. @@ -161,7 +161,7 @@ Example: .. code-block:: php - FnDispatcher::getInstance()->registerCustomFunction('myFunction', $callbackFunction, [['number'], ['string']]); + 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,