Cómo generar el WSDL de un Webservice PHP

Un suscriptor del newsletter de Leeway Academy me envía el siguiente correo:

Hola. muy bueno los mails, concuerdo con lo de los errores del otro día.

Pero estoy atrapado en un problema.  Genere un Web service, me anda bárbaro, pero la gente que va a consumirlo en un sistema hecho en C# no puede procesar el WSDL que generé, me tiran errores.

Lo estoy haciendo a mano.

Hay alguna herramienta que se pueda usar?

Muchas gracias.

Ouch. Que temita que son los archivos WSDL, ¿no?

Para dar un poquito más de contexto, se trata de un webservice de tipo SOAP desarrollado usando PHP.

En general, el protocolo SOAP, aunque su nombre incluya la palabra Simple, dista bastante de serlo.

Los XML que hay que enviar y recibir aportan bastante a la confusión general.

Claro que, si se utilizan las herramientas adecuadas todo se vuelve mucho más sencillo.

Yendo al problema que dio origen a este post, la clave está en «Lo estoy haciendo a mano.«.

En línea general, es siempre preferible usar alguna librería ya hecha ya que, seguramente estará ampliamente testeada.

En php disponemos de buenas herramientas para trabajar con XML pero para SOAP en particular pueden ser un poco rústicas. Mejor usar otra más específica: las clases provistas por la extensión Soap, en particular para este caso SoapServer.

Con este par de herramientas tenemos cubierto el 80% de nuestras necesidades. Falta un detalle nomás: SoapServer no tiene la capacidad (Al menos hasta la versión 8.1 de php) de generar el archivo de definición del WebService: el WSDL.

¿Significa eso que tenemos que hacerlo a mano?

Claro que es una posibilidad, se puede generar un archivo como este:

<definitions name="Greetings" targetNamespace="http://localhost:8000/greetings_server.php" xmlns="http://schemas.xmlsoap.org/wsdl/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:tns="http://localhost:8000/greetings_server.php" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap-enc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/">
   <types>
      <xsd:schema targetNamespace="http://localhost:8000/greetings_server.php"/>
   </types>
   <portType name="GreetingsPort">
      <operation name="sayHello">
         <documentation>sayHello</documentation>
         <input message="tns:sayHelloIn"/>
         <output message="tns:sayHelloOut"/>
      </operation>
      <operation name="sayGoodBye">
         <documentation>sayGoodBye</documentation>
         <input message="tns:sayGoodByeIn"/>
         <output message="tns:sayGoodByeOut"/>
      </operation>
   </portType>
   <binding name="GreetingsBinding" type="tns:GreetingsPort">
      <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
      <operation name="sayHello">
         <soap:operation soapAction="http://localhost:8000/greetings_server.php#sayHello"/>
         <input>
            <soap:body use="literal" namespace="http://localhost:8000/greetings_server.php"/>
         </input>
         <output>
            <soap:body use="literal" namespace="http://localhost:8000/greetings_server.php"/>
         </output>
      </operation>
      <operation name="sayGoodBye">
         <soap:operation soapAction="http://localhost:8000/greetings_server.php#sayGoodBye"/>
         <input>
            <soap:body use="literal" namespace="http://localhost:8000/greetings_server.php"/>
         </input>
         <output>
            <soap:body use="literal" namespace="http://localhost:8000/greetings_server.php"/>
         </output>
      </operation>
   </binding>
   <service name="GreetingsService">
      <port name="GreetingsPort" binding="tns:GreetingsBinding">
         <soap:address location="http://localhost:8000/greetings_server.php"/>
      </port>
   </service>
   <message name="sayHelloIn">
      <part name="name" type="xsd:string"/>
   </message>
   <message name="sayHelloOut">
      <part name="return" type="xsd:string"/>
   </message>
   <message name="sayGoodByeIn">
      <part name="name" type="xsd:string"/>
   </message>
   <message name="sayGoodByeOut">
      <part name="return" type="xsd:string"/>
   </message>
</definitions>

Y ciertamente el uso de SimpleXML será de gran ayuda… pero para hacerlo exitosamente habrá que conocer bien los detalles del protocolo SOAP y el manejo de Namespaces XML.

¿No existe acaso una librería que lo haga todo?

Librerías para generar WSDL a partir de PHP

Esa misma pregunta me hice yo, así que fui a buscar y encontré algunas opciones:

Los probé todos (y algunos más que no recuerdo en este momento) y ninguno funcionaba al 100%.

Algunos son tan viejos que ni tienen mantenimiento y han quedado obsoletos.

Otros resolvían parcialmente el problema.

Estaba a punto de darme por vencido cuando finalmente apareció una luz al final del túnel: la clase AutoDiscover del paquete SOAP de Laminas.

A partir de ahí todo se volvió cuesta abajo.

Uso de Laminas Soap\AutoDiscover

Partiendo de este código que tenía para ejemplificar:

<?php

class GreetingsServer
{
    function sayHello(string $name): string
    {
        return "Hello $name!";
    }

    function sayGoodBye(string $name): string
    {
        return "Goodbye $name!";
    }
}

$server = new SoapServer(__DIR__.'/grettings.wsdl');

$server->setClass(GreetingServer::class);
$server->handle();

Le agregué la dependencia usando composer:

composer require laminas/laminas-soap

Luego se trata de discriminar el caso de que se esté solicitando el wsdl o no. Basta con un simple if:

if (array_key_exists('wsdl', $_GET)) {
    header('Content-Type: application/wsdl+xml');
    die($wsdl);
}

Y ahora viene la parte interesante… ¿de dónde sale el contenido de $wsdl?

Precisamente, de usar un objeto Laminas\Soap\AutoDiscover:

$wsdl = (new AutoDiscover())
    ->setClass(GreetingsServer::class)
    ->setUri(SERVER_URI)
    ->setServiceName('Greetings')
    ->setOperationBodyStyle([
        'use' => 'literal'
    ])
    ->setBindingStyle(
        [
            'style' => 'rpc'
        ])
    ->generate()
    ->toXml();

En el caso de que no se pida el wsdl (Es decir, que el QueryString sea diferente de ?wsdl), se debe crear el servidor usando un archivo ya generado:

$server = new SoapServer(__DIR__ . '/greetings.wsdl');
$server->setClass(GreetingsServer::class);
$server->handle();

¿Cómo se genera ese archivo? Una forma muy simple es hacer un request a este mismo servicio y guardarlo en el mismo directorio:

curl http://localhost:8000/greetings_server.php\?wsdl > greetings.wsdl

En mi caso dice localhost:8000 porque es ahí donde está levantado el WebService pero esta URL debería cambiar según dónde tengas todo montado.

Y listo, con esto se logra el bendito archivo WSDL que otros pueden consumir.

Si querés ver el código completo podés descargarlo de GitHub.

Ejemplo de clientes

El ejemplo no estaría completo sin ver el cliente, ¿cierto?

Empecemos por una versión en PHP:

<?php

$client = new SoapClient(
    'http://localhost:8000/greetings_server.php?wsdl',
    [
        'soap_version' => SOAP_1_2,
        'cache_wsdl' => WSDL_CACHE_NONE,
    ]);

if ($argc === 1) {
    echo "Service description:" . PHP_EOL;
    echo "--------------------" . PHP_EOL;
    echo "Functions:" . PHP_EOL;
    print_r($client->__getFunctions());
    echo "=====" . PHP_EOL;
    echo "Types:" . PHP_EOL;
    print_r($client->__getTypes());
} elseif ($argv[1] === "h") {
    echo $client->sayHello($argv[2]);
} elseif ($argv[1] === "g") {
    echo $client->sayGoodBye($argv[2]);
}

Con este pequeño script de línea de comandos es posible ver la definición del webservice y ejecutar ambos servicios como si se tratara de llamadas locales.

Y ahora, para ver un ejemplo algo más jugoso, veamos un script de python que consume este webservice:

from zeep import Client
import sys

client = Client('http://localhost:8000/greetings_server.php?wsdl')

cmd = sys.argv[1]

if cmd == "h":
    print (client.service.sayHello(sys.argv[2]))
else:
    print (client.service.sayGoodBye(sys.argv[2]))

Usando el comando python3.8 client.py h Mauro obtenemos Hello Mauro! y con python3.8 client.py g Mauro obtenemos Goodbye Mauro!

Y así vemos cómo es posible integrar dos aplicaciones desarrolladas en dos lenguajes diferentes usando un WebService SOAP.

Cómo manejar el cambio de definición del WebService

Es muy probable que, durante el desarrollo al menos, la definición del servicio cambie y, por lo tanto, el WSDL también debería cambiar.

Teniendo en cuenta que el WSDL es un archivo estático, basta con refrescarlo cada vez que cambie el servicio (o con una periodicidad razonable).

Esto se puede lograr a través de, entre otras:

Ahora sí, dicho esto, se acabó el misterio de los archivos WSDL

mchojrin

Por mchojrin

Ayudo a desarrolladores PHP a acceder mercados y clientes más sofisticados y exigentes

¿Te quedó alguna duda? Publica aca tu pregunta

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.