Concepts
Encoder et décoder
Encoder, c’est traduire une donnée structurée - toujours un tableau PHP - en une chaine de caractères conforme à un format d’échange. Décoder est l’opération inverse : reconstruire un tableau PHP à partir d’une chaine.
Les deux opérations sont pensées comme symétriques : ce qu’un plugin encode, il doit pouvoir le relire. Cette symétrie n’est toutefois jamais parfaite, chaque format ayant ses propres limites de représentation. Un tableau PHP contient des types que le JSON ne connait pas, le XML impose une racine unique, le CSV ne représente qu’un tableau à deux dimensions. Le plugin ne cherche donc pas à garantir un aller-retour à l’identique mais à fournir, pour chaque format, la traduction la plus fidèle possible.
Le traitement des erreurs incombe au plugin appelant, et Encoder Factory s’attache à le lui rendre simple : plutôt que de laisser remonter une exception, il capte les erreurs des librairies qu’il utilise, journalise le détail et renvoie une valeur vide du type attendu - chaine vide à l’encodage, tableau vide au décodage. Un appelant qui se moque de l’échec peut donc ignorer le retour sans risquer d’erreur fatale.
Le format
Le format est l’identifiant du langage d’échange visé : json, yaml, xml ou csv pour ceux que le plugin fournit. C’est lui qui détermine le service appelé.
Sa forme n’est pas libre : composant le nom du fichier inclus, il doit être un identifiant simple - lettres, chiffres et soulignés. L’API le vérifie et refuse l’appel dans le cas contraire, ce qui interdit qu’un chemin relatif fasse inclure un fichier étranger au plugin. La même règle vaut pour le préfixe du plugin appelant.
Un format n’est pas une extension de fichier ni un type MIME, même s’il leur ressemble souvent. C’est un identifiant interne au plugin, ce qui permet à un plugin appelant de définir ses propres formats ou de distinguer plusieurs variantes d’un même langage.
Les options
Chaque format admet des options qui en règlent le détail : indentation, délimiteur, profondeur de récursivité, nom de la balise racine… Elles sont fournies dans un tableau associatif dont les index sont propres à chaque format, et dont aucun n’est obligatoire : le service complète toujours les options manquantes par ses valeurs par défaut.
C’est ce qui permet à l’API de rester générique - quatre arguments, quel que soit le format - tout en donnant accès aux réglages fins de chaque encodeur.
API fonctionnelle PHP
L’API est incluse dans le fichier inc/ezcodec.php et se réduit à trois fonctions.
| Fonction | Description |
|---|---|
contenu_encoder |
Encode un tableau dans le format demandé et renvoie la chaine obtenue, ou une chaine vide en cas d’erreur. |
contenu_decoder |
Décode une chaine du format indiqué et renvoie le tableau obtenu, ou un tableau vide en cas d’erreur. |
erreur_ezcodec_lire |
Renvoie l’erreur de la dernière opération, ou un tableau vide si elle a réussi. |
Les deux premières partagent une signature similaire :
contenu_encoder(string $plugin, array $contenu, string $format, ?array $options = []) : string
contenu_decoder(string $plugin, string $contenu, string $format, ?array $options = []) : arrayLe plugin appelant vient toujours en premier, comme dans Cache Factory et N-Core, puis le contenu, le format, et enfin les options, facultatives.
Pourquoi le préfixe du plugin ? Parce qu’il permet à un plugin de se donner son propre traitement d’un format sans rien changer pour les autres. Un plugin qui n’a pas ce besoin le transmet quand même : le mécanisme retombe alors sur le service du format, et il n’a rien d’autre à savoir.
Les formats fournis
| Format | Identifiant | Implémentation | Dépendance |
|---|---|---|---|
| JSON | json |
fonctions natives de PHP | aucune |
| YAML | yaml |
plugin YAML de SPIP, librairie Symfony | plugin YAML |
| XML | xml |
SimpleXMLElement |
aucune |
| CSV | csv |
fonctions d’import/export CSV de SPIP | aucune |
La dépendance au plugin YAML est déclarée en <utilise> et non en <necessite> : un site qui n’encode jamais de YAML n’a pas à installer le plugin correspondant. En contrepartie, les services YAML vérifient sa présence avant d’agir et signalent son absence, qui ne doit pas être confondue avec un contenu vide.
Les options de chaque format, avec leur valeur par défaut, sont décrites dans le document de conception et dans la documentation PHPDoc du code. Quelques points méritent d’être connus avant de choisir un format :
- JSON : le drapeau
JSON_PARTIAL_OUTPUT_ON_ERRORest appliqué par défaut, ce qui évite l’échec complet sur des valeurs commeINFouNaN; l’anomalie est alors journalisée, la sortie étant partielle. Le drapeauJSON_THROW_ON_ERRORest systématiquement retiré des options fournies, conformément au contrat de non-levée. - XML : l’encodage ajoute une balise englobante, nommée
ezcodecpar défaut, le XML imposant une racine unique. À l’encodage toujours, les clés du tableau deviennent des noms de balise et doivent donc en respecter la grammaire - une lettre ou_en tête, puis lettres, chiffres,_,.et-, les accents étant admis. Un tableau à clés numériques n’est ainsi pas encodable en XML : depuis la 1.1.0 l’encodage le refuse et le signale, là où il produisait auparavant un document qu’aucun analyseur ne relisait, pas même Encoder Factory. Surtout, la conversion d’un document XML en tableau a des limites qu’il faut connaitre : la structure obtenue dépend du nombre d’éléments - deux<livre>donnent une liste, un seul donne directement le contenu -, les attributs sont rangés sous la clé@attributes, et les valeurs sont toujours des chaines. Un plugin dont les données supportent mal ces contraintes a intérêt à préférer le JSON. - CSV : le format ne représente qu’un tableau de lignes, chacune étant un tableau associatif dont les index donnent les noms de colonnes.
Fonctionnement d’Encoder Factory
La dissociation API - services
Le plugin reprend l’architecture API - services déjà employée par Cache Factory et N-Core : une API publique, stable et générique, et des services propres à chaque format, appelés par elle.
L’intérêt est double. Le plugin appelant n’a qu’un seul point d’entrée à connaitre, quel que soit le format. Et l’ajout d’un format ne modifie jamais l’API : il consiste à déposer un fichier de plus.
Les services fournis par le plugin sont rangés dans le dossier ezcodec/, à raison d’un fichier par format, nommé d’après l’identifiant du format. À côté d’eux, le fichier ezcodec/ezcodec.php réunit les services qui ne dépendent d’aucun format : l’aiguillage et la mémorisation des erreurs.
L’aiguillage vers le service
Un plugin appelant a deux besoins de nature opposée, et c’est ce qui commande tout le mécanisme.
| Besoin | Nature | Où loge la fonction |
|---|---|---|
| Mon propre traitement d’un format, existant ou non | privé : ce que je décide ne change rien pour les autres, et personne n’a à connaitre son existence | ezcodec/<prefixe>.php, fonction préfixée <prefixe>_contenu_encoder_<format>() |
| Un format partagé, fourni par Encoder Factory ou ajouté par un plugin | public : le nom du format est l’identifiant que tout le monde emploie | ezcodec/<format>.php, fonction au nom nu contenu_encoder_<format>() |
L’API normalise les options, vérifie les identifiants reçus, puis délègue au service, qui cherche d’abord le traitement du plugin et à défaut celui du format.
Cette distinction n’est pas cosmétique. Le nom du fichier de format est un espace de noms partagé : include_spip() s’arrête au premier fichier trouvé dans le chemin de SPIP. Faire passer un besoin privé par cet espace conduisait à une impasse - deux plugins voulant chacun leur propre XML livraient chacun un ezcodec/xml.php, et ni l’un ni l’autre ne maitrisait lequel serait retenu, l’ordre dépendant des dépendances entre plugins. Le perdant ne recevait aucun signal. La forme préfixée supprime le problème par construction.
C’est aussi pourquoi aucune fonction du plugin ne porte le suffixe _dist : ce suffixe est le marqueur d’une surcharge par homonymie, et ce n’est pas le mécanisme retenu.
Le signalement des erreurs
Aucune fonction du plugin ne lève d’exception : une opération qui échoue renvoie la valeur vide du type attendu. Ce contrat évite à l’appelant d’encadrer chaque appel d’un bloc try, mais il ne dit pas pourquoi l’opération a échoué, et une chaine vide peut recouvrir des situations très différentes.
Le plugin mémorise donc, en plus de la journalisation, l’erreur de la dernière opération. L’appelant la consulte avec erreur_ezcodec_lire(), qui renvoie un tableau à trois index - code, message et format - et les codes suivants :
| Code | Signification |
|---|---|
plugin_invalide |
Le préfixe du plugin n’est pas un identifiant simple. |
format_invalide |
L’identifiant de format n’est pas un identifiant simple. |
format_inconnu |
Aucun service ne fournit l’opération demandée pour ce format. |
dependance_absente |
Le plugin dont dépend le format n’est pas actif : le YAML aujourd’hui. |
encodage |
Le contenu fourni n’a pas pu être encodé. |
decodage |
La chaine fournie n’a pas pu être décodée. |
La distinction la plus utile est celle entre dependance_absente et les deux dernières : la première relève de l’installation du site, les autres des données traitées.
La mémoire ne vaut que pour le dernier appel : chaque appel d’API la vide avant d’agir, à l’image de la fonction json_last_error() de PHP dont le plugin reprend le principe. Ce mécanisme est facultatif à l’usage : un appelant qui se contente de tester la valeur de retour n’a rien à changer.
Mise en œuvre dans un plugin utilisateur
Utiliser l’API
L’usage courant se réduit à un appel, précédé de l’inclusion de l’API. Le premier argument est le préfixe de votre plugin :
include_spip('inc/ezcodec');
// Encodage
$chaine = contenu_encoder('monplugin', $tableau, 'json');
// Décodage, avec une option
$tableau = contenu_decoder('monplugin', $chaine, 'csv', ['delim' => ';']);Comme aucune exception n’est levée, il revient à l’appelant de tester le retour. Quand il ne suffit pas de savoir que l’opération a échoué, erreur_ezcodec_lire() en donne la cause :
if (!$chaine = contenu_encoder('monplugin', $tableau, 'yaml')) {
$erreur = erreur_ezcodec_lire();
if ($erreur['code'] === 'dependance_absente') {
// le plugin YAML n’est pas actif : c’est la configuration du site qui est
// en cause, pas le contenu fourni
}
}L’appel doit suivre immédiatement l’opération : un encodage ou un décodage intermédiaire remettrait la mémoire à zéro.
Ajouter un format partagé
Un format partagé est destiné à être employé par n’importe quel plugin, y compris ceux qui ignorent qui le fournit. Il s’ajoute en créant, dans son propre plugin, un fichier ezcodec/<format>.php contenant les deux fonctions :
function contenu_encoder_monformat(array $contenu, ?array $options = []) : string { … }
function contenu_decoder_monformat(string $contenu, ?array $options = []) : array { … }Trois règles à respecter : le nom du fichier est l’identifiant du format, les signatures sont imposées, et les conventions de retour et d’erreur s’appliquent - valeur vide du type attendu et signalement par ezcodec_erreur_signaler(), jamais d’exception qui remonterait à l’appelant.
Rien n’oblige à fournir les deux fonctions : un format qui n’a de sens qu’en lecture peut n’implémenter que le décodage.
Fournir son propre traitement d’un format
C’est le besoin inverse : obtenir un comportement qui ne vaut que pour vous, sur un format existant ou sur un format que vous êtes seul à connaitre. Il ne passe pas par le nom du format mais par le vôtre. Créez dans votre plugin un fichier ezcodec/<votre_prefixe>.php et préfixez-y vos fonctions :
// Dans ezcodec/monplugin.php
function monplugin_contenu_decoder_xml(string $contenu, ?array $options = []) : array {
// Le traitement d’origine reste appelable, son fichier n’étant jamais masqué.
include_spip('ezcodec/xml');
$contenu_decode = contenu_decoder_xml($contenu, $options);
// … le traitement complémentaire propre à votre plugin
return $contenu_decode;
}Dès lors, tout appel de votre plugin qui transmet son préfixe obtient ce traitement. Quatre propriétés en découlent :
- personne d’autre n’est affecté : les autres plugins continuent d’obtenir le XML d’Encoder Factory, même sur le même site et dans la même requête ;
- deux plugins peuvent le faire simultanément sur le même format, sans se masquer l’un l’autre ;
- rien n’oblige à traiter les deux sens : si vous ne déclarez que le décodage, l’encodage retombe naturellement sur le service du format ;
- le traitement d’origine reste appelable, comme dans l’exemple ci-dessus.
Une seule interdiction : ne déclarez jamais une fonction au nom nu - contenu_decoder_xml() sans préfixe - pour un format fourni par Encoder Factory. Ce nom appartient au format, et le déclarer une seconde fois provoquerait une erreur fatale de redéclaration.
Résumé pour le développement d’un plugin utilisateur
Dans l’immense majorité des cas, la mise en œuvre tient en trois lignes : déclarer la dépendance dans paquet.xml, inclure inc/ezcodec, appeler contenu_encoder() ou contenu_decoder() en transmettant son préfixe. Il n’y a rien à configurer, rien à déclarer, aucun service à écrire.
Les deux mécanismes d’extension - ajouter un format partagé, fournir son propre traitement d’un format - ne servent qu’aux plugins qui en ont explicitement besoin, et l’un comme l’autre se réduisent à déposer un fichier.
Pour aller plus loin, le document de conception du plugin, plus détaillé que cette présentation, est disponible dans le dépôt : https://git.spip.net/spip-contrib-e....
La documentation technique produite depuis la PHPDoc du code est consultable sur https://code.spip.net/ezcodec/, et l’historique détaillé des versions dans le journal des modifications du dépôt : https://git.spip.net/spip-contrib-e....
La version 1.0.0 est la première version stable : elle marque le passage de l’API sous contrat, et rompt la compatibilité avec la ligne 0.x en ajoutant le préfixe du plugin appelant en premier argument. Les plugins Cache Factory et Mashup Factory ont été adaptés en conséquence ; une note de migration figure dans le changelog.
La version 1.1.0 déclare la compatibilité avec SPIP 5.0 et relève en conséquence son plancher à SPIP 4.4 : les sites en 4.0 à 4.3 restent sur la 1.0.0. Elle renomme erreur_codec_lire() en erreur_ezcodec_lire(), afin d’aligner la lecture de l’erreur sur la forme employée par les plugins de la même famille - c’est la seule fonction concernée. Elle répare enfin deux défauts de l’encodage, présents depuis l’origine : le CSV échouait dès lors qu’aucune option n’était fournie, et le XML produisait sans le signaler un document invalide sur un tableau à clés numériques.
N’hésitez donc à partager vos retours.
No discussion
Add a comment
Avant de faire part d’un problème sur un plugin X, merci de lire ce qui suit :
Merci d’avance pour les personnes qui vous aideront !
Par ailleurs, n’oubliez pas que les contributeurs et contributrices ont une vie en dehors de SPIP.
Follow the comments:
|
