【问题标题】:Constructor not being called from SOAP response object未从 SOAP 响应对象调用构造函数
【发布时间】:2016-04-20 09:28:45
【问题描述】:

我正在使用 PHP 的 SOAPClient class 与 SOAP API 进行通信。其中一个选项允许您使用自己的类重新映射 WSDL 文件中指定的类型:

classmap 选项可用于将某些 WSDL 类型映射到 PHP 类。此选项必须是一个数组,其中 WSDL 类型作为键,PHP 类的名称作为值。

我这样创建我的客户:

$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() 并收到以下错误:

注意:未定义的属性:Result::$data

当我收到 SOAP 响应时,如何调用我需要的构造函数?我尝试使用__wakeup(),但这似乎也不起作用。

附:我用一个小的解决方法“解决”了它,但我认为它并不理想。这是我所做的:

function getData(){
    if($this->data === NULL){
        parent::__construct();
    }

    return $this->data[$this->name];
}

【问题讨论】:

  • 你好,我只是好奇你在使用 $api->method('param')->getName() 时如何调用 getData 函数?请不要对我投反对票,我只是想学习一些东西...我认为该类应该具有要调用的方法的方法名称,但我可以看到任何方法 getName() 这就是我问的原因...提前谢谢你的答案:-)
  • @Redrif:这只是一个小错字,对不起。我试图从我的真实代码中复制并粘贴它(并更改事物的名称)。那实际上应该是$api->method('param')->getData()。这里没有魔法,只是问题中的一个错字。

标签: php soap soap-client


【解决方案1】:

您可以使用 __get 和 __set。

像这样写一个类

class Test
{
    private $container = [];

    public function __get($key)
    {
        return $this->container[$key] ?? null;
    }

    public function __set($key, $value)
    {
        $this->container[$key] = $value;
    }
}

您可以在需要时对按键执行额外的“魔法”

【讨论】:

  • 我也试过了。并且 __set 正在为 ResultObjects 工作(如果属性不是公共的),但是当您将对象传递给请求时,永远不会调用 __get
【解决方案2】:

更新:另一种解决方法

您可以将 SoapClient 包装在另一个可以正确调用构造函数的类中。为了省事,该类检查是否需要它。仍然不理想,但我认为这样更简单。

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类不需要有任何关于你自己逻辑的信息,你的逻辑也不需要改变。

问题

我认为这种行为是故意的或已知的错误(即,背后一定有一些原因或问题),因为在手册页中它是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;
                }
        }

...如果定义了类映射并且已知,则调用 zend_fetch_class()。

我相信应该在从节点获取的值上调用某种 ctor() 函数,例如在 PDO::fetchObject 中(参见文件“ext/pdo/pdo_stmt.c”)。

目前,这似乎没有完成。可能是因为 XML 树中对象的求值顺序,这使得向构造函数提供适当的参数变得很棘手,但在这一点上我只是猜测。

但是,您是正确的,当时没有“官方”解决方案(您无法获得比源代码更多的官方)。 p>

破解源代码

我试图在 PHP 源代码中添加一个构造函数运行器,只是为了它的地狱。不幸的是,我似乎需要几个不在我需要的范围内的变量,所以我必须更改几个结构来传递构造函数信息等等,而这些结构在 SOAP 代码中无处不在。

除了具有无参数构造函数且没有析构函数的对象的最简单情况之外,对代码的必要修改在我看来一点也不小。

【讨论】:

  • 我真的很喜欢你的ActiveSOAPClient 想法!这比在每个需要它的方法中都输入parent::__construct(); 更容易。
【解决方案3】:

这是已知行为 (bug report)。

按照错误报告中的建议(miceleparkip at web dot de):

这不是错误。这很正常。

soap 对象是在服务器端创建的。所以构造函数只是在服务器上调用。

我同意她的立场。

同一错误报告中的后续评论(php at hotblocks dot nl)不同意:

服务器不创建对象,它发送 XML。客户端解码该 XML 并创建对象。

虽然从技术角度来看这是无可争辩的事实,但“抽象”对象可以说是在服务器端创建的。是否先转换成 XML 再在客户端重构是底层的问题,应用层不需要知道。

如果您的应用程序需要比服务器提供的功能更多的对象,我将创建一个本地类,将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);

【讨论】:

  • 对我来说,这甚至看起来像是一种最佳实践方法,不仅限于此特定情况。
猜你喜欢
  • 1970-01-01
  • 2012-10-08
  • 2013-02-12
  • 2018-08-11
  • 1970-01-01
  • 1970-01-01
  • 2017-01-14
  • 2023-03-31
  • 2017-07-08
相关资源
最近更新 更多