Re: DOMXPath / XSLTProcessor function callbacks
| From: | Niels Dossche | Date: | Mon, 16 Oct 2023 21:19:02 +0000 |
| Subject: | Re: DOMXPath / XSLTProcessor function callbacks | ||
| References: | 1 2 | Groups: | php.internals |
| Request: | Send a blank email to internals+get-121340@lists.php.net to get a copy of this message | ||
Hi Frederik
Sorry for the resend... I accidentally replied to you only without including the list the first
time.
On 15/10/2023 21:37, Frederik Bosch wrote:
> Dear Niels,
>
> First of all, thanks for all your hard work already on the DOM and SimpleXML extensions. I have
> been following your work in PHP-SRC, great! I am the author of this XSL 2.0 Transpiler in PHP
> package (https://github.com/genkgo/xsl). It is indeed possible to use workarounds for closures or
> object methods. I am relying on them in my package.
>
> My suggestion for the future would be to add the following method.
>
> public XSLTProcessor::registerFunctionsNS(string $namespace, array|ArrayAccess $functions):
> void
>
> Then a user can register functions like this.
>
> $xsltProcessorOrDomXpath->registerFunctionsNS('urn:my.namespace',
> array('upper-case', 'strtoupper', 'pow' => fn ($a, $b) =>
> $a[0]->textContent ** $b[0]->textContent, 'other' => [$obj, 'method']);
>
Interesting suggestion. So you want to be able to use something like
my.namespace:function(...) as I understand it.
I'm not sure that adding this is that much more beneficial though (complexity vs benefit
trade-off).
I assume this is motivated by the fact that you can then use third party libraries while having to
worry less about name clashes?
Let's say we add non-namespace registerFunction(string $name, callable $callback):
void, you can then still use a convention of using a prefix, thus _kinda_ achieving the same.
In any case, this is going to be hard to support in combination with the underlying library
(libxslt).
That's because the function namespace registration is process-wide, so this cannot be changed
at runtime and certainly not for ZTS SAPIs.
> The registered functions should use the same methodology as php:function(). Hence, string
> casting of arguments is something the library user should do. I would leave registerPHPFunctions as
> is, and maybe discourage it in favor of the method above. What if both are called? I think it would
> be most clear if the registerFunctionsNS method throws InvalidArgumentException when
> http://php.net/xsl or
> http://php.net/xpath is passed as namespace.
>
> Cheers,
> Frederik
Cheers
Niels
>
>
> On 13-10-2023 00:39, Niels Dossche wrote:
>> I'm looking to extend the functionality of calling PHP functions from within the
>> DOMXPath or XSLTProcessor classes.
>>
>> In case you're unfamiliar here's a quick rundown.
>> The DOMXPath class allows you to execute XPath queries on a DOM tree to lookup certain
>> nodes satisfying a filter.
>> PHP allows the user to execute function callbacks within these. For example (from the
>> manual):
>> $xpath->query('//book[php:functionString("substr", title, 0, 3) =
>> "PHP"]');
>> This will read the title element's text content, call substr on it, and then compare
>> the output against "PHP".
>> You can not only call builtin functions, but also user functions.
>>
>> To be able to call PHP functions, you need to use DOMXPath::registerPhpFunctions()
>> (https://www.php.net/manual/en/domxpath.registerphpfunctions.php).
>> You either pass in NULL to allow all functions, or pass in which function names are allowed
>> to be called.
>>
>> Similarly, XSLTProcessor has the same registerPhpFunctions() method.
>> For XSLT it's mostly used for performing arbitrary manipulations on input data.
>> Normally the output of the function is put into the resulting document.
>>
>>
>> So what's the problem?
>> The current system doesn't allow you to call closures or object methods.
>> There are tricks you can do with global variables and global functions to try to work
>> around this, but that's quite cumbersome.
>>
>> There are two feature requests for this on the old bugtracker:
>> - https://bugs.php.net/bug.php?id=38595
>> - https://bugs.php.net/bug.php?id=49567
>>
>> It's not hard to implement support for this, the question is just what API we should
>> go with.
>> Based on what I've read, there are at least two obvious options:
>>
>>
>> OPTION 1) Extend registerPHPFunctions() such that you can pass in callables
>>
>> ```
>> // Adapted from https://bugs.php.net/bug.php?id=38595
>> $xslt->registerPHPFunctions(array(
>> 'functionblah', // Like we used to
>> 'func2' => fn ($x) => ...,
>> 'func3' => array($obj, 'method'), // etc
>> ));
>> ```
>>
>> Example: Using php:function("func3") inside XPath/XSLT in this case will result
>> in calling method on $obj.
>> Similarly func2 will call the closure, and functionblah in the snippet just allowlists
>> calling functionblah.
>>
>> It's a backwards compatible solution and a natural extension to the current method.
>> It may be hard to discover this feature compared to having a new API though.
>>
>> Furthermore, once you pass in function names to registerPHPFunctions(), you're
>> restricting what can be called.
>> For example: imagine you want to call both ucfirst() and $obj->method(), so you pass in
>> an entry like func3 in the above example.
>> Now you have to pass in ucfirst to registerPHPFunctions() too, because
>> registerPHPFunctions() acts as an allowlist. May be a bit inconvenient.
>>
>>
>> OPTION 2) Add new methods to register / unregister callables
>>
>> This may be the cleaner way to go about it on first sight, but there's a potential BC
>> break when new methods clash in user-defined subclasses.
>>
>> Question here is: what about the interaction with registerPHPFunction?
>> What if both registerPHPFunction() and the register method add something with the same
>> name?
>> What if registerPHPFunction() didn't allowlist a function but the register method
>> added it, may be a bit confusing for users.
>> The interaction may be surprising.
>>
>>
>>
>> Please let me know your thoughts.
>>
>> Cheers
>> Niels
>>
>