Encoder Factory

Encoder Factory est une boite à outils pour les plugins : il traduit un tableau PHP en une chaine de caractères conforme à un format d’échange, et l’inverse. Les formats JSON, YAML, XML et CSV sont fournis, tout plugin peut en ajouter, et chacun peut créer son propre traitement d’un format sans rien changer pour les autres.

Le plugin ne rend aucun service visible à l’utilisateur d’un site : ni page dans l’espace privé, ni saisie, ni squelette. Il est notamment la dépendance qui permet à Cache Factory de coder le contenu de ses fichiers cache.

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.

FonctionDescription
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 = []) : array

Le 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

FormatIdentifiantImplémentationDé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_ERROR est appliqué par défaut, ce qui évite l’échec complet sur des valeurs comme INF ou NaN ; l’anomalie est alors journalisée, la sortie étant partielle. Le drapeau JSON_THROW_ON_ERROR est systématiquement retiré des options fournies, conformément au contrat de non-levée.
  • XML : l’encodage ajoute une balise englobante, nommée ezcodec par 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.

BesoinNatureOù 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 :

CodeSignification
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.

Discussion

No discussion

Add a comment

Avant de faire part d’un problème sur un plugin X, merci de lire ce qui suit :

  • Désactiver tous les plugins que vous ne voulez pas tester afin de vous assurer que le bug vient bien du plugin X. Cela vous évitera d’écrire sur le forum d’une contribution qui n’est finalement pas en cause.
  • Cherchez et notez les numéros de version de tout ce qui est en place au moment du test :
    • version de SPIP, en bas de la partie privée
    • version du plugin testé et des éventuels plugins nécessités
    • version de PHP (exec=info en partie privée)
    • version de MySQL / SQLite
  • Si votre problème concerne la partie publique de votre site, donnez une URL où le bug est visible, pour que les gens puissent voir par eux-mêmes.
  • En cas de page blanche, merci d’activer l’affichage des erreurs, et d’indiquer ensuite l’erreur qui apparaît.

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.

Who are you?
[Log in]

To show your avatar with your message, register it first on gravatar.com (free et painless) and don’t forget to indicate your Email addresse here.

Enter your comment here

This form accepts SPIP shortcuts {{bold}} {italic} -*list [text->url] <quote> <code> and HTML code <q> <del> <ins>. To create paragraphs, just leave empty lines.

Add a document

Follow the comments: RSS 2.0 | Atom