Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
<http://jmespath.org/specification.html#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 <https://github.com/jmespath/jmespath.site/issues>`__.

To create custom functions:

* Create any `callable <http://php.net/manual/en/language.types.callable.php>`_
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
=======

Expand Down
65 changes: 60 additions & 5 deletions src/FnDispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,60 @@
*/
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.
*
* @return 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
);
}

/**
Expand Down Expand Up @@ -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}");
}
}
40 changes: 40 additions & 0 deletions tests/FnDispatcherTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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];
}
}