Na sequência do artigo anterior, onde mostrámos como consumir WebServices com autenticação NTLM em PHP, apresentamos agora uma particularidade do método definido nesse artigo.

A dificuldade ocorre quando existe mais que uma operação, no WSDL, com o mesmo nome (ou seja, foram definidos overloaded methods). Para casos sem autenticação NTLM, a solução é simples, mas com NTLM a solução é um pouco mais complexa.

  1. Métodos com diferentes assinaturas (method overloading)
  2. Em PHP, como é possível especificar qual o métodos a consumir?
    1. WDSL com múltiplas definições de métodos
    2. Definir operação a consumir sem autenticação NTLM
    3. Definir operação a consumir com autenticação NTLM

1. Métodos com diferentes assinaturas (method overloading)

Para perceberes o caso em questão, existem várias linguagens de programação que permitem aquilo a que se chama de method overload, o que significa que podes declarar vários métodos com o mesmo nome e a única diferença prende-se com os parâmetros recebidos.

Como exemplo, podes considerar as seguintes assinaturas de métodos C#:

array ListaOpcoes (int start, int end)
array ListaOpcoes (int start, int end, int pageNumber, int perPage)

Isto quer dizer que em linguagens como C# ou Java é possível teres vários métodos com o mesmo nome (no exemplo, ListaOpcoes) mas que recebem diferentes parâmetros.

Como nota, o PHP não permite overload de métodos.

2. Em PHP, como é possível especificar qual o métodos a consumir?

Na maioria dos casos, basta passar um objeto ou um array, com a estrutura correta, para que seja invocado o método pretendido. Para casos em que se use autenticação NTLM é preciso alguma imaginação.

2.1. WDSL com múltiplas definições de métodos

Neste momento apresento um caso real. Se tens um caso semelhante, a melhor forma de diagnóstico é analisar o WSDL (descrição em Inglês).

No meu caso o XML tinha, entre outras coisas, o seguinte conteúdo:

<?xml version="1.0" encoding="utf-8"?>
    <wsdl:definitions xmlns:s="https://www.w3.org/2001/XMLSchema" xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/" xmlns:http="http://schemas.xmlsoap.org/wsdl/http/" xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/" xmlns:tns="http://www.url.pt/webservices" xmlns:s2="http://www.url.pt/webservices/AbstractTypes" xmlns:s1="http://microsoft.com/wsdl/types/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tm="http://microsoft.com/wsdl/mime/textMatching/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" targetNamespace="http://www.url.pt/webservices" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">
    (....)
    <s:element name="ListS">
        <s:complexType>
            <s:sequence>
                <s:element minOccurs="0" maxOccurs="1" name="query" type="s:string" />
                <s:element minOccurs="1" maxOccurs="1" name="pageNumber" type="s:int" />
                <s:element minOccurs="1" maxOccurs="1" name="pageSize" type="s:int" />
                <s:element minOccurs="0" maxOccurs="1" name="sortBy" type="s:string" />
            </s:sequence>
        </s:complexType>
    </s:element>
    <s:element name="List">
        <s:complexType>
            <s:sequence>
                <s:element minOccurs="0" maxOccurs="1" name="processQuery" type="tns:ProcessQuery" />
            </s:sequence>
        </s:complexType>
    </s:element>
    <s:complexType name="ProcessQuery">
        <s:sequence>
            <s:element minOccurs="0" maxOccurs="1" name="QueryGenericData" type="tns:QueryGenericData" />
            <s:element minOccurs="1" maxOccurs="1" name="MyProcessesOnly" type="s:boolean" />
            <s:element minOccurs="1" maxOccurs="1" name="IsAdminQuery" type="s:boolean" />
            <s:element minOccurs="0" maxOccurs="1" name="Status" type="tns:ArrayOfString" />
            <s:element minOccurs="1" maxOccurs="1" name="OrganizationName" type="s:boolean" />
            <s:element minOccurs="1" maxOccurs="1" name="KeysOnly" type="s:boolean" />
            <s:element minOccurs="0" maxOccurs="1" name="Classifications" type="tns:ArrayOfClassificationKey" />
            <s:element minOccurs="0" maxOccurs="1" name="Classification" type="tns:ClassificationKey" />
            <s:element minOccurs="0" maxOccurs="1" name="ProcessCode" type="s:string" />
            (....)
        </s:sequence>
        </s:complexType>
    (....)
    <wsdl:message name="ListSSoapIn">
        <wsdl:part name="parameters" element="tns:ListS" />
    <wsdl:message name="ListSoapIn">
        <wsdl:part name="parameters" element="tns:List" />
    </wsdl:message>
    (....)
    <wsdl:operation name="List">
        <wsdl:documentation xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">Get a result query in an XmlDocument.</wsdl:documentation>
        <wsdl:input name="ListS" message="tns:ListSSoapIn" />
        <wsdl:output name="ListS" message="tns:ListSSoapOut" />
    </wsdl:operation>
    <wsdl:operation name="List">
        <wsdl:documentation xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">Get a result query in an DataSet.</wsdl:documentation>
        <wsdl:input message="tns:ListSoapIn" />
        <wsdl:output message="tns:ListSoapOut" />
    </wsdl:operation>
    (....)
</wsdl:definitions>

O WSDL começa por indicar os tipos complexos. O elemento ListS é um tipo complexo composto por tipos simples (string, int, int, string) e o elemento List é um tipo complexo composto por um único ProcessQuery. Neste exemplo, não vamos dar interesse ao tipo de resultados.

A definição do ProcessQuery é um tipo composto por outros tipos complexos e tipos simples.

Mais abaixo no WSDL são apresentadas as mensagens. Para este exemplo, o WSDL tinha duas mensagens: ListSSoapIn que recebe um objeto do tipo tns:ListS e outra mensagem, ListSoapIn, que recebe um objeto do tipo tns:List.

Por fim, percebemos que existem duas operações com o nome List, sendo que uma tem como input a mensagem tns:ListSSoapIn e outra tem como input a mensagem tns:ListSoapIn.

2.2. Definir operação a consumir sem autenticação NTLM

Por norma é bastante simples indicar qual o método que se pretende consumir, bastando para isso passar o objeto correto na chamada do webservice. Por exemplo, para consumir o método List, com a mensagem tns:ListS, bastaria executar o seguinte código (exemplo usando o SoapClient):

$client = new SoapClient("my.wsdl");
$parameters = array(
    'query'      => '',
    'pageNumber' => 1,
    'pageSize'   => 10,
    'sortBy'     => 'name
);
$result = $client->List($parameters);

Outra possibilidade é a criação de um objeto com as propriedades corretas, usando o stdClass (mais informação neste post do Stackoverflow - em inglês). Por exemplo:

$client = new SoapClient("my.wsdl");
$parameters = new stdClass();
$parameters->query = '';
$parameters->pageNumber = 1;
$parameters->pageSize = 10;
$parameters->sortBy = 'name';

$result = $client->List($parameters);

2.3. Definir operação a consumir com autenticação NTLM

A invocação de uma operação especifica com autenticação NTLM é mais complexa. A razão é simples, para que a autenticação NTLM funcione é necessário extender o SoapClient e, com isso, o método __doRequest(), que é responsável pela execução dos pedidos ao WebService.

A forma como eu ultrapassei a questão foi através da definição do SoapAction e a criação do envelope XML que é enviado. Deixei de usar a classe SoapClient e passei a fazer um cURL para consumir o WebService. Aqui fica o código para consumir o método List com o elemento tns:ProcessQuery:


// Criação do envelope XML
$request = '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:web="http://www.url.pt/webservices">
    <soapenv:Header/>
    <soapenv:Body>
        <web:List>
            <web:processQuery>
            <web:ProcessCode>' . $params->ProcessCode . '</web:ProcessCode>
            <web:Year>' . $params->Year . '</web:Year>
            <web:NumberFrom>' . $params->NumberFrom . '</web:NumberFrom>
            <web:NumberTo>' . $params->NumberTo . '</web:NumberTo>
            <web:Subject>' . $params->Subject . '</web:Subject>
            </web:processQuery>
        </web:List>
    </soapenv:Body>
</soapenv:Envelope>';

// Cria o objeto cURL e define as propriedades por defeito
$soap_do = curl_init();
curl_setopt($soap_do, CURLOPT_URL, 'my.wsdl);
curl_setopt($soap_do, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($soap_do, CURLOPT_TIMEOUT, 10);
curl_setopt($soap_do, CURLOPT_RETURNTRANSFER, true);
curl_setopt($soap_do, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($soap_do, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($soap_do, CURLOPT_POST, true);
// Aqui definimos que o conteúdo do post é o envelope (XML) criado anteriormente
curl_setopt($soap_do, CURLOPT_POSTFIELDS, $request);

// Esta opção permite monitorizar os pedidos através do Fiddler
// @see https://www.telerik.com/fiddler
curl_setopt($soap_do, CURLOPT_PROXY, '127.0.0.1:8888');

// Neste momento estamos a definir qual é o correto SOAPAction. O SOAPAction deve ter o mesmo nome do elemento (s:element),
// neste caso seria List ou ListS.
curl_setopt($soap_do, CURLOPT_HTTPHEADER, array(
    'Method: POST',
    'User-Agent: PHP-SOAP-CURL',
    'Content-Type: text/xml; charset=UTF-8',
    'SOAPAction: "http://www.myurl.pt/webservices/List"',
    'Accept-Encoding: gzip,deflate',
));

// Define o correcto encoding do pedido
curl_setopt($soap_do, CURLOPT_ENCODING, "gzip");

// Por fim, e sem esquecer que estamos a trabalhar com autenticação NTLM, temos de definir o tipo de
// autenticação e o utilizador e password
curl_setopt($soap_do, CURLOPT_HTTPAUTH, CURLAUTH_NTLM);
curl_setopt($soap_do, CURLOPT_USERPWD, 'domain\user:password');

// Executa o pedido e obtem a resposta do WebService
$curlResponse = curl_exec($soap_do);

Como se pode analisar do código apresentado, a solução passa por criar o próprio envelope XML e criar todo o cURL manualmente. Desta forma, o $request tem o formato correto e garantimos que o SOAPAction (a operação) executado é o que pretendemos.

Na verdade a solução não é muito elegante devido à necessidade de definir o XML em vez de ser criado automaticamente. Mas este caso é, de facto, bastante específico e não deverá ser regra para os casos em que é possível usar o SoapClient.