php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Request #49567 DOM/XSL: Suggestion: registerObjectMethods()
Submitted: 2009-09-15 23:07 UTC Modified: 2010-11-24 14:41 UTC
From: mjs at beebo dot org Assigned:
Status: Open Package: XSLT related
PHP Version: 5.3.0 OS: OS X
Private report: No CVE-ID: None
View Add Comment Developer Edit
Welcome! If you don't have a Git account, you can't do anything here.
You can add a comment by following this link or if you reported this bug, you can edit this bug over here.
(description)
Block user comment
Status: Assign to:
Package:
Bug Type:
Summary:
From: mjs at beebo dot org
New email:
PHP Version: OS:

 

 [2009-09-15 23:07 UTC] mjs at beebo dot org
Description:
------------
A suggestion: it would be useful if there was a 
registerObjectMethods() that worked in a similar way to 
registerPHPFunctions() except that it took an object upon which 
methods could be called.  i.e. something like

class Greet {

    public function byName($name) {
        return "Hello, $name";
    }

}

$xml = DOMDocument::loadXML("<root/>");
$xsl = DOMDocument::loadXML("... <xsl:value-of 
select="php:call('byName', 'Michael')"/> ...");

$proc = new XSLTProcessor();
$proc->registerObjectMethods(new Greet());
$proc->importStyleSheet($xsl);

echo $proc->transformToXML($xml);


Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2009-09-22 07:14 UTC] mjs at beebo dot org
Had a look at how the xsl extension works, and had a shot at 
implementing this myself--the patch is below.  

Instead of adding a new function I enhanced registerPHPFunctions() so 
that it can take an object.  It works like this: if an object is 
provided (say $foo), then <xsl:value-of select="php:function('quux', 
44)"/> in the XSL results in a call to $foo->quux(44).

i.e. in PHP:

$foo = new Foo();

$processor->registerPHPFunctions($foo);

In XSL:

<xsl:value-of select="php:function('quux', 44)"/>

Two of the tests in ext/xsl failed, but they fail without this patch 
also.

The patch is:

Index: ext/xsl/tests/xsltprocessor_registerPHPFunctions-method.phpt
===================================================================
--- ext/xsl/tests/xsltprocessor_registerPHPFunctions-method.phpt	
(revision 0)
+++ ext/xsl/tests/xsltprocessor_registerPHPFunctions-method.phpt	
(revision 0)
@@ -0,0 +1,38 @@
+--TEST--
+Checks XSLTProcessor::registerPHPFunctions($obj) where the 
"functions"
+exposed in the XSL file are methods of $obj.
+--SKIPIF--
+<?php 
+if (!extension_loaded('xsl')) {
+    die("skip\n");
+}
+?>
+--FILE--
+<?php
+
+class Foo {
+    public function greet($name) {
+        return "Hello, $name";
+    }
+    public function add($i, $j) {
+        return $i + $j;
+    }
+}
+
+$xml = new DOMDocument();
+$xml->loadXML("<root/>");
+$xsl = new DOMDocument();
+$xsl->load(dirname(__FILE__) . "/phpmethod.xsl");
+
+$proc = new XSLTProcessor();
+$proc->importStylesheet($xsl);
+
+$foo = new Foo();
+
+$proc->registerPHPFunctions($foo);
+
+echo $proc->transformToXml($xml);
+--EXPECTF--
+<result greet="Hello, Clem" add="7"/>
+--CREDITS--
+Michael Stillwell <mjs@beebo.org>
\ No newline at end of file
Index: ext/xsl/tests/phpmethod.xsl
===================================================================
--- ext/xsl/tests/phpmethod.xsl	(revision 0)
+++ ext/xsl/tests/phpmethod.xsl	(revision 0)
@@ -0,0 +1,15 @@
+<xsl:stylesheet 
+  version="1.0"
+  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+  xmlns:php="http://php.net/xsl"
+  exclude-result-prefixes="php">
+  <xsl:output omit-xml-declaration="yes"/>
+<xsl:template match="/">
+  <result>
+  <!-- pass a single string argument -->
+  <xsl:attribute name="greet"><xsl:value-of 
select="php:function('greet', 'Clem')"/></xsl:attribute>
+  <!-- pass two integer arguments -->
+  <xsl:attribute name="add"><xsl:value-of select="php:function('add', 
3, 4)"/></xsl:attribute>
+  </result>
+</xsl:template>  
+</xsl:stylesheet>
\ No newline at end of file
Index: ext/xsl/php_xsl.h
===================================================================
--- ext/xsl/php_xsl.h	(revision 288545)
+++ ext/xsl/php_xsl.h	(working copy)
@@ -55,6 +55,7 @@
 	HashTable *node_list;
 	php_libxml_node_object *doc;
 	char *profiling;
+	zval *object_ptr;
 } xsl_object;
 
 void php_xsl_set_object(zval *wrapper, void *obj TSRMLS_DC);
Index: ext/xsl/xsltprocessor.c
===================================================================
--- ext/xsl/xsltprocessor.c	(revision 288545)
+++ ext/xsl/xsltprocessor.c	(working copy)
@@ -308,11 +308,11 @@
 	
 	fci.function_name = &handler;
 	fci.symbol_table = NULL;
-	fci.object_ptr = NULL;
 	fci.retval_ptr_ptr = &retval;
 	fci.no_separation = 0;
+	fci.object_ptr = intern->object_ptr ? intern->object_ptr : 
NULL;
 	/*fci.function_handler_cache = &function_ptr;*/
-	if (!zend_make_callable(&handler, &callable TSRMLS_CC)) {
+	if ((intern->registerPhpFunctions != 3) && 
!zend_make_callable(&handler, &callable TSRMLS_CC)) {
 		php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to 
call handler %s()", callable);
 		
 	} else if ( intern->registerPhpFunctions == 2 && 
zend_hash_exists(intern->registered_phpfunctions, callable, 
strlen(callable) + 1) == 0) { 
@@ -320,6 +320,9 @@
 		/* Push an empty string, so that we at least have an 
xslt result... */
 		valuePush(ctxt, xmlXPathNewString(""));
 	} else {
+		if (intern->object_ptr) {
+			fci.object_ptr = intern->object_ptr;
+		}
 		result = zend_call_function(&fci, NULL TSRMLS_CC);
 		if (result == FAILURE) {
 			if (Z_TYPE(handler) == IS_STRING) {
@@ -794,6 +797,7 @@
 	zval *id;
 	xsl_object *intern;
 	zval *array_value, **entry, *new_string;
+	zval *obj_ptr;
 	int  name_len = 0;
 	char *name;
 
@@ -823,6 +827,12 @@
 		zend_hash_update(intern->registered_phpfunctions, 
name, name_len + 1, &new_string, sizeof(zval*), NULL);
 		intern->registerPhpFunctions = 2;
 		
+	} else if (zend_parse_parameters_ex(ZEND_PARSE_PARAMS_QUIET, 
ZEND_NUM_ARGS() TSRMLS_CC, "o", &obj_ptr) == SUCCESS) {
+		intern = (xsl_object *)zend_object_store_get_object(id 
TSRMLS_CC);
+		zend_hash_clean(intern->registered_phpfunctions);
+		intern->object_ptr = obj_ptr;
+		Z_ADDREF_P(obj_ptr);
+		intern->registerPhpFunctions = 3;
 	} else {
 		intern = (xsl_object *)zend_object_store_get_object(id 
TSRMLS_CC);
 		intern->registerPhpFunctions = 1;
Index: ext/xsl/php_xsl.c
===================================================================
--- ext/xsl/php_xsl.c	(revision 288545)
+++ ext/xsl/php_xsl.c	(working copy)
@@ -84,6 +84,10 @@
 	zend_hash_destroy(intern->registered_phpfunctions);
 	FREE_HASHTABLE(intern->registered_phpfunctions);
 	
+	if (intern->object_ptr) {
+		Z_DELREF_P(intern->object_ptr);
+	}
+
 	if (intern->node_list) {
 		zend_hash_destroy(intern->node_list);
 		FREE_HASHTABLE(intern->node_list);
@@ -127,6 +131,7 @@
 	intern->node_list = NULL;
 	intern->doc = NULL;
 	intern->profiling = NULL;
+	intern->object_ptr = NULL;
 
 	zend_object_std_init(&intern->std, class_type TSRMLS_CC);
 	zend_hash_copy(intern->std.properties, &class_type-
>default_properties, (copy_ctor_func_t) zval_add_ref, (void *) &tmp, 
sizeof(zval *));
 [2009-09-22 13:30 UTC] chregu@php.net
Thanks for your work. I didn't test it out, but looking at the code 2 
questions pop up:

You can register exactly one object?
and
What happens, if there's a function with that name? eg. 'add' in your 
test?

Not sure, if I'm happy how it is right now, but it certainly goes into 
the right direction.
 [2009-09-23 09:10 UTC] mjs at beebo dot org
Thanks for your reply.

Yes, you can register exactly one object.  If you call 
registerPHPFunctions() twice, the functions/methods registered will be 
those of the second object.

If there's a function of the same name, it won't get called--
essentially if you have php:function('foo') in your XSL, foo() is 
either considered as a function in global scope (if you registered an 
array of strings, or nothing at all), or a method name in the "scope" 
of the registered object (if you registered an object).

This is not the most flexible approach but I think it's a simple and 
straightforward enhancement that requires no extra syntax on either 
the PHP or XSL side.  Also, anything more advanced is beyond my 
extension-writing capabilities!

I've made a few changes to the patch, and am working on a few more 
little things that don't affect the syntax.  Is here the right place 
to post it when I'm done?
 [2009-09-25 08:33 UTC] chregu@php.net
Thanks for your work. Here's the right place for the patches, but you 
could also send it to me or to php-internals, so that it won't get 
forgotten :)

chregu
 [2010-11-24 14:41 UTC] jani@php.net
-Package: Feature/Change Request +Package: XSLT related
 [2017-03-16 18:51 UTC] tom at r dot je
This would be really useful, at the moment I've needed to use an incredibly ugly hack to keep track of all instances by hash to workaround this and call functions on the correct instance.

It would be better if it was possible to do something like this:

```php

$xpath->registerPhpFunction('foo', function() {


});
```

That way, you could register any callback and give it a specific name. This would be more flexible that the suggested `registerObjectMethods()` 


For anyone finding this wanting a workaround here's an ugly hack that works:

```
<?php

class MyClass {
	private static $instances = [];

	public function runXpath() {
		$document = new \DomDocument();
		$xpath = new \DomXPath();
		$xpath->registerNamespace('php', 'http://php.net/xpath');
		$xpath->registerPhpFunctions();

		$hash = spl_object_hash($this);

		//Register this instance
		self::$instances[$hash] = $this;

		//Pass the object hash as part of the XPath query so it gets passed
		//to the static method `callFunction` 

		$results = $xpath->query('//[php:function("\MyClass::callFunction", "theFunctionIReallyWantToCall" "' . $hash . '", "arg1", .)]';	}
	}

	public static function callFunction($functionName, $instanceHash, ...$args) {
		//The static function has been called but we want to call a function
		//on the correct instance
		//Locate the instance by its hash
		$instance = self::$instances[$instanceHash];

		//now call the function on the instance with the remaining args
		return call_user_func($instance, $functionName, $args);
	}

	public function theFunctionIReallyWantToCall($arg1, \DomElement $element) {
		//do whatever with access to $this and class variables.
	}
}


```

This is ugly as hell but until this 8 year old feature request is implemented it's the best way to solve it.
 
PHP Copyright © 2001-2019 The PHP Group
All rights reserved.
Last updated: Fri Nov 15 17:01:42 2019 UTC