Où sommes nous ?

Retour

Web Service et PHP

PHP n'est malheureusement pas une plateforme cible privilégiée pour créer des web services. Pourtant, PHP5 dispose d'une bonne interface SOAP qui rend la création de client vraiment triviale ... ça reste un peu plus sportif pour un serveur, mais on y reviendra.

Client PHP (exemple 1)

Mon premier exemple ici sera un simple "Echo", c'est-à-dire un web service qui renverra simplement la chaîne qu'on lui a fournit, le fournisseur étant ici un application server webMethods.

Un exemple très simple car les besoins de développements spécifiques sont généralement pour faire des clients. Et comme la partie serveur est plus complexe, je voulais commencer par faire peur.

Création du "provider" (côté webMethods)

Je ne vais pas entrer dans les détails, vu que ce n'est pas le propos de ce billet et c'est une chose relativement simple depuis la version 7.1. Il faut suivre la procédure suivante :

Et voila, c'est fini.

Bug ?

Plusieurs types sont disponibles pour le dernier paramètre ... mais je n'ai pas réussi à faire fonctionner "Document literal" : les paramètres d'entrées sont ignorés ...
Je ne sais pas de quel côté vient le problème, mais bon, vu le nombre de bugs auxquels je fais face tous les jours avec webMethods, je ne vais pas chercher plus loin : ça marche avec RPC literal point ...

Création du "Consumer" (côté PHP donc)

Le code pour un client est vraiment très simple et se résume à :

Et ca donne :

#!/usr/bin/php
<?php
/* Test d'appel a un WEBService */
$wsdl = 'http://monserveur.chez.moi/ws/STPrivateLF.TestWS:TstEchoWS?WSDL';
$options = array('cache_wsdl' => WSDL_CACHE_NONE);

try {
$soapclt = new SoapClient($wsdl, $options);
$res = $soapclt->Echo( 'coucou' );
echo "Ok !n";
var_dump($res);
} catch (SoapFault $fault) {
trigger_error("SOAP Fault: (faultcode: {$fault->faultcode}, faultstring: {$fault->faultstring})", E_USER_ERROR);
}
?>

Note : le tableau $options ne contient ici que la directive qui supprime le cache du fichier WSDL. Si on peut (doit) le faire pendant le développement, on gagnera grandement à le laisser activé pour une utilisation réelle.

On le lance et on obtient :

laurent@Nunux:~/projets/b2b/testWS$ ./tstEcho 
Ok !
string(27) "Your message was : 'coucou'"

Simple non ?

Serveur PHP (Provider)

Une des raison pour laquelle JAVA et .net sont souvent associés aux web services, c'est qu'ils sont fortement typés et donc qu'ils peuvent générer facilement des définitions WSDL. Avec PHP, faiblement typés, il n'y a pas à ce jour cette possibilité de génération automatique (Soyons clairs, on peut très bien se  passer du WSDL, mais c'est quant même tellement plus pratique ... ).

Comme ce problème n'est pas nouveau, quelques solutions sont apparues sur la toile ... plus ou moins abouties. Je me suis arrêté sur wsdl-writer qui fait du bon boulo sans être trop complexe. Heureusement d'ailleurs qu'il fonctionne bien car son créateur a abandonné son développement, repris par une certaine Katy ... dont le site n'est même plus en ligne (MàJ 2013 : son site est de retour ...). Mais, comme je l'ai dit, il a l'avantage de fonctionner.
J'ai pu le retrouver sur le site de placeoweb que je remercie au passage sinon j'aurai sans doute laissé tomber.

Mes recherches sur le web m'ont fait découvrir WSO2 WS/PHP qui semble une solution vraiment compléte, en particulier simplifiant la génération des WSDL.

Je ne l'utiliserai pas ici (et à vrais dire, je ne l'ai pas testé) car elle ne fait pas partie des packages d'Ubuntu et il faudrait donc la recompiler mais :

Installation et fonctionnement de wsdl-writer

C'est simple, il suffit d'extraire l'archive et de modifier le fichier bin/wsdl pour qu'il trouve le répertoire contenant ses classes.

bin/wsdl est un script qui se lance du shell, qui lit les commentaires de la définitions d'une classes pour générer le WSDL ... comme on le verra dans les exemples ci-dessous.

Exemple 2 : Hello world

Commençons par un simple web service qui ne fait que renvoyer un message : le classique du classique donc.

hello.php

Ce fichier contient TOUTE la partie serveur, c'est-à-dire à la fois la classe qui implémente le webservice et le code générique d'interfaçage nécessaire à PHP.
TOUTE : car nous sommes ici dans un exemple. Dans un projet réel un peu complexe, il sera sans doute plus judicieux de les mettre dans des fichiers différents, surtout pour faciliter le travail de wdsl-writer ... mais on voit que ça fonctionne aussi avec un seul fichier.

<?php
/* classe d'implementation du web service */
class hello {

/**
* Hello World Method
*
* @return string On dit bonjour
*/
public function helloWorld() {
return "Cool, ca marche";
}
}

/* partie serveur */
$soap = new SoapServer(
'hello.wsdl',
array('uri' => 'http://127.0.0.1/~laurent')
);
$soap->setClass('hello');
$soap->handle();
?>

Pour la classe d'implémentation :

Pour la partie serveur :

Génération du fichier wsdl

Très simple :

.../wsdl_writer/bin/wsdl hello.php http://127.0.0.1/~laurent/ hello.wsdl endpoint.php

avec

Comme indiqué, les fichiers wsdl et endpoint sont optionnels : S'ils sont absents, <nom_classe>.wsdl et <nom_classe>.php sont utilisés.

Enfin, on place hello.php et hello.wsdl dans le répertoire du site web correspondant à l'URI. Ici ça sera public_html de mon compte.

NOTE IMPORTANTE

Durant mes tests, j'ai eu parfois des erreurs ... bizarre, genre des 

Fatal error: SOAP Fault: (faultcode: Client, faultstring: looks like we got no XML document) in /home/laurent/projets/b2b/tstPHPWS/Echo/tstEcho on line 18

Y'a franchement de quoi s'arracher les cheveux mais je me suis aperçu que mes fichiers WSDL étaient cachés côté serveur bien que j'ai demandé le contraire ! Ce qui faisait qu'évidemment, les modifications des signatures des webservices n'étaient pas pris en compte .

La solution dans ce cas est de :

Le client : TstHello

On est dans un domaine connu :

#!/usr/bin/php
<?php
/* Test d'appel a un WEBService */
$wsdl = 'http://127.0.0.1/~laurent/hello.wsdl';
$options = array('trace'=>true, 'cache_wsdl' => WSDL_CACHE_NONE );

try {
$soapclt = new SoapClient($wsdl, $options);
$res = $soapclt->HelloWorld();
echo "Ok !n";
var_dump($res);
} catch (SoapFault $fault) {
trigger_error("SOAP Fault: (faultcode: {$fault->faultcode}, faultstring: {$fault->faultstring})", E_USER_ERROR);
}
?>

Et ça marche ...

laurent@chose:~/projets/tstPHPWS/HelloWorld$ ./tstHello 
Ok !
string(15) "Cool, ca marche"
laurent@chose:~/projets/tstPHPWS/HelloWorld$

Vous trouverez les fichiers correspondant dans cette archive.

Exemple 3 : Echo

Exactement le même webservice que dans l'exemple 1 ... sauf qu'ici, le fournisseur est écrit en PHP.

Pour une raison qui m'échappe, je n'ai pu mettre ce coup-ci à la fois la classe et le dispatcher dans le même fichier.

La définition de la classe gagne donc un paramètre @param string $s comme suit :

class WSEcho {
/**
* Echo method
*
* @param string $s First integer
* @return string Result of addition
*/
public function allo($s)
{
return "Votre chaine etait '$s'";
}
}

Et tous les fichiers correspondants sont disponibles dans cette archive.

Exemple 4 : C'est la classe

Il est possible de passer en argument ou de retourner un objet complexe, autrement dit ... une classe. Dans cette exemple, nos webservices, renvoient une date et une heure sous la forme d'une structure.

Dans notre fichier include, on trouve d'abord la définition de notre classe.

class DateHeure {
	/** @var int date au format AAAAMMJJ */
	public $date;

	/** @var int heure au format HHMM*/
	public $heure;

	/** @var int timestamp, l'instant a convertir
	 * @access private
	 */
	private $ts;	// Normalement, celui-ci ne devrait pas etre exporte car privavee

	/**
	 * Constructeur de DateHeure
	 *
	 * @var int ats timestamp a convertir, si NULL, c'est on prend l'heure actuelle
	 */
	public function __construct($ats = NULL){
		$this->ts = $ats ? $ats : time();

		$this->date = Date('Ymd', $this->ts);
		$this->heure = Date('His', $this->ts);
	}
}

On remarquera au passage que la variable privée ne sera pas exportée dans le fichier WSDL.

Suit la classe correspondant aux web services.

class Horloge {
	/**
	 * Classe de test qui renvoie l'heure et la date
	 *
	 * @return DateHeure bla bla
	 */
	public function QuelleHeure(){
		return new DateHeure();
	}


	/**
	 * Classe de test qui renvoie l'heure et la date d'un timestamp
	 *
	 * @param int $ts bla bla
	 * @return DateHeure  bla bla
	 */
	public function QuelleHeureEtaitIl( $ts ){
		return new DateHeure( $ts );
	}
}

Il y en a 2, car en PHP, il est possible d'avoir un argument avec une valeur par défaut ... possibilité que je n'ai pas retrouvée au niveau du WSDL (n'hésitez pas à m'envoyer un message si vous savez comment faire ). Évidemment, cela implique aussi que les 2 webservices aient un nom différent.

Et voici une archive des codes correspondants.

Exemple 5 : Protection du web service

Avant de déployer un web service, il faut bien évidemment prendre en compte l'aspect sécurité. Et comme ça peut se faire classiquement au niveau d'http, on est en terrain connu.  

 

Nous allons reprendre l'exemple précédent et simplement ajouter une authentification.  

 

Protection côté serveur

On ne va pas faire de fioritures, on reste dans le classique : 

laurent@chose:~$ cd ~/public_html
laurent@chose:~/public_html$ htpasswd -c .htpasswd test
New password: 
Re-type new password: 
Adding password for user test
laurent@chose:~/public_html$

Et on crée un fichier .htaccess comme suit :

AuthType Basic
AuthName "Test"
AuthUserFile /home/laurent/public_html/.htpasswd
Require user test

côté client

Là non plus aucune difficulté : il n'y a juste qu'à rajouter le login et le mot de passe dans les options de SoapClient comme suit :

$options = array('trace'=>true, "exceptions" => true, 'cache_wsdl' => WSDL_CACHE_NONE, 'login' => 'test', 'password' => 'MonPassWD');
et ça marche sans problème :
laurent@chose:~/projets/b2b/tstPHPWS/Class$ ./tstHorloge 
Ok !
object(stdClass)#2 (2) {
  ["date"]=>
  int(20110406)
  ["heure"]=>
  int(4925)
}
Ok !
object(stdClass)#3 (2) {
  ["date"]=>
  int(20110402)
  ["heure"]=>
  int(132925)
}

Debugage et problèmes rencontrés

Génération du fichier wsdl

wsdl-writer est à la fois très chatouilleux au niveau de la syntaxe des fichiers PHP et en même temps très timide : s'il ne comprend pas quelque chose, c'est simple, il l'ignore ... ce qui ne nous rend pas forcement la tâche facile.

Règle numéro 1 : toujours vérifier que le fichier WSDL contient bien les arguments d'entrées et de sortie de chacun des services.

Dans mon cas, il semble qu'il n'apprécie que moyennement d'avoir des définitions de variable, d'argument ou de retour qui ne contiennent pas de commentaire. 

Corolaire à la règle numéro 1 : lorsque l'on change le fichier WSDL, il faut toujours s'assurer que l'ancienne versions n'est plus en cache. S'il existe des fichiers /tmp/wsdl-*, c'est qu'il existe des versions en cache. Il faut donc les effacer et redémarrer apache si c'est côté serveur.  

L'infâme : looks like we got no XML document

Cette erreur indique que le parser XML de PHP ne valide pas la réponse qu'il reçoit ... ce qui n'est pas forcement dû au web service lui-même comme nous allons le voir. Face à cette erreur, il faut :

  1. Vérifier que le fichier WSDL contient bien les informations qu'on attend : Typiquement, il m'est arrivé de modifier la signature d'une fonction sans que ce soit répercuté dans le WSDL reçu par le client ... et si les 2 ne correspondent pas, SoapServer ne renvoie rien ! (y'a peut-être une exception levée par handle() mais j'avoue humblement ne pas avoir vérifié).
  2. Supprimer les exceptions de SoapClient, activer les traces et afficher le contenu de la requête et de la réponse, comme dans le code suivant :
    $options = array('trace'=>true, "exceptions" => false, 'cache_wsdl' => WSDL_CACHE_NONE );
    
    try {
    	$soapclt = new SoapClient($wsdl, $options);
    	$res = $soapclt->QuelleHeure();
    	echo "Ok !n";
    	var_dump($res);
    print "<pre>n";
    print "Request :n"".$soapclt->__getLastRequest() .""n";
    print "Response:n"".$soapclt->__getLastResponse().""n";
    print "</pre>n";
     } catch (SoapFault $fault) { 	trigger_error("SOAP Fault: (faultcode: {$fault->faultcode}, faultstring: {$fault->faultstring})", E_USER_ERROR); }
    On vérifie que le XML des 2 est bien valide.
  3. Si c'est le cas, vérifier ... qu'il n'y ait pas d'espace ou de retour à la ligne devant la balise d'ouverture d'XML et après celle de fermeture. En effet, notre XML est pourtant bien valide, mais le parser de PHP n'aime pas du tout.
    Parfois, on n'arrive pas à trouver d'où est provient le problème ... alors il faut supprimer TOUS les retours chariot qui pourrait se trouver après la balise de fermeture de PHP de chacun de nos includes (J'ai mis du temps à la trouver celle la !!!!!). Heu, ça ressemble quant même furieusement à un bogue de PHP ce truc la, mais bon ...

     

Pour en savoir plus ...

Sur cette page, vous aurez compris que je me suis focalisé sur côté technique : si vous voulez approfondir le sujet, je ne saurai que vous conseiller d'aller faire un tour sur cet excellent blog que j'ai trouvé durant mes recherches ...

Je n'ai pas réussi à trouver une documentation sur la syntaxe utilisée par wsdl-writer, il semblerait que ce soit la même que c'est de PHPDoc

@internal soaprequires WrappedString LoginObject

M'enfin, les investigations continues ...


Visitez :
La liste de nos voyages
Nos sorties Ski et rando
Copyright Laurent Faillie 2001-2024
N'oubliez pas d'entrer le mot de passe pour voir aussi les photos perso.
Contactez moi si vous souhaitez réutiliser ces photos et pour les obtenir avec une plus grande résolution.
Visites durant les 7 derniers jours Nombre de visites au total.

Vous pouvez laissez un commentaire sur cette page.