2016-01-14 26 views
12

Sto comunicando con un'API SOAP utilizzando PHP SOAPClient class. Una delle opzioni ci consente di rimappare i tipi specificati nel file WSDL con le proprie classi:Il costruttore non viene chiamato dall'oggetto risposta SOAP

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.

Creo il mio cliente in quanto tale:

$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 
]); 

Questo funziona e quando chiamo $api->method('param'), ho ottenere un oggetto Result indietro (anziché solo un oggetto StdClass). Il problema è che il metodo Result::__construct() non viene mai chiamato, quindi alcune proprietà private di Result non vengono mai impostate.

Ecco cosa 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]; 
    } 
} 

Quello che sta succedendo è che sto facendo $api->method('param')->getData() e ottenere il seguente errore:

Notice: Undefined property: Result::$data

Come posso chiamare la funzione di costruzione che ho bisogno di quando ottenere un SOAP risposta? Ho provato a utilizzare __wakeup(), ma non sembrava funzionare.

P.S. L'ho "risolto" con una piccola soluzione, ma non penso sia l'ideale. Ecco quello che ho fatto:

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

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

Ciao, im solo curioso come chiami la funzione getData quando usi $ api-> method ('param') -> getName()? Non mi sottovalutare, voglio solo imparare qualcosa ... pensavo che la classe dovesse avere il nome del metodo per il metodo da chiamare, ma posso vedere lì qualsiasi metodo getName() ecco perché sto chiedendo ... Thx in anticipo per la tua risposta :-) – Redrif

+0

@Redrif: Era solo un piccolo refuso, mi dispiace. Ho provato a copiare e incollare questo dal mio codice reale (e cambiare i nomi delle cose). Questo dovrebbe essere in realtà il metodo $ $ api-> ('param') -> getData() '. Nessuna magia qui, solo un refuso nella domanda. –

risposta

4

Comportamento noto (bug report).

Come someoned consigliato nella segnalazione di bug (miceleparkip al 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.

condivido la sua posizione.

Un commento successivo (php a hotblocks dot nl) nello stesso bug report non è d'accordo:

The server doesn't create objects, it sends XML. The client decodes that XML and creates the objects.

Mentre questo è indiscutibilmente vero da un pont di vista tecnico, l'oggetto "astratto" è senza dubbio creato sul lato server. Se viene prima convertito in XML e quindi ricostruito dal lato client, è una preoccupazione di basso livello di cui il livello dell'applicazione non deve essere a conoscenza.

Se l'applicazione ha bisogno di oggetti con più funzioni rispetto a quelle fornite dal server, vorrei creare una classe locale che prende l'oggetto creato dal SOAPClient come argomento del costruttore:

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); 
+1

Per me questo sembra addirittura un approccio di best practice in più di questo caso specifico. – FloydThreepwood

5

Aggiornamento: un'altra soluzione

Puoi avvolgere SoapClient in un'altra classe che richiamerà i costruttori in modo corretto. Per evitare problemi, la classe controlla se è necessario o meno. Ancora non è l'ideale, ma penso che sia più semplice in questo modo.

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; 
    } 
} 

Il vantaggio è che la classe ActiveSOAPClient non ha bisogno di avere tutte le informazioni sulla propria logica e la logica non ha bisogno di cambiare.

Il problema

Credo che questo comportamento è intenzionale o un bug noto (vale a dire, ci deve essere una ragione o per problemi alle spalle), perché nella pagina di manuale è already noted as of seven years ago.

Ho controllato il codice sorgente da PHP 5.5.6. Per quanto posso leggere il codice a 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; 
       } 
     } 

... se una mappa classe è definita, ed è noto, zend_fetch_class() viene richiamato.

Credo che una sorta di funzione ctor() debba essere chiamata in seguito sui valori recuperati dal nodo, come è fatto ad es. in PDO :: fetchObject (consultare il file "ext/pdo/pdo_stmt.c").

Attualmente, questo non sembra essere fatto. Forse è a causa dell'ordine di valutazione degli oggetti nell'albero XML, che rende difficile fornire argomenti appropriati al costruttore, ma a questo punto sto solo supponendo.

Tuttavia, lei ha ragione a non essendoci nessuna soluzione "ufficiale" al momento (non si può ottenere molto di più ufficiale che il codice sorgente).

Hacking la fonte

ho cercato di aggiungere un costruttore-runner nel codice sorgente PHP, solo per il gusto di farlo. Sfortunatamente, mi sembra di aver bisogno di diverse variabili che non rientrano nello scopo in cui ho bisogno di esse, quindi dovrei cambiare un paio di strutture per passare le informazioni del costruttore e così via, e quelle strutture sono usate ubiquamente nel codice SOAP.

Eccetto forse nel caso più semplice di un oggetto con costruttore senza parametri e senza distruttore, le modifiche necessarie al codice non mi sembrano affatto secondarie.

+0

Mi piace molto l'idea 'ActiveSOAPClient'! Questo rende più facile che dover mettere 'parent :: __ costruct();' in ogni metodo che lo richiede. –