[spip-dev] API Objet - Nouvelle fonction de lecture

Hello,

Dans les derniers plugins que j’ai réalisé j’ai pris l’habitude de créer des API d’objets pour lire un objet et pour répertorier une liste d’objets éventuellement filtrée.
Au fil du temps j’ai donc écrit les mêmes fonctions à quelques améliorations près, mais le code est rigoureusement le même dans le fond.
L’objet de ce mail est de discuter de la première fonction soit objet_lire().

J’ai donc écrit une fonction générique objet_lire() qui permet de lire un objet par son id ou par un autre identifiant unique (comme prefixe pour les plugins) et d’utiliser ou pas une sauvegarde statique pour limiter les accès SQL quand on est dans un traitement qui le nécessite.
Il est possible de choisir les champs à récupérer et un pipeline post_relecture permet de compléter la description brute.

C’est donc une fonction très générique (peut-être trop mais finalement si ça marche pourquoi pas) et qui pourrait remplacer pas mal d’appels SQL récurrents qui à la longue nuisent un peu à la lisibilité du code ou à la maintenance (enfin c’est mon avis).
Je vous mets le code ci-dessous. Pensez-vous qu’on aurait intérêt à ajouter cette fonction à l’API objet ?
Si oui, on pourrait se poser la question pour les fonctions objet_supprimer et objet_repertorier.

function objet_lire($objet, $champ_id, $valeur_id, $informations = array(), $stockage = false) {

   // Initialisation du tableau des descriptions et des id d'objet (au sens id_xxx).
   // Le tableau des descriptions est toujours indexé par l'objet et l'id objet.
   static $descriptions = array();
   static $ids = array();

   // On détermine le nom du champ id de la table.
   include_spip('base/objets');
   $table_id = id_table_objet($objet);

   // On détermine si on a passé l'id objet ou un autre identifiant unique de la table :
   if ($champ_id != $table_id) {
      // on a passé un identifiant différent que l'id de l'objet, on cherche si cet objet a déjà été rencontré
      // car dans ce cas on a déjà stocké son id objet.
      $index = isset($ids[$valeur_id]) ? $ids[$valeur_id] : 0;
   } else {
      $index = $valeur_id;
   }

   // Même si on a pas demandé le stockage sur l'appel en cours, on vérifie si l'objet demandé
   // n'est pas déjà stocké.
   if (isset($descriptions[$objet][$index])) {
      $description = $descriptions[$objet][$index];
   } else {
      $description = array();
   }

   // Si l'objet n'a pas encore été stocké, il faut le récupérer sa description complète.
   if (!$description) {
      // On récupère la table SQL à partir du type d'objet.
      $table = table_objet_sql($objet);

      // La condition est appliquée sur le champ désigné par l'utilisateur. Si ce champ n'est pas l'id objet
      // on considère qu'il est de type chaine.
      $where = ($champ_id != $table_id)
         ? array("${champ_id}=" . sql_quote($valeur_id))
         : array("${champ_id}=" . intval($valeur_id));

      // Acquisition de tous les champs de l'objet : si l'accès SQL retourne une erreur on renvoie un tableau vide.
      if (!$description = sql_fetsel('*', $table, $where)) {
         $description = array();
      } else {
         // On complète éventuellement la description brute de l'objet via le pipeline post_lecture.
         $description = pipeline('post_lecture', $description);
      }

      // Si on a demandé le stockage statique on sauvegarde la description à l'index correspondant à l'id objet.
      if ($stockage) {
         if (!$index) {
            // Première sauvegarde de l'objet qui est forcément lu via un champ qui n'est pas l'id objet.
            // Il faut donc stocker l'index pour un futur appel si la description est non vide.
            if ($description) {
               $index = $description[$table_id];
               $ids[$valeur_id] = $index;
            }
         }

         // Si l'index a bien été trouvé, on stocke la description à cet index.
         if ($index) {
            $descriptions[$objet][$index] = $description;
         }
      }
   }

   // On ne retourne maintenant que les champs demandés.
   if ($description and $informations) {
      // Extraction des seules informations demandées.
      // -- si on demande une information unique on renvoie la valeur simple, sinon on renvoie un tableau.
      // -- si une information n'est pas un champ valide elle n'est pas renvoyée sans renvoyer d'erreur.
      if (is_array($informations)) {
         if (count($informations) == 1) {
            // Tableau d'une seule information : on revient à une chaine unique.
            $informations = array_shift($informations);
         } else {
            // Tableau des informations valides
            $description = array_intersect_key($description, array_flip($informations));
         }
      }

      if (is_string($informations)) {
         // Valeur unique demandée.
         $description = isset($description[$informations]) ? $description[$informations] : '';
      }
   }

   return $description;
}


A vous lire.

Hello,

Aucun avis pour la 3.4 ?

Je ne sais pas si j'ai tout compris mais je vois pas l'intérêt de
demander le stockage ou pas, sachant que de toute façon même si on le
demande pas, ça va chercher le stockage pré-existant s'il y en a. Alors
que si jamais il devait y avoir une option, ça serait plutôt pour de
manière ponctuelle s'assurer que ça ne vient PAS du stockage, mais bien
de la toute dernière lecture de la base.

Donc à mon avis, ça doit toujours stocker en static, et dans les
arguments, il faut une option du genre "forcer_base" qui là va forcer de
pas lire la static et bien prendre la dernière valeur en base.

Sinon, question API objet, ce qui me parait le plus urgent c'est
vraiment objet_supprimer() car là on n'a aucune cohérence entre les
pratiques core et plugins, et en plus ya pas de pipeline qui permet de
faire des choses quand il y a vraiment destruction (avant ou après).
J'ai fait un ticket là dessus il y a 5 ans. :smiley:
https://core.spip.net/issues/3247

Re,

Aucun avis pour la 3.4 ?

Je ne sais pas si j’ai tout compris mais je vois pas l’intérêt de
demander le stockage ou pas, sachant que de toute façon même si on le
demande pas, ça va chercher le stockage pré-existant s’il y en a. Alors
que si jamais il devait y avoir une option, ça serait plutôt pour de
manière ponctuelle s’assurer que ça ne vient PAS du stockage, mais bien
de la toute dernière lecture de la base.

Donc à mon avis, ça doit toujours stocker en static, et dans les
arguments, il faut une option du genre « forcer_base » qui là va forcer de
pas lire la static et bien prendre la dernière valeur en base.

Oui absolument tu as raison.
Je vais changer.

Sinon, question API objet, ce qui me parait le plus urgent c’est
vraiment objet_supprimer() car là on n’a aucune cohérence entre les
pratiques core et plugins, et en plus ya pas de pipeline qui permet de
faire des choses quand il y a vraiment destruction (avant ou après).
J’ai fait un ticket là dessus il y a 5 ans. :smiley:
https://core.spip.net/issues/3247

Tout à fait, l’idée de mon mail était bien de compléter l’API objet avec lire, répertorier et supprimer et j’ai juste commencé avec les deux premières car j’ai du code assez générique pour ça.
Pour supprimer comme tu dis il faut au moins un pipeline post, je ne suis pas sur qu’un pré soit nécessaire.

Après, je pense qu’il manque peut-être dans la fonction lire aussi un appel à une fonction spécifique de l’objet comme on le fait pour les autres fonctions de l’API.

Bon je ne sais pas si l’une ou l’autre est plus prioritaire mais je ne suis pas sur que ce soit un gros de les proposer dans la prochaine version.
Je vais continuer à proposer les trois fonctions manquantes et on verra alors.

Hop,

Hello,

moi je suis sceptique sur le fait d’introduire des données dans la lecture via un/des pipelines, ce qui veut dire que de facto on a plus d’équivalence entre un sql_select et l’utilisation de l’API de lecture.

La fonction peut avoir de l’intérêt en tant que shorthand, pour fluidifier le code et/ou bénéficier du static (mais il faudrait voir si cela apporte vraiment un gain de perf, car de son côté mysql cache aussi les requêtes, c’est à dire que le même select va beaucoup plus vite la seconde fois), mais si elle doit devenir un passage obligé pour la lecture des données (pour cause de pipeline et d’injection externe de contenu hors SQL), ça va être contreproductif :

on va se retrouver à faire des gros SQL select avec des conditions compliquées en SQL pour récupérer les id, puis à appeler la fonction de lecture pour chaque id, et ça va ramer très fort à la fin.
De plus, cela veut dire aussi qu’on récupérerait via l’api de lecture des données qui ne correspondent pas à celles qu’on a avec une boucle.

Je pense donc que la fonction ne doit pas permettre de modifier/alterer/compléter ce qui vient de la base et n’être donc qu’un simple raccourci de sql_xx pour qui veut s’en servir.

Coucou,

Hello,

moi je suis sceptique sur le fait d’introduire des données dans la lecture
via un/des pipelines, ce qui veut dire que de facto on a plus d’équivalence
entre un sql_select et l’utilisation de l’API de lecture.

C'est une option que j'ai rajouté en me disant qu'il pourrait être
intéressant de rajouter à un objet des informations attenantes appartenant
à des objets liés (le terme lié est à définir c'est clair).
Mais comme tu le dis, ça crée de facto une différence avec l'appel SQL seul
ou la boucle simple ce qui pourrait aussi justifier la fonction d'un autre
coté.
Maintenant, si ça perturbe on peut clairement l'enlever.

La fonction peut avoir de l’intérêt en tant que shorthand, pour fluidifier
le code et/ou bénéficier du static (mais il faudrait voir si cela apporte
vraiment un gain de perf, car de son côté mysql cache aussi les requêtes,
c’est à dire que le même select va beaucoup plus vite la seconde fois),
mais si elle doit devenir un passage obligé pour la lecture des données
(pour cause de pipeline et d’injection externe de contenu hors SQL), ça va
être contreproductif :

Oui c'était pour ça que j'avais rajouté un argument stockage oui ou non
pour piloter les cas où on en avait besoin (par exemple dans un traitement
ou l'on sait qu'on va relire n fois le même objet).
Mais sur les conseils de Rasta j'ai transformé la fonction en stockant
systématiquement et en permettant le forcage du recalcul de l'objet.

on va se retrouver à faire des gros SQL select avec des conditions
compliquées en SQL pour récupérer les id, puis à appeler la fonction de
lecture pour chaque id, et ça va ramer très fort à la fin.

Je pige pas là.

De plus, cela veut dire aussi qu’on récupérerait via l’api de lecture des
données qui ne correspondent pas à celles qu’on a avec une boucle.

Je pense donc que la fonction ne doit pas permettre de
modifier/alterer/compléter ce qui vient de la base et n’être donc qu’un
simple raccourci de sql_xx pour qui veut s’en servir.

Comme tu veux je peux l'enlever sans souci.

    on va se retrouver à faire des gros SQL select avec des conditions
    compliquées en SQL pour récupérer les id, puis à appeler la fonction
    de lecture pour chaque id, et ça va ramer très fort à la fin.

Je pige pas là.

Bé quand tu travailles pas juste sur UN unique contenu, tu fais un
select de plein de champs d'un coup (et des jointures éventuelles), et
tu récupères tout en une seule requête.

Là si pour cause de pipeline, que tout soit toujours cohérent, il FAUT
passer par cette fonction d'API, alors il va falloir faire la sélection
de la liste des contenus avant, mais SEULEMENT leur "id", et ensuite
pour chacun d'entre eux, aller récupérer les infos complètes avec la
fonction d'API. Donc si t'as 1000 contenus, ça fait 1001 requêtes. Au
lieu d'une seule unique avec un select qui a tous les champs et
jointures voulus d'un coup.

Ça serait une grosse perte.

    De plus, cela veut dire aussi qu’on récupérerait via l’api de
    lecture des données qui ne correspondent pas à celles qu’on a avec
    une boucle.

Moi c'est ce point qui me turlupine le plus je crois.

En fait l'intérêt d'une fonction de lecture en PHP, je la vois surtout
si on finit par avoir une réelle cohérence entre récupération en boucle
dans les squelettes et récupération en PHP par une API. Donc avec TOUTES
les optimisations de performance que ça fait pour les boucles, et toutes
les jointures automatiques ajoutées, etc, etc. Et cela pas juste pour un
unique contenu, mais bien pour une liste aussi (comme les boucles).

Ça pour le coup je trouve que c'est un manque, car quand on est en PHP,
par exemple dans une action, et qu'on veut produire du CSV ou du JSON,
et bien si on veut profiter de toutes les jointures, cache,
optimisations fiscales, etc, il n'y a aucun autre moyen que de… faire
tout ça dans un squelette, qui va produire un JSON, et le décoder en
array() ensuite dans le PHP pour l'utiliser. On peut pas dire que ce
soit le plus pratique. :smiley:

Re,

on va se retrouver à faire des gros SQL select avec des conditions
compliquées en SQL pour récupérer les id, puis à appeler la fonction
de lecture pour chaque id, et ça va ramer très fort à la fin.

Je pige pas là.

Bé quand tu travailles pas juste sur UN unique contenu, tu fais un
select de plein de champs d’un coup (et des jointures éventuelles), et
tu récupères tout en une seule requête.

Oui ok, pigé.
En fait, cet API est à usage simple.
Si l’usage qu’on a est plus sophistiqué il faut revenir à l’écriture SQL plus optimisée, je suis d’accord.
Je me disais que statistiquement on devrait avoir pas mal de cas simples qui gagnerait à être factorisés avec cette fonction, c’est plutôt ça mon approche.

De plus, cela veut dire aussi qu’on récupérerait via l’api de
lecture des données qui ne correspondent pas à celles qu’on a avec
une boucle.

Moi c’est ce point qui me turlupine le plus je crois.

En fait l’intérêt d’une fonction de lecture en PHP, je la vois surtout
si on finit par avoir une réelle cohérence entre récupération en boucle
dans les squelettes et récupération en PHP par une API. Donc avec TOUTES
les optimisations de performance que ça fait pour les boucles, et toutes
les jointures automatiques ajoutées, etc, etc. Et cela pas juste pour un
unique contenu, mais bien pour une liste aussi (comme les boucles).

Oui c’est vrai.
D’ailleurs c’est assez marrant car je me suis décidé à proposer cette fonction après avoir créer récemment dans SVP la fonction plugin_lire() qui ne peut pas être remplacée elle par ma proposition objet_lire() car pour un plugin il faut une jointure avec la table spip_depots pour exclure les plugins installés (si on cherche par le préfixe).
Or dans une boucle ce critère est automatique.
Donc oui c’est exact ça ne résout pas ce souci.

Bon après, je peux faire une PR de la fonction modifiée avec tes remarques précédentes et sans pipeline et on voit ce qu’on en fait.

Hello,

J’ai essayé de voir comment faire une pull-request.
J’ai rien compris…
Je ne vois pas comment et via quel outil je peux le faire.

Je fais quoi ?

Coucou,

Je relance ce thread car on va bien sortir un jour la 3.3.
Donc que fais-je avec ma fonction objet_lire() ?

Le code est le suivant :

/**
 * Lit un objet donné connu par son id ou par un identifiant textuel unique et renvoie tout ou partie de sa
 * description.
 * Il est possible pour un objet donné de fournir la fonction <objet>_lire_champs qui renvoie simplement tous les
 * champs de l'objet concerné sans aucun autre traitement. Sinon, l'appel SQL est réalisé par l'API.