PHP : Soyez sympa, autodocumentez vos webservices !

Il est assez crispant de faire appel à un webservice et de se voir rétorquer ’40x’ ou ’50x’ … sans plus d’explications. Bien sûr la doc sur le wiki ou sur le site n’est pas à jour et ne vous éclaire en rien sur ce code d’erreur, ou ne vous indique pas la faute (de frappe) que vous avez faite ou les paramètres manquant.

Je vous propose dans cet article d’être sympa avec les utilisateurs de votre webservice tout en étant vous-même plutôt fainéant : auto(ou presque)documentez vos webservices.

La méthode que je vous propose est celle ne nécessitant que très peu de code supplémentaire, c’est à dire que je ne vais pas utiliser de framework additionnel tel que Swagger. Appelez ça la méthode artisanale, v0.1, ou l’art hache.

J’ai implémenté ça sur des webservices développés sur un de mes serveurs, pour mes besoins personnels, à la mode MVC et REST mais sans framework. Ce petit projet fait 1,2kloc, TU inclus, et est destiné à tourner sur un Raspberry Pi.

L’implémentation devrait être facile à répliquer, et je suis en train de le faire chez mon client (qui a beaucoup de webservices, dont la doc obsolète est dans un wiki *ahum*)

Deux approches :

La première pourrait être que vos webservices répondent à ceci :

[seti@home]~$ curl http://ws/plats/infos

L’approche se défend : mentalement, si un webservice (ici plats) vous parle mal il semble logique de le rappeler avec la même méthode en lui demandant des infos.

Toutefois, cela fait une nouvelle ressource GET, ce qui n’est pas le plus élégant et le plus RESTful. Et en vérité elle occasionne un peu plus de code que la seconde approche : utiliser la méthode http OPTIONS, qui semble presque faite pour ça.

[seti@home]~$ curl -X OPTIONS http://ws/plats

Moins classe et plus long à taper, mais plus respectueux des standards. La réponse serait quelque chose comme ça :

* get :
Recherche un plat

@params classe (dessert, ...)
Eventuellement
@params critère (passé en ilike %critère%)

Exemple : curl http://ws/plats/Dessert/phro

* post :
Ajoute un plat

@params classe (Dessert, ...)
@params nom

Exemple : curl http://ws/plats -d name="Ile flottant" -d classe="Dessert"

L’endroit le plus adéquat pour mettre la doc des webservices est à mon sens dans le code, au plus proche de l’endroit où ils sont implémentés : en tant que commentaire des classes ou méthodes qui les implémente. Chez moi, ces webservices sont portés par une classe modèle. Dans cet exemple : models/Plats.php, dans laquelle chaque méthode implémente le webservice à rendre pour une méthode http donnée (‘get’, ‘post’, ‘put’, ‘trace’, …)

Dans models/Plats.php j’ai donc ceci :

class Plats extends Ws {

...

/**
* Recherche un plat
*
* @params classe (dessert, ...)
* Eventuellement
* @params critère (passé en ilike %critère%)
*
* Exemple : curl http://ws/plats/Dessert/phro
*
*/
public function get($class,$name=null)
{
...

/**
* Ajoute un plat
*
* @params classe (Dessert, ...)
* @params nom
*
* Exemple : curl http://ws/plats -d name="Ile flottant" -d classe="Dessert"
*/
public function post($class,$name)
{...

Réfléchissons

Le secret est d’utiliser la réflexion, disponible nativement dans PHP 5. La réflexion permet au code d’en savoir plus sur lui-même. Celle de PHP a en plus la possibilité d’afficher de la documentation si celle-ci est entre ‘/** */’

Je souhaite implémenter la réflexion de la manière la plus simple et moins intrusive possible, c’est à dire en touchant au moins possible de fichiers (à part pour mettre de la documentation, bien sûr).

L’API s’utilise ainsi :

$refClass=new ReflectionClass(MyClass);

Notez que la classe à réfléchir peut être passée en tant qu’objet … ou en tant que chaîne de caractère

Pour une méthode :

$refMethod=new ReflectionMethod(MyClass,method);

De la même manière, la méthode peut être passée en tant qu’objet ou chaîne de caractère.

Et pour afficher les commentaires de la méthode :

$refMethod->getDocComment();

Ajoutez à ceci que ReflectionClass a une méthode permettant de lister toutes les méthodes de la classe :

$refClass->getMethods();

J’ai maintenant toutes les billes pour afficher les commentaires des méthodes de toutes mes classes, donc de mes webservices.

Contrôlons

Le premier endroit où mon code sait quel webservice est appelé avec quelle méthode, à part dans index.php, est dans le contrôleur (méthode get ou getAction si vous utilisez Zend, mais pour mon si petit projet je fais du routage à la main, dans index.php)

Mon exemple décrète que curl -X OPTIONS http://ws/plats doit retourner les infos du webservice plats. Il suffit donc d’implémenter une réponse à cette méthode dans chaque contrôleur, le dispatcheur se chargera de l’appeler.

Comme tous mes contrôleurs héritent de la classe WsController, il est possible d’un coup d’un seul d’implémenter l’autodoc pour tous les webservices !

Dans controllers/WsController.php

class WsController
{
    public function options($class=array())
    {
        $infos=array();
        // Le dispatch m'envoie un tableau ; j'en veux pas.
        // Si rien ne m'est passé, j'ai un tableau ; j'en veux pas
        if(!is_string($class))
            $class=get_class($this);
        // Transformation de 'PlatsController' en 'Plats'
        $classes=new ReflectionClass(preg_replace('/Controller/','',$class));
        foreach($classes->getMethods() as $id => $method) {
            // La doc de toutes les classes non finales, et pas pour __construct (des fois qu'elle soit surchargée)
            if ($method->getName() != '__construct' and !$method->isFinal())
            {
                $infos[$method->getName()]=$method->getDocComment();
            }
        }
        return array('infos'=>$infos);
    }

La méthode prend un tableau en paramètre afin de pouvoir la tester en test unitaires (et demander la doc de n’importe quelle classe). Si rien ne lui est passé, elle affichera la doc des méthodes de la classe modèle correspondant à la classe contrôleur qui hérite d’elle : si PlatsController hérite d’elle, options affichera la doc de la classe Plats.

La méthode options n’est pas finale, ce qui veut dire que le contrôleur peut la modifier s’il le souhaite.

Voyons

De la même manière, la vue a une classe par méthode http, appelée par le dispatcheur, et toutes les vues héritent de la même classe mère WsView

class WsView
{
    public function options($data)
    {
        $data=$data['infos'];
        $s='';
        $allow=array();
        foreach($data as $method => $infos)
        {
            // Fait sauter les /** * */
            $infos=trim(preg_replace('#\s*/?\*+/?\s*#',"\n",$infos));
            $s.="\n* $method : \n$infos\n";
            $allow[]=$method;
        }
        // Pour la RFC (et avoir en un coup d'oeil les méthodes dispo)
        $header='Allow: '.strtoupper(join(', ',array_values($allow)));
        return $this->_echo($s,$header);
    }
...

La méthode _echo affiche différemment si le code est lancé sous TU ou si la sortie est destinée à un « navigateur » (cURL, Firefox, …)

Finalement, voici un schéma simplifié de ma petite application :

Mon appli Ws

Concluons

Le code ‘travailleur’ n’est logé qu’en deux endroits :

  • La classe mère de tous les contrôleurs.
  • La classe mère de toutes les vues.

Mes classes modèles ne sont pas impactées, et en deux petites fonctions j’ai rendu mes webservices (plus ou moins)autodocumentés !

Toutefois, si vous regardez la RFC, vous verrez qu’il manque quelques étapes afin de rendre mon code conforme :

  • Gestion de Content-Length et Transfer-Encoding afin de répondre avec un Content-Type approprié.
  • Gestion de ‘*’ en URI.

Que pensez-vous de mon approche ?

PS : le code de cet exemple est téléchargeable ici

Mots-clés: , , , ,

2 commentaires pour “PHP : Soyez sympa, autodocumentez vos webservices !”

  1. Je suis assez gêné car je ne perçois pas l’objectif et j’ai du mal à croire que cet objectif soit atteint.

    # Si on destine ce code à un humain j’ai du mal à imaginer que demander au développeur de faire un `curl -X` soit plus simple qu’aller sur le site web de l’éditeur et cliquer sur « documentation », ou que la forme de cette documentation puisse être aussi agréable que des pages web avec illustrations, liens et explications complexes.

    C’est d’autant plus vrai qu’ici on contraint le phpdoc interne à être visible du client final et à documenter la vue web au lieu des détails d’implémentation internes. Est-ce que tu documenteras les paramètres optionnels comme ton `$class` qui servent d’injection pour les tests ? c’est très pertinent en interne (celui qui utilise le code) mais pas du tout en externe (celui qui utilise le service).

    À la limite pour une API en environnement de test ou recette, envoyer le phpdoc pourrait avoir du sens sur les renvois d’erreur (4xx ou 5xx). Ce ne sera pas la révolution mais ça peut aider le développeur vraiment flemmard qui refuse d’avoir la documentation sur son second écran en parallèle de ses tests. Ça raccrocherait plus avec l’intention exprimée en introduction que faire une requête sur `OPTIONS`

    # Si on destine à un robot, il saura se servir de l’entête `Allow` mais pas vraiment plus à priori. Travailler sur l’hypermedia dans le corps du service lui-même sera probablement plus efficace. C’est là que seront documentés non seulement les ressources atteignables mais aussi les méthodes activables sur ces ressources, voire les paramètres utilisables.

    La partie serveur demande de penser l’architecture depuis le début. La complexité sera sur la partie cliente (j’ai vu extrêmement peu de cas en situation réelle où les clients étaient suffisamment intelligents pour suivre autre chose que des simples liens).

    (après il y a probablement des discussions à avoir sur le code lui-même mais pour ça il faudrait l’avoir sur un endroit en ligne où on peut faire du commentaire ligne à ligne)

  2. Hello,
    Merci pour ton commentaire, qui m’oblige à éclaircir mes intentions :)

    L’exemple que je propose est parti de trois constatations distinctes:
    * Chez mon client il y beaucoup de webservices, peu sont, pour moi, bien documentés (dans un wiki). Il y a une interface web pour les tester, qui permet de choisir le ws, la méthode et les paramètres. Quand je suis face à cette interface et que je ne connais pas les paramètres, je suis frustré parce que je sens que le wiki ne va pas m’aider. J’aimerais rester dans cette interface et avoir la doc la plus à jour possible sur ces paramètres.
    * Chez moi j’ai un petit peu de ws, pas documentés puisque seul moi les utilise (c’est pas une raison, je sais). Je les appel parfois en curl, et j’oublie parfois les paramètres à passer. J’aimerais rester en ligne de commande et avoir la doc de mes ws.
    * « La doc c’est has been » ( (c) moi ) et je sais par expérience qu’une doc est rarement synchro avec le produit, pour plein de raisons, l’une d’elle est que quand tu code, tu code, et aller documenter ce que tu code dans un wiki te demande de faire l’effort de quitter ton code et donc de changer de contexte (navigateur, cliquer, s’authentifier, cherche la page ou la créer, …) J’aimerais rester dans mon code pour y écrire la doc.

    Partant de ces trois constatations, j’ai cherché un moyen KISS de résoudre mes frustrations : la réflexion + doc dans les commentaire fait le job.
    Je ne prétend pas que cette approche règle tous les/vos problèmes de doc de ws. J’ai voulu présenter UNE piste (artisanale, v0.1, l’art hache) pour documenter les ws.
    Il y en a d’autres, clairement, et chacun peut pousser les pistes plus loin, ou en sortir.
    Mais ma foi si j’ai pu sensibiliser un ou deux lecteurs à la réflexion sur php et l’(auto)doc des ws, ça me va :)

Laissez un commentaire