Конструктор, не вызываемый из объекта ответа SOAP

Я общаюсь с SOAP API, используя PHP класс SOAPClient. Один из вариантов позволяет переназначить типы, указанные в файле 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() и получаю следующую ошибку:

Примечание: Undefined свойство: Результат:: $data​​p >

Как я могу вызвать функцию конструктора, которая мне нужна при получении ответа SOAP? Я попытался использовать __wakeup(), но это тоже не сработало.

P.S. Я "решил" его с небольшим обходным решением, но я не думаю, что он идеален. Вот что я сделал:

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

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

Ответ 1

Это известное поведение (отчет об ошибке).

Как сообщается в отчете об ошибке (miceleparkip at web dot de):

Это не ошибка. Это нормально.

Мыльный объект создается на стороне сервера. Поэтому конструктор просто вызывается на сервере.

Я разделяю ее позицию.

Следующий комментарий (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);

Ответ 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...
            $key    = array_search(get_class($ret), $this->_classmap, true);
            if ($key !== false) {
                // ...then assume it an incomplete object and try activating it.
                $ret->__construct();
            }
        }
        return $ret;
    }
}

Преимущество состоит в том, что класс ActiveSOAPClient не должен иметь никакой информации о вашей собственной логике, и ваша логика не нуждается в изменении.

Проблема

Я думаю, что это поведение преднамеренное или известная ошибка (т.е. должна быть какая-то причина или проблема), потому что на странице руководства это уже отмечено от семи лет назад.

Я проверил исходный код с 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() должна вызываться впоследствии по значениям, полученным из node, как это сделано, например, в PDO:: fetchObject (см. файл "ext/pdo/pdo_stmt.c" ).

В настоящее время это, похоже, не выполняется. Возможно, это связано с порядком оценки объектов в дереве XML, что затрудняет предоставление конструкторам подходящих аргументов, но на данный момент я просто догадываюсь.

Однако вы правы в том, что в то время не было "официального" решения (вы не можете получить гораздо более официальный, чем исходный код).

Hack (серверная сторона)

Я попытался добавить конструктора-бегуна в исходный код PHP, просто для черта его. К сожалению, мне, похоже, нужны несколько переменных, которые не входят в область, в которой они мне нужны, поэтому мне пришлось бы изменить пару структур для передачи информации о конструкторах и т.д., И эти структуры используются повсеместно в коде SOAP.

За исключением, возможно, в простейшем случае объекта с конструктором без параметров и без деструктора, необходимые изменения в коде не выглядят вообще незначительными.