PHP 5 et l'extension SOAP

Introduction

Cet article décrit la nouvelle extension SOAP pour PHP. Il a pour objectif de montrer la mise en œuvre des services Web, ou bien l'utilisation de SOAP pour accéder à des services Web existant. Le présent article suppose une certaine familiarité avec les services Web, SOAP et WSDL (Web Services Description Language).

Le protocole SOAP

SOAP (Simple Object Access Protocol) est un protocole léger basé sur le langage XML pour l'échange d'informations structurées entre des applications distribuées. SOAP spécifie les formats des messages XML, la manière dont ces messages doivent être traités etc...

Les services Web sont une technologie moderne et populaire. La liste des protocoles et technologies en relation avec les services Web croît chaque jour, mais le protocole SOAP est de loin le plus important et celui qui tend à devenir le protocole standard pour l'accès aux services Web. Le protocole SOAP utilise des messages XML pour échanger des informations entre différents points.

L'extension SOAP de PHP5 est la première implémentation du protocole SOAP en C pour PHP. Elle possède certains avantages par rapport aux implémentations existantes écrites en PHP directement, le principal avantage concernant la vitesse d'exécution. L'extension est encore marquée comme expérimentale mais devrait entrer dans un état stable et fiable très rapidement.

L'extension SOAP comprend les spécifications SOAP 1.1, SOAP 1.2 et WSDL 1.1. L'objectif clé est l'utilisation de la fonctionnalité RPC du protocole SOAP. WSDL est utilisé lorsque cela est possible dans le but de rendre l'implémentation d'un service Web plus efficacement.

Un premier client simple SOAP en PHP

Pour démontrer comment mettre en œuvre un client SOAP, le service Web de demo « Delayed Stock Quote » du site XMethods sera utilisé comme cible. Avant de coder la moindre ligne de code PHP, quelques informations doivent être collectées à propos de ce service en particulier :

  • le nom de la méthode RPC (getQuote)
  • l'URL où le service tourne
  • l'entête SOAPAction pour la méthode
  • le namespace URI de la méthode
  • les paramètres d'entrée et de sortie et les typages de la méthode

Le site http://www.xmethods.com donne le profil du service RPC qui sera utilisé dans la démo.

Nom de la méthode getQuote
URL http://66.28.98.121:9090/soap
SOAPAction urn:xmethods-delayed-quotes#getQuote
Namespace URI urn:xmethods-delayed-quotes
Paramètres en entrée (Input Parameters) Symbol String
Paramètres de sortie (Output Parameters) Result float

Client PHP SOAP (sans description WSDL)

L'architecture d'appel RPC de la méthode getQuote du Web Service peut être résumée avec le schéma suivant :

Le script qui suit présente la mise en œuvre d'un client PHP SOAP pour le service Web getQuote sans utilisation d'une description WSDL du service Web getQuote.

<?php
    $client = new SoapClient (NULL, array(
              "location" => "http://66.28.98.121:9090/soap",
              "uri"      => "urn:xmethods-delayed-quotes",
              "style"    => SOAP_RPC,
              "use"      => SOAP_ENCODED
               ));
    
    print($client->__call(
       
       /* Nom de la méthode SOAP */
       "getQuote",
       
       /* Paramètres */
       array(
            new SoapParam(
              /* Valeur du Paramètre */
              "ibm",
              /* Nom du Paramètre */
              "symbol"
            )
        ),
        
        /* Options */
        array(
              /* Namespace de la méthode SOAP */
              "uri" => "urn:xmethods-delayed-quotes",
              /* SOAPAction HTTP Header pour la méthode SOAP */ 
              "soapaction" => "urn:xmethods-delayed-quotes#getQuote")). "\n");

?>

Sans description WSDL, cette simple tâche requiert beaucoup de travail et de paramétrage.

Client PHP SOAP (avec description WSDL)

Heureusement, les services Web peuvent se décrire eux mêmes au client en utilisant le WSDL (Web Service Description Language).

Voici le même client PHP SOAP écrit en utilisant le document WSDL. Beaucoup de paramètres n'ont plus à être spécifiés, ces dernières informations étant renseignées directement dans le fichier WSDL.

<?php
    $client = new SoapClient (
       "http://services.xmethods.net/soap/urn:xmethods-delayed-quotes.wsdl"
    );
    
    print($client->getQuote("ibm"));
?>

Ce procédé est nettement plus simple.

WSDL et PHP

Quels sont les problèmes avec WSDL ?  Le seul argument contraire à l'utilisation des documents WSDL réside dans la nécessité au client de charger le document WSDL à partir du serveur avant que l'appel de la méthode distante ne soit effectuée, ceci pouvant prendre un temps significatif dans un environnement Web.

L'extension SOAP propose une fonctionnalité de cache WSDL pour contrer ce problème. Le cache WSDL est mis en œuvre avec le paramètre d'initialisation soap.wsdl_cache_enabled au niveau du fichier php.ini ou bien en utilisant ini_set().

Les différentes directives gérant le cache WSDL sont les suivantes :

[soap] 
soap.wsdl_cache_enabled = "1" 
; enables or disables WSDL caching feature 
soap.wsdl_cache_dir = "/tmp" 
; sets the directory name where SOAP extension will put cache files 
soap.wsdl_cache_ttl = "86400" 
; (time to live) sets the number of second while cached file will be used 
; instead of original one

Par défaut, le cache WSDL est activé et les fichiers sont mis en cache pour un jour.

Client SOAP et Google

Dans cet exemple, une classe permettant d'insérer les résultats du moteur de recherche Google sur un site est mise en œuvre. L'API Google est basée sur les protocoles SOAP et WSDL (XML).

Licence Google :

Pour pouvoir utiliser le Web Service de Google, il est nécessaire de récupérer un numéro de licence gratuite dans un contexte d'utilisation non commerciale.

API Google

Format de la requête

Voici la description des paramètres à envoyer pour effectuer une recherche. La casse est très importante dans ces paramètres.

Paramètre Type Description
key String Numéro de licence Google
q String La recherche. Cette recherche doit impérativement être encodée au format UTF-8
Start Integer Index de début de la recherche. Ce paramètre permet notamment de réaliser un affichage par page des résultats
maxResults Integer Nombre de résultats renvoyés par la requête
filter Boolean Ce paramètre permet de filtrer les résultats renvoyés en supprimant les contenus très similaires et/ou venant du même site
restricts String Ce paramètre permet de restreindre la recherche selon certains critères précis.
Il est possible de restreindre la recherche aux pages venant d'un certain pays, par exemple countryFR pour la France, ou encore à certains topics.
Google propose 4 topics : Gouvernement Américain (unclesam), Linux (linux), Macintosh (mac) et FreeBSD (bsd).
Il est possible de combiner les deux modes de restriction grâce à des opérateurs booléens : NOT (-), OR (|), AND (.) et de faire des regroupements : parenthèses ()
safeSearch Boolean Paramètre permettant de filtrer les contenus pour adultes
lr String Paramètre permettant de restreindre la recherche à certains pays
ie String Obsolète
oe String Obsolète

Format de la réponse

Paramètre Description
documentFiltering Booléen indiquant si les résultats de la recherche ont été filtrés
searchComments Commentaire renvoyé par Google, pouvant contenir notamment les mots clés non pris en compte
estimatedTotalResultsCount Estimation du nombre de résultats renvoyés par la requête
estimateIsExact Booléen indiquant si l'estimation précedente est juste
resultElements Tableau contenant les résultats de la recherche
searchQuery La requète que Google a reçu (Paramètre q de la recherche)
startIndex Index (-1) du premier résultat. Ce paramètre sert à réaliser un affichage par page des résultats
endIndex Index (-1) du dernier résultats
searchTips Aide renvoyée par Google contenant notamment des conseils pour la recherche
directoryCategories Liens vers les catégories de Google
searchTime Nombre à virgule flottante ( Float ) indiquant le temps que Google a mis pour effectuer la recherche
resultsElements [ summary ] Résumé de la page
resultsElements [ URL ] L'URL du lien renvoyé
resultsElements [ snippet ] Un extrait significatif de la page contenant les mots clés en gras
resultsElements [ title ] Titre du résultat renvoyé
resultsElements [ cachedSize ] Poids de la page dans le cache de Google + "k"
resultsElements [ relatedInformationPresent ] Booléen indiquant si le terme ''related'' (permettant d'obtenir des pages similaires) est supporté par cette URL

Exemple de classe client Google

Classe Cls_Google

Dans le présent tutoriel, la classe Cls_Google se charge d'adresser les requêtes et de traiter les réponses avec le Web Service Google :

http://api.google.com/GoogleSearch.wsdl

Les méthodes mises en œuvre dans cette classe sont les suivantes :

  • __construct
  • __destruct
  • f_search
  • f_error

La trame de la classe Cls_Google est la suivante :

if (! defined("_GOOGLE_LICENCE_KEY"))  {
      define("_GOOGLE_LICENCE_KEY","v70jh5lQFHLjCvjz2a8zFPjnEclQQcff");
}

class Cls_Google {
     //  URL du fichier WSDL de l'API Google
     private $wsdlUri = 'http://api.google.com/GoogleSearch.wsdl';
     
     public function __construct() {
     } // end function __construct
     
     public function f_search() {
     }
     
     public static function f_error() {
     }
     
     public function __destruct() {
     }
}  // end class Cls_Google
Méthode__construct

Le constructeur de la classe se charge de vérifier que l'extension SOAP est bien chargée et le cas échéant tente de le charger à la volée. Le constructeur est déclaré en mode public :

public function __construct() {
      // Vérification que l'extension soap est chargée
      
      if(!extension_loaded('soap')) {
        // Cette dernière n'est pas chargée, tentative de chargement
        if(!@dl('soap')) {
          // Le chargement n'est pas réalisé, exit
          die('Impossible d\'utiliser l\'extension SOAP');
        }
      }
      
      // L'extension est chargée.
      return true;
} // end function __construct
Méthode f_search

On passera toutes les possibilités de recherche en tant que paramètres de la méthode de recherche f_search, seul le premier paramètre search est obligatoire, les autres sont optionnels.

Il se peut que pour une raison x ou y une erreur se produise lors de la communication avec un serveur SOAP.

L'extension SOAP de PHP fournit une classe ( SoapFault ) qui permet de gérer ces erreurs.

Les exceptions sont donc gérées dans la méthode f_search grâce à la classe SoapFault.

public function f_search($search , $start=0, $maxresults=5, $filter=false,
        $restrict='', $safesearch=false , $lang='') {
        
    // Création du tableau contenant les paramètres à envoyer à Google.
    $params = array(
          'key' => _GOOGLE_LICENCE_KEY,
          'q' => utf8_encode($search),
          'start' => (int)$start,
          'maxResults' => (int)$maxresults,
          'filter' => (boolean)$filter,
          'restrict' => $restrict,
          'safeSearch' => (boolean)$safesearch,
          'lr' => $lang,
          'ie' => '',
          'oe' => ''
    );
          
    // on tente d'instancier le client SOAP
    try { $Client = new SoapClient($this->wsdlUri); }
    // Erreur : on gére une exception grâce à la classe SoapFault,retour de l'objet
    catch (SoapFault $fault) { return $fault; }
    
    // On tente d'interroger le service web.
    try { $O =  $Client->__call('DoGoogleSearch', $params); }
    // Erreur : on gére une exception grâce à la classe SoapFault, retour de l'objet
    catch (SoapFault $fault) { return $fault; }
    
    // tout s'est bien déroulé, on retourne le résultat.
    return $O;
}
Méthode f_error

La méthode f_error prend un objet SoapFault comme paramètre. Cette méthode est appelée uniquement de manière statique.

public static function f_error(SoapFault $fault) {
         echo 'SOAP Fault :<br/> <strong>FaultCode</strong> : ',
         $fault->faultcode,
         '<br/><strong>FaultString</strong> : ',
         $fault->faultstring;
}

Mise en œuvre de Cls_Google

L'exemple de mise en œuvre de la classe Cls_Google construit un formulaire simple permettant d'interroger l'API de Google et d'en afficher les résultats.

Le résultat renvoyé par la classe Cls_Google est analysée grâce à la fonction is_soap_fault().

<?php require_once(_FWK_PHPDIR."/Cls_Google.php");?>
<form method="post" action="<?php echo $_SERVER['PHP_SELF']; ?>">
  <fieldset>
    
    <legend> Votre recherche avec Google </legend>
    
    <div>
      <input type="text" name="search" tabindex="1" accesskey="s"/>
    </div>
    
    <div>
      <input type="submit" value="et hop" tabindex="2" accesskey="g"/>
    </div>
    
  </fieldset>
</form>

<hr/>
<?php
// Le formulaire a été soumis.
if(isset($_REQUEST['search'])) {
    $G = new Cls_Google();   // Nouvelle instance.
    $O = $G->f_search($_REQUEST['search']);    // Envoi de la requète au serveur SOAP.
    
    // Vérification du résultat renvoyé.
    if(is_soap_fault($O)) {
      // Affichage des erreurs a appeler de manière statique
      Cls_Google::f_error($O);
      die();
    }
    
    // Tout s'est bien passé , affichage sommaire du résultat.
    echo '<div>';
    echo '<div>NB results ::: ', $O->estimatedTotalResultsCount, '</div>';
    
    foreach($O->resultElements as $key => $val) {
      echo '<div>\/ ';
      echo '<a href="', $val->URL, '">', utf8_decode($val->title), '</a>';
      echo '</div>';
      echo '<p>';
      echo utf8_decode($val->snippet);
      echo '</p>';
      echo '<div>';
      echo '<a href="', $val->URL, '">', utf8_decode($val->URL), '</a> ---- ';
      echo 'en cache : ', $val->cachedSize;
      echo '</div>';
    }
    echo '</div>';
  }
?>

Debugging SOAP et format des messages

Le constructeur SoapClient( ) accepte un tableau associatif en second paramètre. Diverses options peuvent être passées dans ce tableau associatif, en voici deux :

  • trace : autorise le client à stocker les requêtes et réponses SOAP (désactivé par défaut)
  • exceptions : autorise le client à contrôler le mécanisme des exceptions (activé par défaut).

Les méthodes __getLastRequest et __getLastResponse permettent d'extraire les informations échangées.

<?php
  $client = new SoapClient("./wsdl/stockquote.wsdl",array(
      "trace"      => 1,
      "exceptions" => 0)
  );
      
  $client->getQuote("ibm");
      
  print "<pre>\n";
  print "Request :\n".htmlspecialchars($client->__getLastRequest()) ."\n";
  print "Response:\n".htmlspecialchars($client->__getLastResponse())."\n";
  print "</pre>";
?>
Request :
<?xml version="1.0" encoding="UTF-8"?>

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
  xmlns:ns1="urn:xmethods-delayed-quotes" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
  SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  
  <SOAP-ENV:Body>
    <ns1:getQuote>
      <symbol xsi:type="xsd:string">ibm</symbol>
    </ns1:getQuote>
  </SOAP-ENV:Body>
  
</SOAP-ENV:Envelope>


Response:
<?xml version="1.0" encoding="UTF-8"?>

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
  xmlns:ns1="urn:xmethods-delayed-quotes" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
  SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  
  <SOAP-ENV:Body>
    <ns1:getQuoteResponse>
      <Result xsi:type="xsd:float">98.42</Result>
    </ns1:getQuoteResponse>
  </SOAP-ENV:Body>
  
</SOAP-ENV:Envelope>

Serveur SOAP PHP

Il est possible avec l'extension PHP d'écrire un service Web ou serveur SOAP, la première étape consistant à créer un document WSDL décrivant le service et pouvant être interprété par le client.

Dans la démonstration, un service Web ou serveur SOAP extrait des informations d'un serveur MySQL, informations récupérées par un client SOAP avec la méthode getParameters :

Mise en œuvre du service Web SOAP

Description du fichier WSDL mySqlParms.wsdl

La première tâche consiste à créer le document WSDL mySqlParms.wsdl décrivant le service dans un format que la requête cliente sera capable d'interpréter.

La section message définit deux messages. Le premier message est getParametersRequest, message requête pour la méthode getParameters avec une chaîne de caractère en paramètre appelé _param. Le second message getParametersResponse est le message réponse de la méthode getParameters, contenant une valeur chaîne de caractères appelé _result.

La section portType définit une opération, getParameters, laquelle décrit quels sont les messages listés dans la section message et qui seront utilisés pour la requête et la réponse.

La section binding définit comment les messages sont transmis et encodés. C'est dans cette section que sont indiqués que les messages sont envoyés en mode RPC avec l'encodage SOAP à travers HTTP. Cette section décrit également le nom d'espace (namespace) et la valeur de l'entête SOAPAction pour la méthode getParameters.

En dernier lieu, la section service définit l'URL où le service est exécuté.

mySqlParms.wsdl
<?xml version ='1.0' encoding ='UTF-8' ?>
<definitions name='getParameters'
    targetNamespace='http://example.org/getParameters'
    xmlns:tns=' http://example.org/getParameters '
    xmlns:soap='http://schemas.xmlsoap.org/wsdl/soap/'
    xmlns:xsd='http://www.w3.org/2001/XMLSchema'
    xmlns:soapenc='http://schemas.xmlsoap.org/soap/encoding/'
    xmlns:wsdl='http://schemas.xmlsoap.org/wsdl/'
    xmlns='http://schemas.xmlsoap.org/wsdl/'>
    
<message name='getParametersRequest'>
  <part name='_param' type='xsd:string'/>
</message>

<message name='getParametersResponse'>
  <part name='_result' type='xsd:string'/>
</message>

<portType name='getParametersPortType'>
  <operation name='getParameters'>
    <input message='tns:getParametersRequest'/>
    <output message='tns:getParametersResponse'/>
  </operation>
</portType>

<binding name='getParametersBinding' type='tns:getParametersPortType'>
  <soap:binding style='rpc'
    transport='http://schemas.xmlsoap.org/soap/http'/>
    
    <operation name='getParameters'>
      <soap:operation soapAction='urn:mySqlParms#getParameters'/>

      <input>
        <soap:body use='encoded' namespace='urn:mySqlParms'
          encodingStyle='http://schemas.xmlsoap.org/soap/encoding/'/>
      </input>
      
      <output>
        <soap:body use='encoded' namespace='urn:mySqlParms'
          encodingStyle='http://schemas.xmlsoap.org/soap/encoding/'/>
      </output>
    </operation>
</binding>

<service name='getParametersService'>
  <port name='getParametersPort' binding='getParametersBinding'>
    <soap:address location='http://[insert real path here]/srv_mySqlParms.php'/>
  </port>
</service>

</definitions>
Note: La fonctionnalité de cache WSDL est activée par défaut. Durant le développement , cette fonctionnalité doit être désactivée car dans le cas contraire le rafraîchissement des fichiers WSDL n'est pas réalisée.

Mise en œuvre du serveur SOAP srv_mySqlParms.php

La seconde étape consiste à créer le serveur en implémentant la fonction getParameters( ), laquelle sera accédée comme une fonction service par les messages requête.

La seconde étape consiste à créer un objet SoapServer et de le connecter à la fonction service getParameters( ) en utilisant la méthode SoapServer ::addFunction( ). Le constructeur SoapServer( ) possède un seul paramètre : le chemin d'accès au document WSDL du service.

Le serveur SOAP peut fonctionner sans document WSDL comme le client SOAP mais ceci ne présente aucun intérêt compte tenu de la lourdeur de la mise en œuvre.

srv_mySqlParms.php
<?php
  $prmdb["db_user"]='root';
  $prmdb["db_database"]='mysql';
  $prmdb["db_pwd"]='';
  
  function getParameters($parm) {
    global $prmdb;
    $returnedValue = "";
    
    $link = mysql_connect("localhost", $prmdb["db_user"], $prmdb["db_pwd"]);
    mysql_select_db($prmdb["db_database"],$link);
    
    $sql="show variables like '".$parm."%'";
    
    $rsc = mysql_query($sql,$link);
    if ($rsc) {
      while ($row = mysql_fetch_assoc($rsc)) {
        $returnedValue=$row["Value"];
        break;
      }
    }
    else { throw new SoapFault("Error","Retrieve has failed, unknown variable"); }
    
    mysql_free_result($rsc);
    
    mysql_close($link);
    
    return $returnedValue;
    
  }
    
  ini_set("soap.wsdl_cache_enabled", "0"); // désactivation du cache WSDL
    
  $server = new SoapServer("../wsdl/mySqlParms.wsdl");
  $server->addFunction("getParameters");
  $server->handle();
?>

Dans la mesure du possible, il est nécessaire de gérer les erreurs. Le protocole SOAP spécifie un format spécial pour les erreurs. Pour générer de tels messages, il est simple de générer une exception en utilisant l'objet SoapFault. Le premier paramètre du constructeur de SoapFault est le code de l'erreur (String) et le second la description de l'erreur (String).

Mise en œuvre du client SOAP soap_client.php

Voici un exemple de client du service Web implémenté dans le paragraphe précédent

soap_client.php
<?php
  $client = new SoapClient("./wsdl/mySqlParms.wsdl");
  
  try {
    echo "<pre>\n";
    print($client->getParameters("version"));
    echo "\n";
    print($client->getParameters("version_comment"));
    echo "\n</pre>\n";
  }
  
  catch (SoapFault $exception) {
    echo $exception;
  }
?>

Recommandations sur l'écriture des services Web SOAP en PHP

Dans l'exemple précédent, la fonctionnalité du service Web est codée dans une fonction simple mais il faut savoir qu'il est possible d'encapsuler les fonctionnalités du service Web dans une classe PHP grâce à la méthode SoapServer ::setclass.

En implémentant une classe entière pour un service Web, toutes les méthodes de cette classe seront accessibles par SOAP sans avoir à les définir individuellement : c'est la méthode générale qu'il faut adopter.

Ci-après, voici une réécriture du service Web en respectant l'encapsulation de la fonction getParameters dans une classe cls_mySqlService ; pour ce qui concerne le client, aucune modification ne sera nécessaire :

ini_set("soap.wsdl_cache_enabled", "0"); // désactivation du cache WSDL
$server = new SoapServer("../wsdl/mySqlParms.wsdl");
$server->setClass("cls_mySqlService");
$server->handle();
class cls_mySqlService {
 
  private $prmdb = array("db_user"=>"root",
                    "db_database"=>"mysql",
                    "db_pwd"=>"");
        
  function getParameters($parm) {
        global $prmdb;
        $returnedValue = "";
        
        $link = mysql_connect("localhost", $this->prmdb["db_user"], $this->prmdb["db_pwd"]);
        mysql_select_db($this->prmdb["db_database"],$link);
        
        $sql="show variables like '".$parm."%'";
        
        $rsc = mysql_query($sql,$link);
        
        if ($rsc) {
          while ($row = mysql_fetch_assoc($rsc)) {
              $returnedValue=$row["Value"];
              break;
          }
        }
        else { throw new SoapFault("Error","Retrieve has failed, unknown variable"); }
        
        
        mysql_free_result($rsc);
        mysql_close($link);
        return $returnedValue;
  }
}