未从 SOAP 响应对象调用构造函数
Constructor not being called from SOAP response object
我正在使用 PHP 的 SOAPClient class 与 SOAP API 通信。其中一个选项允许您使用自己的 类:
重新映射 WSDL 文件中指定的类型
The classmap option can be used to map some WSDL types to PHP classes. This option must be an array with WSDL types as keys and names of PHP classes as values.
我这样创建我的客户端:
$api = new SOAPClient('http://example.com/soap.wsdl', [
'location' => 'http://example.com/soap/endpoint',
'soap_version' => SOAP_1_2,
'compression' => SOAP_COMPRESSION_ACCEPT | SOAP_COMPRESSION_GZIP,
'cache_wsdl' => WSDL_CACHE_BOTH,
'classmap' => [
'APIResultObject' => 'Result'
],
# TODO: Set for debug only?
'trace' => TRUE,
'exceptions' => TRUE
]);
这有效,当我调用 $api->method('param')
时,我得到了一个 Result
对象(而不只是一个 StdClass
对象)。问题是从未调用 Result::__construct()
方法,因此从未设置 Result
的一些私有属性。
Result
是这样的:
class DataClass{
protected $data;
function __construct(){
$this->data = ['a' => 0, 'b' => 1, 'c' => 2];
}
}
class Result extends DataClass{
public $value, $name, $quantity;
function __construct(array $values){
parent::__construct();
foreach(['value', 'name', 'quantity'] as $var){
$this->$var = isset($values[$var]) ? $values[$var] : NULL;
}
}
function getData(){
return $this->data[$this->name];
}
}
我正在做 $api->method('param')->getData()
并收到以下错误:
Notice: Undefined property: Result::$data
如何在获取 SOAP 响应时调用我需要的构造函数?我尝试使用 __wakeup()
,但这似乎也不起作用。
P.S。我 "solved" 它有一个小的解决方法,但我认为它不是理想的。这是我所做的:
function getData(){
if($this->data === NULL){
parent::__construct();
}
return $this->data[$this->name];
}
更新:另一种解决方法
您可以将 SoapClient 包装在另一个 class 中,它将正确调用构造函数。为了省事,class 会检查它是否需要。仍然不理想,但我认为这样更简单。
class ActiveSOAPClient extends SoapClient {
// Intercept all calls to class.
public function __call($func, $params) {
// Pass it to parent class.
$ret = parent::__call($func, $params);
// If the answer is an object...
if (is_object($ret)
// Taboo functions that will pass unhindered.
// && (!in_array($func, [ ARRAY OF EXCLUDED_FUNCTIONS ]))
) {
// ...and the object is in the auto classmap...
if (false !== array_search(get_class($ret), $this->_classmap, true)) {
// ...then assume it's an incomplete object and try activating it.
$ret->__construct();
}
}
return $ret;
}
}
优点是ActiveSOAPClient
class不需要有任何关于你自己逻辑的信息,你的逻辑不需要改变。
问题
我认为这种行为是故意的或已知的错误(即,背后一定有某种原因或问题),因为在手册页中它是 already noted as of seven years ago。
我查看了 PHP 5.5.6 的源代码。据我所知,php_encoding.c、
中的代码
/* Struct encode/decode */
static zval *to_zval_object_ex(encodeTypePtr type, xmlNodePtr data, zend_class_entry *pce TSRMLS_DC)
{
zval *ret;
xmlNodePtr trav;
sdlPtr sdl;
sdlTypePtr sdlType = type->sdl_type;
zend_class_entry *ce = ZEND_STANDARD_CLASS_DEF_PTR;
zval *redo_any = NULL;
if (pce) {
ce = pce;
} else if (SOAP_GLOBAL(class_map) && type->type_str) {
zval **classname;
zend_class_entry *tmp;
if (zend_hash_find(SOAP_GLOBAL(class_map), type->type_str, strlen(type->type_str)+1, (void**)&classname) == SUCCESS &&
Z_TYPE_PP(classname) == IS_STRING &&
(tmp = zend_fetch_class(Z_STRVAL_PP(classname), Z_STRLEN_PP(classname), ZEND_FETCH_CLASS_AUTO TSRMLS_CC)) != NULL) {
ce = tmp;
}
}
...如果定义了 class 映射并且已知,则会调用 zend_fetch_class()。
我认为之后应该对从节点获取的值调用某种类型的 ctor() 函数,例如在 PDO::fetchObject 中(参见文件 "ext/pdo/pdo_stmt.c")。
目前,这似乎还没有完成。可能是因为 XML 树中对象的评估顺序,这使得向构造函数提供适当的参数变得棘手,但在这一点上我只是猜测。
但是,你是对的,当时没有"official"解决方案(你不能得到更多官方 比源代码)。
破解源代码
我试图在 PHP 源代码中添加一个 constructor-runner,只是为了它。不幸的是,我似乎需要几个不在我需要它们的范围内的变量,所以我必须更改几个结构来传递构造函数信息等等,而这些结构在 SOAP 代码中无处不在。
除了具有 parameter-less 构造函数且没有析构函数的对象的最简单情况外,对代码的必要修改在我看来一点也不小。
这是已知行为 (bug report)。
正如错误报告中有人建议的那样(miceleparkip at web dot de):
This is not a bug. It's quite normal.
The soap object is created on the server side. So the constructor is just called on the server.
我同意她的立场。
同一错误报告中的后续评论(php at hotblocks dot nl)不同意:
The server doesn't create objects, it sends XML. The client decodes that XML and creates the objects.
虽然从技术角度来看这无疑是正确的,但 "abstract" 对象可以说是在服务器端创建的。是否先转换成XML再在客户端重构是low-level的一个问题,应用层不需要知道。
如果您的应用程序需要的对象具有比服务器提供的功能更多的功能,我会创建一个本地 class,它将 SOAPClient
创建的对象作为构造函数参数:
class MySoapResultClass {
// whatever
}
class LocalApplicationClass {
public function __construct(MySoapResultClass $soapResult) {
// your local initialization code
$this->data = ['a' => 0, 'b' => 1, 'c' => 2];
// then either extract your data from $soapResult,
// or just store a reference to it
}
public function getData(){
return $this->data[$this->name];
}
}
$api = new SOAPClient(...);
$soapResult = $api->method('param');
$myUsefulObject = new LocalApplicationClass($soapResult);
您可以使用 __get 和 __set。
写一个class喜欢
class Test
{
private $container = [];
public function __get($key)
{
return $this->container[$key] ?? null;
}
public function __set($key, $value)
{
$this->container[$key] = $value;
}
}
您可以根据需要对按键执行额外的 "magic"
我正在使用 PHP 的 SOAPClient class 与 SOAP API 通信。其中一个选项允许您使用自己的 类:
重新映射 WSDL 文件中指定的类型The classmap option can be used to map some WSDL types to PHP classes. This option must be an array with WSDL types as keys and names of PHP classes as values.
我这样创建我的客户端:
$api = new SOAPClient('http://example.com/soap.wsdl', [
'location' => 'http://example.com/soap/endpoint',
'soap_version' => SOAP_1_2,
'compression' => SOAP_COMPRESSION_ACCEPT | SOAP_COMPRESSION_GZIP,
'cache_wsdl' => WSDL_CACHE_BOTH,
'classmap' => [
'APIResultObject' => 'Result'
],
# TODO: Set for debug only?
'trace' => TRUE,
'exceptions' => TRUE
]);
这有效,当我调用 $api->method('param')
时,我得到了一个 Result
对象(而不只是一个 StdClass
对象)。问题是从未调用 Result::__construct()
方法,因此从未设置 Result
的一些私有属性。
Result
是这样的:
class DataClass{
protected $data;
function __construct(){
$this->data = ['a' => 0, 'b' => 1, 'c' => 2];
}
}
class Result extends DataClass{
public $value, $name, $quantity;
function __construct(array $values){
parent::__construct();
foreach(['value', 'name', 'quantity'] as $var){
$this->$var = isset($values[$var]) ? $values[$var] : NULL;
}
}
function getData(){
return $this->data[$this->name];
}
}
我正在做 $api->method('param')->getData()
并收到以下错误:
Notice: Undefined property: Result::$data
如何在获取 SOAP 响应时调用我需要的构造函数?我尝试使用 __wakeup()
,但这似乎也不起作用。
P.S。我 "solved" 它有一个小的解决方法,但我认为它不是理想的。这是我所做的:
function getData(){
if($this->data === NULL){
parent::__construct();
}
return $this->data[$this->name];
}
更新:另一种解决方法
您可以将 SoapClient 包装在另一个 class 中,它将正确调用构造函数。为了省事,class 会检查它是否需要。仍然不理想,但我认为这样更简单。
class ActiveSOAPClient extends SoapClient {
// Intercept all calls to class.
public function __call($func, $params) {
// Pass it to parent class.
$ret = parent::__call($func, $params);
// If the answer is an object...
if (is_object($ret)
// Taboo functions that will pass unhindered.
// && (!in_array($func, [ ARRAY OF EXCLUDED_FUNCTIONS ]))
) {
// ...and the object is in the auto classmap...
if (false !== array_search(get_class($ret), $this->_classmap, true)) {
// ...then assume it's an incomplete object and try activating it.
$ret->__construct();
}
}
return $ret;
}
}
优点是ActiveSOAPClient
class不需要有任何关于你自己逻辑的信息,你的逻辑不需要改变。
问题
我认为这种行为是故意的或已知的错误(即,背后一定有某种原因或问题),因为在手册页中它是 already noted as of seven years ago。
我查看了 PHP 5.5.6 的源代码。据我所知,php_encoding.c、
中的代码/* Struct encode/decode */
static zval *to_zval_object_ex(encodeTypePtr type, xmlNodePtr data, zend_class_entry *pce TSRMLS_DC)
{
zval *ret;
xmlNodePtr trav;
sdlPtr sdl;
sdlTypePtr sdlType = type->sdl_type;
zend_class_entry *ce = ZEND_STANDARD_CLASS_DEF_PTR;
zval *redo_any = NULL;
if (pce) {
ce = pce;
} else if (SOAP_GLOBAL(class_map) && type->type_str) {
zval **classname;
zend_class_entry *tmp;
if (zend_hash_find(SOAP_GLOBAL(class_map), type->type_str, strlen(type->type_str)+1, (void**)&classname) == SUCCESS &&
Z_TYPE_PP(classname) == IS_STRING &&
(tmp = zend_fetch_class(Z_STRVAL_PP(classname), Z_STRLEN_PP(classname), ZEND_FETCH_CLASS_AUTO TSRMLS_CC)) != NULL) {
ce = tmp;
}
}
...如果定义了 class 映射并且已知,则会调用 zend_fetch_class()。
我认为之后应该对从节点获取的值调用某种类型的 ctor() 函数,例如在 PDO::fetchObject 中(参见文件 "ext/pdo/pdo_stmt.c")。
目前,这似乎还没有完成。可能是因为 XML 树中对象的评估顺序,这使得向构造函数提供适当的参数变得棘手,但在这一点上我只是猜测。
但是,你是对的,当时没有"official"解决方案(你不能得到更多官方 比源代码)。
破解源代码
我试图在 PHP 源代码中添加一个 constructor-runner,只是为了它。不幸的是,我似乎需要几个不在我需要它们的范围内的变量,所以我必须更改几个结构来传递构造函数信息等等,而这些结构在 SOAP 代码中无处不在。
除了具有 parameter-less 构造函数且没有析构函数的对象的最简单情况外,对代码的必要修改在我看来一点也不小。
这是已知行为 (bug report)。
正如错误报告中有人建议的那样(miceleparkip at web dot de):
This is not a bug. It's quite normal.
The soap object is created on the server side. So the constructor is just called on the server.
我同意她的立场。
同一错误报告中的后续评论(php at hotblocks dot nl)不同意:
The server doesn't create objects, it sends XML. The client decodes that XML and creates the objects.
虽然从技术角度来看这无疑是正确的,但 "abstract" 对象可以说是在服务器端创建的。是否先转换成XML再在客户端重构是low-level的一个问题,应用层不需要知道。
如果您的应用程序需要的对象具有比服务器提供的功能更多的功能,我会创建一个本地 class,它将 SOAPClient
创建的对象作为构造函数参数:
class MySoapResultClass {
// whatever
}
class LocalApplicationClass {
public function __construct(MySoapResultClass $soapResult) {
// your local initialization code
$this->data = ['a' => 0, 'b' => 1, 'c' => 2];
// then either extract your data from $soapResult,
// or just store a reference to it
}
public function getData(){
return $this->data[$this->name];
}
}
$api = new SOAPClient(...);
$soapResult = $api->method('param');
$myUsefulObject = new LocalApplicationClass($soapResult);
您可以使用 __get 和 __set。
写一个class喜欢
class Test
{
private $container = [];
public function __get($key)
{
return $this->container[$key] ?? null;
}
public function __set($key, $value)
{
$this->container[$key] = $value;
}
}
您可以根据需要对按键执行额外的 "magic"