Skip to content

Commit

Permalink
Merge pull request #81 from veewee/xpath-xslt-callback-functions
Browse files Browse the repository at this point in the history
Introduce XPath and XSLT callback functions
  • Loading branch information
veewee authored Sep 20, 2024
2 parents d0933f1 + 15c7d96 commit 5a4c3f9
Show file tree
Hide file tree
Showing 11 changed files with 183 additions and 9 deletions.
14 changes: 13 additions & 1 deletion docs/dom.md
Original file line number Diff line number Diff line change
Expand Up @@ -1543,7 +1543,19 @@ use VeeWee\Xml\Dom\Document;
use function VeeWee\Xml\Dom\Xpath\Configurator\functions;

$doc = Document::fromXmlFile('data.xml');
$xpath = $doc->xpath(functions(['has_multiple']));
$xpath = $doc->xpath(functions(['has_multiple' => has_multiple(...)]));
```

#### namespaced_functions

Registers a list of namespaced functions to the XPath object, allowing you to use `prefix:somefunction()` inside your XPath query.

```php
use VeeWee\Xml\Dom\Document;
use function VeeWee\Xml\Dom\Xpath\Configurator\namespaced_functions;

$doc = Document::fromXmlFile('data.xml');
$xpath = $doc->xpath(namespaced_functions('http://ns', 'prefix', ['has_multiple' => has_multiple(...)]));
```

#### namespaces
Expand Down
17 changes: 16 additions & 1 deletion docs/xslt.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,22 @@ use function VeeWee\Xml\Xslt\Configurator\functions;

$processor = Processor::fromTemplateDocument(
Document::fromXmlFile('xml-to-yaml-converter.xslt'),
functions(['ucfirst'])
functions(['ucfirst' => ucfirst(...)])
);
```

#### namespaced_functions

Registers specific namespaced functions to the XSLTProcessor object, allowing you to use `prefix:function('ucfirst',string(uid))` inside your XSLT Template.

```php
use VeeWee\Xml\Dom\Document;
use VeeWee\Xml\Xslt\Processor;
use function VeeWee\Xml\Xslt\Configurator\namespaced_functions;

$processor = Processor::fromTemplateDocument(
Document::fromXmlFile('xml-to-yaml-converter.xslt'),
namespaced_functions('http://ns', ['ucfirst' => ucfirst(...)])
);
```

Expand Down
2 changes: 1 addition & 1 deletion src/Xml/Dom/Xpath/Configurator/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
use Dom\XPath;

/**
* @param non-empty-list<string> $functions
* @param array<string, (callable(mixed...): mixed)> $functions
*
* @return Closure(XPath): XPath
*/
Expand Down
25 changes: 25 additions & 0 deletions src/Xml/Dom/Xpath/Configurator/namespaced_functions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace VeeWee\Xml\Dom\Xpath\Configurator;

use Closure;
use Dom\XPath;

/**
* @param array<string, (callable(mixed...): mixed)> $functions
*
* @return Closure(XPath): XPath
*/
function namespaced_functions(string $namespace, string $prefix, array $functions): Closure
{
return static function (XPath $xpath) use ($namespace, $prefix, $functions) : XPath {
namespaces([$prefix => $namespace])($xpath);
foreach ($functions as $functionName => $callback) {
$xpath->registerPhpFunctionNS($namespace, $functionName, $callback);
}

return $xpath;
};
}
4 changes: 1 addition & 3 deletions src/Xml/Xslt/Configurator/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@
use XSLTProcessor;

/**
* TODO : Add support for callables : https://wiki.php.net/rfc/improve_callbacks_dom_and_xsl (either here or through a separate configurator)
*
* @param non-empty-list<string> $functions
* @param array<string, (callable(mixed...): mixed)> $functions
*
* @return Closure(XSLTProcessor): XSLTProcessor
*/
Expand Down
24 changes: 24 additions & 0 deletions src/Xml/Xslt/Configurator/namespaced_functions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace VeeWee\Xml\Xslt\Configurator;

use Closure;
use XSLTProcessor;

/**
* @param array<string, (callable(mixed...): mixed)> $functions
*
* @return Closure(XSLTProcessor): XSLTProcessor
*/
function namespaced_functions(string $namespace, array $functions): Closure
{
return static function (XSLTProcessor $processor) use ($namespace, $functions) : XSLTProcessor {
foreach ($functions as $functionName => $callback) {
$processor->registerPhpFunctionNS($namespace, $functionName, $callback);
}

return $processor;
};
}
2 changes: 2 additions & 0 deletions src/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
'Xml\Dom\Validator\xsd_validator' => __DIR__.'/Xml/Dom/Validator/xsd_validator.php',
'Xml\Dom\Xpath\Configurator\all_functions' => __DIR__.'/Xml/Dom/Xpath/Configurator/all_functions.php',
'Xml\Dom\Xpath\Configurator\functions' => __DIR__.'/Xml/Dom/Xpath/Configurator/functions.php',
'Xml\Dom\Xpath\Configurator\namespaced_functions' => __DIR__.'/Xml/Dom/Xpath/Configurator/namespaced_functions.php',
'Xml\Dom\Xpath\Configurator\namespaces' => __DIR__.'/Xml/Dom/Xpath/Configurator/namespaces.php',
'Xml\Dom\Xpath\Configurator\php_namespace' => __DIR__.'/Xml/Dom/Xpath/Configurator/php_namespace.php',
'Xml\Dom\Xpath\Locator\evaluate' => __DIR__.'/Xml/Dom/Xpath/Locator/evaluate.php',
Expand Down Expand Up @@ -170,6 +171,7 @@
'Xml\Xslt\Configurator\all_functions' => __DIR__.'/Xml/Xslt/Configurator/all_functions.php',
'Xml\Xslt\Configurator\functions' => __DIR__.'/Xml/Xslt/Configurator/functions.php',
'Xml\Xslt\Configurator\loader' => __DIR__.'/Xml/Xslt/Configurator/loader.php',
'Xml\Xslt\Configurator\namespaced_functions' => __DIR__.'/Xml/Xslt/Configurator/namespaced_functions.php',
'Xml\Xslt\Configurator\parameters' => __DIR__.'/Xml/Xslt/Configurator/parameters.php',
'Xml\Xslt\Configurator\profiler' => __DIR__.'/Xml/Xslt/Configurator/profiler.php',
'Xml\Xslt\Configurator\security_preferences' => __DIR__.'/Xml/Xslt/Configurator/security_preferences.php',
Expand Down
2 changes: 1 addition & 1 deletion tests/Xml/Dom/Xpath/Configurator/FunctionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public function test_it_can_evaluate_with_php_function(): void
$doc = Document::fromXmlString(
$xml = '<hello><item>Jos</item></hello>'
);
$xpath = Xpath::fromDocument($doc, Xpath\Configurator\functions(['str_replace']));
$xpath = Xpath::fromDocument($doc, Xpath\Configurator\functions(['str_replace' => str_replace(...)]));

$result = $xpath->evaluate('php:functionString("str_replace", "J", "B", string(//item))', Type\string());
static::assertSame($result, 'Bos');
Expand Down
28 changes: 28 additions & 0 deletions tests/Xml/Dom/Xpath/Configurator/NamespacedFunctionsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace VeeWee\Tests\Xml\Dom\Xpath\Configurator;

use PHPUnit\Framework\TestCase;
use Psl\Type;
use VeeWee\Xml\Dom\Document;
use VeeWee\Xml\Dom\Xpath;

final class NamespacedFunctionsTest extends TestCase
{
public function test_it_can_evaluate_with_php_function(): void
{
$doc = Document::fromXmlString(
$xml = '<hello><item>Jos</item></hello>'
);
$xpath = Xpath::fromDocument($doc, Xpath\Configurator\namespaced_functions(
'http://my-ns',
'my',
['str_replace' => str_replace(...)]
));

$result = $xpath->evaluate('my:str_replace("J", "B", string(//item))', Type\string());
static::assertSame($result, 'Bos');
}
}
4 changes: 2 additions & 2 deletions tests/Xml/Xslt/Configurator/FunctionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public function test_it_can_use_php_functions(): void
{
$processor = Processor::fromTemplateDocument(
$this->createTemplate(),
functions(['strtoupper'])
functions(['strtoupper' => strtoupper(...)])
);

$result = $processor->transformDocumentToString(
Expand All @@ -36,7 +36,7 @@ public function test_it_throws_exception_on_unkown_function(): void
{
$processor = Processor::fromTemplateDocument(
$this->createTemplate(),
functions(['substr'])
functions(['substr' => substr(...)])
);
$doc = Document::fromXmlString(
<<<EOXML
Expand Down
70 changes: 70 additions & 0 deletions tests/Xml/Xslt/Configurator/NamespacedFunctionsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);

namespace VeeWee\Tests\Xml\Xslt;

use PHPUnit\Framework\TestCase;
use VeeWee\Xml\Dom\Document;
use VeeWee\Xml\Exception\RuntimeException;
use VeeWee\Xml\Xslt\Processor;
use function VeeWee\Xml\Xslt\Configurator\namespaced_functions;

final class NamespacedFunctionsTest extends TestCase
{
public function test_it_can_use_php_functions(): void
{
$processor = Processor::fromTemplateDocument(
$this->createTemplate(),
namespaced_functions('http://my-xls', ['strtoupper' => strtoupper(...)])
);

$result = $processor->transformDocumentToString(
Document::fromXmlString(
<<<EOXML
<root>
<hello>World</hello>
</root>
EOXML
)
);

static::assertSame('WORLD', $result);
}

public function test_it_throws_exception_on_unkown_function(): void
{
$processor = Processor::fromTemplateDocument(
$this->createTemplate(),
namespaced_functions('http://my-xls', ['substr' => substr(...)])
);
$doc = Document::fromXmlString(
<<<EOXML
<root>
<hello>World</hello>
</root>
EOXML
);

$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('strtoupper');
$processor->transformDocumentToString($doc);
}

private function createTemplate(): Document
{
return Document::fromXmlString(
<<<EOXML
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:my="http://my-xls"
xmlns:str="http://exslt.org/strings"
xmlns:xsdl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" omit-xml-declaration="yes" indent="no"/>
<xsl:template match="/root">
<xsl:value-of select="my:strtoupper(string(./hello))"/>
</xsl:template>
</xsl:stylesheet>
EOXML
);
}
}

0 comments on commit 5a4c3f9

Please sign in to comment.