Injecter son propre code SQL dans Views

Je ne vais pas revenir sur mon amour immodéré pour views, ce n'est plus trop la peine. Views est ce qu'il est et il arrive parfois que le choix de l'utiliser ne se pose pas, il est là, enraciné dans un projet, indéboulonnable sous peine d'exploser les charges. Et à chaque modification un peu conséquente c'est la même histoire, un temps de dingue à trifouiller en tout sens cette interface maudite pour obtenir à grand coup de prévisualisation une requête que j'ai en tête depuis le début. Passé un moment, on se lasse d'une telle gymnastique et je me suis donc mis à chercher, sans grande conviction, un moyen d'injecter mes propres requêtes dans Views. Et la bonne nouvelle est que oui, c'est faisable !

Pour quoi faire ?

Au delà de l'allergie atavique, de nombreuses bonnes raisons peuvent amener à vouloir injecter son propre code SQL. Déjà parce que certains exercices de style deviennent rapidement infaisables avec le générateur de requêtes (imbrications de select, group by complexes, etc.). Ensuite pour gagner du temps, car nombreux sont ceux qui vont tout de même plus vite à écrire du SQL qu'à click-clicker en tout sens. En revanche si certains comptaient par là améliorer les performances de views, qu'ils ne se fassent pas trop d'illusions. La génération de requête n'est pas, et loin de là, le poste le plus dispendieux de l'établissement. La production du markup à travers la jungle de couches passe-plat (styles de vue, style de ligne, etc.) arrive clairement en tête, et les joyeuseries dans la gestion du requêtage (voir le coup du count_query un peu plus loin) ne font que rallonger la sauce.

Altération de l'objet Query

Avant d'attaquer l'injection de pur code SQL dans nos vues, nous allons nous arrêter sur une technique plus simple qui peut déjà permettre de dynamiser la requête de manière plus complexe qu'avec l'interface graphique. Le principe de base de cette approche est d'exploiter le hook hook_views_query_alter dont le but est de permettre à un module tiers de modifier l'objet requête ($query, classe Query) d'une vue donnée ($view, classe View) avant que le code SQL ne soit généré. Query est une classe de Views qui représente symboliquement la future requête SQL. Vous y trouverez une série de champs contenant tout ce que vous avez défini dans l'interface graphique (relationships, where, orderby, etc).

Le mieux est comme pour un hook_form_alter d'utiliser la commande var_dump(...) pour déterminer ce que vous désirez changer dans la structure. A titre d'exemple, nous pourrions ensuite faire ceci :

 function mon_module_views_query_alter(&$view, &$query) {
   if($view->name=="ma_vue") {
    // A décommenter pour savoir ce que $query contient
    // echo "<pre>"; var_dump($query); exit();
     if ($_SESSION['tri_par_titre']) {
       $query->orderby [0] = 'node_revisions_title DESC';    
    } else {
       $query->orderby [0] = 'users_name ASC';        
   }
}  
Modification dynamique de l'objet Query avant génération du code SQL

Ici nous changions donc la clef de tri en fonction du contenu d'une variable de session. Le premier paramètre $views contient l'objet "vue" avec sa propriété $view->name que nous utilisons pour vérifier que l'on modifie bien la bonne vue. Le paramètre $query contient l'objet de classe Query représentant ce que vous avez défini dans l'IHM de Views. Pour info $query est tout simplement le champ $view->query. L'intérêt de le passer en paramètre m'échappe donc un peu.

Cette technique fonctionne bien, mais reste très liée à la manière dont views formalise les requêtes. Voyons comment descendre un cran plus bas niveau.

Écrasement de la requête SQL générée par Views

La méthode la plus simple pour injecter du vrai code SQL dans Views, est lisible ici dans la langue de Shakespeare. Le principe est d'exploiter un autre hook de l'API Views, hook_views_pre_execute. Ce hook étant invoqué juste avant l'appel à db_query nous laisse l'opportunité de changer la requête SQL qui a déjà été généré par Views à partir de l'objet Query. Voyons directement un exemple d'implémentation :

function mon_module_views_pre_execute(&$view) {
   if($view->name=="ma_vue") {
         // Décommenter pour voir à quoi ressemble la requête générée par Views
         // echo "<pre>"; var_dump($view->build_info['query']); exit();
         $view->build_info['query']=="
          SELECT
             node.nid AS nid,
             node_revisions.title AS node_revisions_title,
             users.uid AS users_uid,
             users.name AS users_name
          FROM bla bla bla"
;
   }
}
Modification de la requête SQL de views, avant son exécution

La propriété $view->build_info est un tableau de trois clefs : query pour la requête SQL, count_query pour la même requête "optimisée" (on y reviendra) pour le comptage des éléments, et query_args qui est un tableau d'arguments (penser ici aux paramètres d'une fonction db_query) appliqués aux deux précédentes requêtes.

Vous n'avez donc plus qu'à écraser la valeur de query comme dans l'exemple donné plus haut. La seule contrainte est que les champs exposés dans le select aient les mêmes noms que ceux de la requête qu'aurait produit Views. C'est encore plus vrai si vous reprenez un projet existant et que vous n'avez pas envie d'aller trifouiller dans les templates.

Du coup, un bon point de départ pour notre code SQL nous est fourni par le panneau de prévisulatisation de views. Attention cependant, la requête visible dans ce panneau n'est pas toujours strictement celle qui serait générée par Views (vive la cohérence de cet outil..). La méthode la plus fiable est un petit var_dump($query->build_info['query']) dans le "if" de l'implémentation donnée plus haut.

Si la requête que vous injectez ne renvoie pas le même nombre d'éléments que celle d'origine, vous devez en injecter une seconde dans $query->build_info['count_query']. C'est en effet à ce genre de détail que l'on constate avec effroi l'efficacité de Views qui effectue systématiquement deux requêtes, même lorsqu'aucune pagination ne justifie ce comptage.

Mais plus drole encore, lorsque vous construisez cette requête count_query, ne cherchez pas à placer là-dedans un select count(*) from ... car cela ne marcherait pas. En effet, pour une raison qui me dépasse, Views utilise le champ count_query de la manière la plus barbare qui soit, en l'encapsulant dans un select count(*) from (LA_REQUETE_COUNT_QUERY). Super sympa non ? ;-)

En patchant Views

La méthode précédente fonctionne très bien mais me pose un petit problème. Déjà que Views est aussi lent qu'un facteur suisse, l'obliger à générer une requête que l'on sait pertinemment devoir écraser juste après n'est pas très acceptable.

Malheureusement il n'existe pas de hook dans l'API de views pour surcharger la génération du code SQL. Il va donc nous falloir tailler dans le vif et patcher. Le meilleur endroit que j'ai trouvé est le fichier includes/view.inc. Ce dernier contient la classe View, et plus particulièrement la méthode build($display_id = NULL) qui fait très exactement ce qui nous intéresse. En effet, à la ligne 649 (de la version 6.x-2.11) se trouve le code responsable de la génération du code SQL. Nous allons donc modifier cette procédure de sorte à permettre à un module tiers de sa propre mouture :

// Let modules modify the query just prior to finalizing it.
foreach (module_implements('views_query_alter') as $module) {
  $function = $module . '_views_query_alter';
  $function($this, $this->query);
}

// { YB PATCH - Don't generate queries if it's already done
if (empty($this->build_info['query'])) {
// } YB PATCH

$this->build_info['query'] = $this->query->query();
$this->build_info['count_query'] = $this->query->query(TRUE);
$this->build_info['query_args'] = $this->query->get_where_args();

// { YB PATCH - Allow a module to generate its own SQL
}
// } YB PATCH  
patch de la classe View

On a fait plus complexe comme patch, avouez. Le principe est d'exploiter au maximum le hook_views_query_alter que nous avons rencontré plus haut en permettant non plus de modifier l'objet Query, mais de fournir une requête SQL tout prête. Le hack consiste donc pour Views de vérifier qu'une requête n'a pas déjà été injectée, et lancer sa propre génération le cas échéant. Notez au passage que le module une fois hacké sera plus compliqué à mettre à jour sauf si vous utilisez la technique des patchs.

Pour l'implémentation de ce hook, l'approche est strictement la même que pour la technique précédente (Ici aussi vous avez la même contrainte de devoir générer les mêmes champs en nombre et en nommage que ce qu'aurait fait views) a la nuance prés qu'il est cette fois obligatoire de générer aussi le champ count_query.

function mon_module_views_query_alter(&$view, &$query) {
   if($view->name=="ma_vue") {
      $view->build_info['query']="
          SELECT
             node.nid AS nid,
             node_revisions.title AS node_revisions_title,
             users.uid AS users_uid,
             users.name AS users_name
          FROM bla bla bla"
;
      $view->build_info['count_query']="SELECT node.nid AS nid FROM bla bla bla";      
   }
}
Injection du requête SQL à la place de celle de Views

Et voilà :) Simple et efficace

Conclusion

J'espère que cette technique vous sera aussi utile qu'à moi. Si vous n'avez aucune contrainte de performance, vous pouvez vous appuyer sur la méthode douce qui a le mérite de laisser Views intact. Mais dans le cas contraire, le petit hack est un prix bien faible à payer pour retrouver enfin sa liberté.

Vus : 998
Publié par arNuméral : 54