3 /***************************************************************************\
4 * SPIP, Systeme de publication pour l'internet *
6 * Copyright (c) 2001-2019 *
7 * Arnaud Martin, Antoine Pitrou, Philippe Riviere, Emmanuel Saint-James *
9 * Ce programme est un logiciel libre distribue sous licence GNU/GPL. *
10 * Pour plus de details voir le fichier COPYING.txt ou l'aide en ligne. *
11 \***************************************************************************/
14 * Déduction automatique d'une chaîne de jointures
16 * @package SPIP\Core\Compilateur\Jointures
19 if (!defined('_ECRIRE_INC_VERSION')) {
25 * Décomposer un champ id_truc en (id_objet,objet,truc)
27 * Exemple : décompose id_article en (id_objet,objet,article)
29 * @param string $champ
30 * Nom du champ à décomposer
31 * @return array|string
32 * Tableau si décomposable : 'id_objet', 'objet', Type de l'objet
33 * Chaine sinon : le nom du champ (non décomposable donc)
35 function decompose_champ_id_objet($champ) {
36 if (($champ !== 'id_objet') and preg_match(',^id_([a-z_]+)$,', $champ, $regs)) {
37 return array('id_objet', 'objet', objet_type($regs[1]));
44 * Mapping d'un champ d'une jointure en deux champs id_objet,objet si nécessaire
46 * Si le champ demandé existe dans la table, on l'utilise, sinon on
47 * regarde si le champ se décompose en objet/id_objet et si la table
48 * possède ces champs, et dans ce cas, on les retourne.
50 * @uses decompose_champ_id_objet()
51 * @param string $champ Nom du champ à tester (ex. id_article)
52 * @param array $desc Description de la table
54 * Liste du/des champs. Soit
55 * - array($champ), si le champ existe dans la table ou si on ne peut décomposer.
56 * - array(id_objet, objet), si le champ n'existe pas mais qu'on peut décomposer
58 function trouver_champs_decomposes($champ, $desc) {
59 if (!is_array($desc) // on ne se risque pas en conjectures si on ne connait pas la table
60 or array_key_exists($champ, $desc['field'])
64 // si le champ se décompose, tester que les colonnes décomposées sont présentes
65 if (is_array($decompose = decompose_champ_id_objet($champ))) {
66 array_pop($decompose);
67 if (count(array_intersect($decompose, array_keys($desc['field']))) == count($decompose)) {
77 * Calculer et construite une jointure entre $depart et $arrivee
79 * L'objet boucle est modifié pour compléter la requête.
80 * La fonction retourne l'alias d'arrivée une fois la jointure construire,
83 * @uses calculer_chaine_jointures()
84 * @uses fabrique_jointures()
86 * @param Boucle $boucle
87 * Description de la boucle
88 * @param array $depart
89 * Table de départ, sous la forme (nom de la table, description de la table)
90 * @param array $arrivee
91 * Table d'arrivée, sous la forme (nom de la table, description de la table)
93 * Colonne cible de la jointure
95 * Flag pour savoir si le critère est conditionnel ou non
96 * @param int $max_liens
97 * Nombre maximal de liaisons possibles pour trouver la jointure.
99 * Alias de la table de jointure (Lx)
101 function calculer_jointure(&$boucle, $depart, $arrivee, $col = '', $cond = false, $max_liens = 5) {
102 // les jointures minimales sont optimales :
103 // on contraint le nombre d'etapes en l'augmentant
104 // jusqu'a ce qu'on trouve une jointure ou qu'on atteigne la limite maxi
107 $milieu_exclus = ($col ?
$col : array());
108 while ($max <= $max_liens and !$res) {
109 $res = calculer_chaine_jointures($boucle, $depart, $arrivee, array(), $milieu_exclus, $max);
116 list($nom, $desc) = $depart;
118 return fabrique_jointures($boucle, $res, $cond, $desc, $nom, $col);
122 * Fabriquer une jointure à l'aide d'une liste descriptive d'étapes
125 * - la jointure dans le tableau $boucle->join,
126 * - la table de jointure dans le from
127 * - un modificateur 'lien'
129 * @uses nogroupby_if()
130 * @uses liste_champs_jointures()
132 * @param Boucle $boucle
133 * Description de la boucle
135 * Chaîne des jointures
137 * array(table_depart,array(table_arrivee,desc),jointure),
140 * Jointure peut être un tableau pour les jointures sur champ decomposé
141 * array('id_article','id_objet','objet','article')
142 * array('id_objet','id_article','objet','article')
144 * Flag pour savoir si le critère est conditionnel ou non
146 * Description de la table de départ
148 * Nom de la table de départ
150 * Colonne cible de la jointure
152 * Écrire les valeurs dans boucle->join en les échappant ou non ?
154 * Alias de la table de jointure (Lx)
156 function fabrique_jointures(&$boucle, $res, $cond = false, $desc = array(), $nom = '', $col = '', $echap = true) {
157 static $num = array();
159 $cpt = &$num[$boucle->descr
['nom']][$boucle->descr
['gram']][$boucle->id_boucle
];
160 foreach ($res as $cle => $r) {
161 list($d, $a, $j) = $r;
166 if (is_array($j)) { // c'est un lien sur un champ du type id_objet,objet,'article'
167 list($j1, $j2, $obj, $type) = $j;
168 // trouver de quel cote est (id_objet,objet)
169 if ($j1 == "id_$obj") {
170 $obj = "$id_table.$obj";
174 // le where complementaire est envoye dans la jointure pour pouvoir etre elimine avec la jointure
175 // en cas d'optimisation
176 //$boucle->where[] = array("'='","'$obj'","sql_quote('$type')");
177 $boucle->join
["L$n"] =
179 array("'$id_table'", "'$j2'", "'$j1'", "'$obj='.sql_quote('$type')")
181 array($id_table, $j2, $j1, "$obj=" . sql_quote($type));
183 $boucle->join
["L$n"] = $echap ?
array("'$id_table'", "'$j'") : array($id_table, $j);
185 $boucle->from
[$id_table = "L$n"] = $a[0];
189 // pas besoin de group by
190 // (cf http://article.gmane.org/gmane.comp.web.spip.devel/30555)
191 // si une seule jointure et sur une table avec primary key formee
192 // de l'index principal et de l'index de jointure (non conditionnel! [6031])
193 // et operateur d'egalite (http://trac.rezo.net/trac/spip/ticket/477)
195 if ($pk = (isset($a[1]) && (count($boucle->from
) == 2) && !$cond)) {
196 $pk = nogroupby_if($desc, $a[1], $col);
200 // si une seule jointure
201 // et si l'index de jointure est une primary key a l'arrivee !
203 and (count($boucle->from
) == 2)
204 and isset($a[1]['key']['PRIMARY KEY'])
205 and ($j == $a[1]['key']['PRIMARY KEY'])
210 // la clause Group by est en conflit avec ORDER BY, a completer
211 $groups = liste_champs_jointures($nom, $desc, true);
213 foreach ($groups as $id_prim) {
214 $id_field = $nom . '.' . $id_prim;
215 if (!in_array($id_field, $boucle->group
)) {
216 $boucle->group
[] = $id_field;
221 $boucle->modificateur
['lien'] = true;
227 * Condition suffisante pour qu'un Group-By ne soit pas nécéssaire
229 * À améliorer, notamment voir si calculer_select ne pourrait pas la réutiliser
230 * lorsqu'on sait si le critere conditionnel est finalement present
232 * @param array $depart
233 * @param array $arrivee
234 * @param string|array $col
237 function nogroupby_if($depart, $arrivee, $col) {
238 $pk = $arrivee['key']['PRIMARY KEY'];
242 $id_primary = $depart['key']['PRIMARY KEY'];
243 if (is_array($col)) {
244 $col = implode(', *', $col);
245 } // cas id_objet, objet
246 return (preg_match("/^$id_primary, *$col$/", $pk) or
247 preg_match("/^$col, *$id_primary$/", $pk));
251 * Lister les champs candidats a une jointure, sur une table
252 * si un join est fourni dans la description, c'est lui qui l'emporte
253 * sauf si cle primaire explicitement demandee par $primary
255 * sinon on construit une liste des champs a partir de la liste des cles de la table
260 * @param bool $primary
263 function liste_champs_jointures($nom, $desc, $primary = false) {
265 static $nojoin = array('idx', 'maj', 'date', 'statut');
267 // si cle primaire demandee, la privilegier
268 if ($primary && isset($desc['key']['PRIMARY KEY'])) {
269 return split_key($desc['key']['PRIMARY KEY']);
272 // les champs declares explicitement pour les jointures
273 if (isset($desc['join'])) {
274 return $desc['join'];
276 /*elseif (isset($GLOBALS['tables_principales'][$nom]['join'])) return $GLOBALS['tables_principales'][$nom]['join'];
277 elseif (isset($GLOBALS['tables_auxiliaires'][$nom]['join'])) return $GLOBALS['tables_auxiliaires'][$nom]['join'];*/
279 // si pas de cle, c'est fichu
280 if (!isset($desc['key'])) {
285 if (isset($desc['key']['PRIMARY KEY'])) {
286 return split_key($desc['key']['PRIMARY KEY']);
289 // ici on se rabat sur les cles secondaires,
290 // en eliminant celles qui sont pas pertinentes (idx, maj)
291 // si jamais le resultat n'est pas pertinent pour une table donnee,
292 // il faut declarer explicitement le champ 'join' de sa description
295 foreach ($desc['key'] as $v) {
296 $join = split_key($v, $join);
298 foreach ($join as $k) {
299 if (in_array($k, $nojoin)) {
308 * Eclater une cle composee en plusieurs champs
314 function split_key($v, $join = array()) {
315 foreach (preg_split('/,\s*/', $v) as $k) {
316 if (strpos($k, '(') !== false) {
317 $k = explode('(', $k);
318 $k = trim(reset($k));
326 * Constuire la chaine de jointures, de proche en proche
328 * @uses liste_champs_jointures()
329 * @uses trouver_champs_decomposes()
331 * @param objetc $boucle
332 * @param array $depart
333 * sous la forme array(nom de la table, description)
334 * @param array $arrivee
335 * sous la forme array(nom de la table, description)
337 * tables deja vues dans la jointure, pour ne pas y repasser
338 * @param array $milieu_exclus
339 * cles deja utilisees, pour ne pas les reutiliser
340 * @param int $max_liens
341 * nombre maxi d'etapes
344 function calculer_chaine_jointures(
349 $milieu_exclus = array(),
352 static $trouver_table;
353 if (!$trouver_table) {
354 $trouver_table = charger_fonction('trouver_table', 'base');
357 if (is_string($milieu_exclus)) {
358 $milieu_exclus = array($milieu_exclus);
360 // quand on a exclus id_objet comme cle de jointure, il faut aussi exclure objet
361 // faire une jointure sur objet tout seul n'a pas de sens
362 if (in_array('id_objet', $milieu_exclus) and !in_array('objet', $milieu_exclus)) {
363 $milieu_exclus[] = 'objet';
366 list($dnom, $ddesc) = $depart;
367 list($anom, $adesc) = $arrivee;
369 $vu[] = $dnom; // ne pas oublier la table de depart
370 $vu[] = $anom; // ne pas oublier la table d'arrivee
374 foreach ($adesc['key'] as $k) {
375 // respecter l'ordre de $adesc['key'] pour ne pas avoir id_trad en premier entre autres...
376 $akeys = array_merge($akeys, preg_split('/,\s*/', $k));
379 // enlever les cles d'arrivee exclues par l'appel
380 $akeys = array_diff($akeys, $milieu_exclus);
382 // cles candidates au depart
383 $keys = liste_champs_jointures($dnom, $ddesc);
384 // enlever les cles dde depart exclues par l'appel
385 $keys = array_diff($keys, $milieu_exclus);
387 $v = !$keys ?
false : array_intersect(array_values($keys), $akeys);
390 return array(array($dnom, array($adesc['table'], $adesc), array_shift($v)));
393 // regarder si l'on a (id_objet,objet) au depart et si on peut le mapper sur un id_xx
394 if (count(array_intersect(array('id_objet', 'objet'), $keys)) == 2) {
395 // regarder si l'une des cles d'arrivee peut se decomposer en
397 // si oui on la prend
398 foreach ($akeys as $key) {
399 $v = decompose_champ_id_objet($key);
401 $objet = array_shift($v); // objet,'article'
402 array_unshift($v, $key); // id_article,objet,'article'
403 array_unshift($v, $objet); // id_objet,id_article,objet,'article'
404 return array(array($dnom, array($adesc['table'], $adesc), $v));
408 // regarder si l'une des cles de depart peut se decomposer en
409 // id_objet,objet a l'arrivee
410 // si oui on la prend
411 foreach ($keys as $key) {
412 if (count($v = trouver_champs_decomposes($key, $adesc)) > 1) {
413 if (count($v) == count(array_intersect($v, $akeys))) {
414 $v = decompose_champ_id_objet($key); // id_objet,objet,'article'
415 array_unshift($v, $key); // id_article,id_objet,objet,'article'
416 return array(array($dnom, array($adesc['table'], $adesc), $v));
421 // si l'on voulait une jointure direct, c'est rate !
422 if ($max_liens <= 1) {
426 // sinon essayer de passer par une autre table
428 foreach ($boucle->jointures
as $v) {
430 and !in_array($v, $vu)
431 and $def = $trouver_table($v, $boucle->sql_serveur
)
432 and !in_array($def['table_sql'], $vu)
434 // ne pas tester les cles qui sont exclues a l'appel
435 // ie la cle de la jointure precedente
436 $test_cles = $milieu_exclus;
438 $max_iter = 50; // securite
439 while (count($jointure_directe_possible = calculer_chaine_jointures($boucle, $depart, array($v, $def), $vu,
442 $jointure_directe_possible = reset($jointure_directe_possible);
443 $milieu = end($jointure_directe_possible);
444 $exclure_fin = $milieu_exclus;
445 if (is_string($milieu)) {
446 $exclure_fin[] = $milieu;
447 $test_cles[] = $milieu;
449 $exclure_fin = array_merge($exclure_fin, $milieu);
450 $test_cles = array_merge($test_cles, $milieu);
452 // essayer de rejoindre l'arrivee a partir de cette etape intermediaire
453 // sans repasser par la meme cle milieu, ni une cle deja vue !
454 $r = calculer_chaine_jointures($boucle, array($v, $def), $arrivee, $new, $exclure_fin, $max_liens - 1);
456 array_unshift($r, $jointure_directe_possible);
468 * applatit les cles multiples
469 * redondance avec split_key() ? a mutualiser
474 function trouver_cles_table($keys) {
476 foreach ($keys as $v) {
477 if (!strpos($v, ",")) {
480 foreach (preg_split("/\s*,\s*/", $v) as $k) {
486 return array_keys($res);
491 * Indique si une colonne (ou plusieurs colonnes) est présente dans l'une des tables indiquée.
493 * @param string|array $cle
494 * Nom de la ou des colonnes à trouver dans les tables indiquées
495 * @param array $tables
496 * Liste de noms de tables ou des couples (alias => nom de table).
497 * - `$boucle->from` (alias => nom de table) : les tables déjà utilisées dans une boucle
498 * - `$boucle->jointures` : les tables utilisables en tant que jointure
499 * - `$boucle->jointures_explicites` les jointures explicitement indiquées à l'écriture de la boucle
500 * @param string $connect
501 * Nom du connecteur SQL
502 * @param bool|string $checkarrivee
503 * false : peu importe la table, si on trouve le/les champs, c'est bon.
504 * string : nom de la table où on veut trouver le champ.
505 * @return array|false
506 * false : on n'a pas trouvé
507 * array : infos sur la table trouvée. Les clés suivantes sont retournés :
508 * - 'desc' : tableau de description de la table,
509 * - 'table' : nom de la table
510 * - 'alias' : alias utilisé pour la table (si pertinent. ie: avec `$boucle->from` transmis par exemple)
512 function chercher_champ_dans_tables($cle, $tables, $connect, $checkarrivee = false) {
513 static $trouver_table = '';
514 if (!$trouver_table) {
515 $trouver_table = charger_fonction('trouver_table', 'base');
518 if (!is_array($cle)) {
522 foreach ($tables as $k => $table) {
523 if ($table && $desc = $trouver_table($table, $connect)) {
524 if (isset($desc['field'])
525 // verifier que toutes les cles cherchees sont la
526 and (count(array_intersect($cle, array_keys($desc['field']))) == count($cle))
527 // si on sait ou on veut arriver, il faut que ca colle
528 and ($checkarrivee == false ||
$checkarrivee == $desc['table'])
532 'table' => $desc['table'],
543 * Cherche une colonne (ou plusieurs colonnes) dans les tables de jointures
544 * possibles indiquées.
546 * @uses chercher_champ_dans_tables()
547 * @uses decompose_champ_id_objet()
548 * @uses liste_champs_jointures()
550 * @param string|array $cle
551 * Nom de la ou des colonnes à trouver dans les tables de jointures
552 * @param array $joints
553 * Liste des jointures possibles (ex: $boucle->jointures ou $boucle->jointures_explicites)
554 * @param Boucle $boucle
555 * Description de la boucle
556 * @param bool|string $checkarrivee
557 * false : peu importe la table, si on trouve le/les champs, c'est bon.
558 * string : nom de la table jointe où on veut trouver le champ.
559 * @return array|string
560 * chaîne vide : on n'a pas trouvé
561 * liste si trouvé : nom de la table, description de la table, clé(s) de la table
563 function trouver_champ_exterieur($cle, $joints, &$boucle, $checkarrivee = false) {
565 // support de la recherche multi champ :
566 // si en seconde etape on a decompose le champ id_xx en id_objet,objet
567 // on reentre ici soit en cherchant une table les 2 champs id_objet,objet
568 // soit une table avec les 3 champs id_xx, id_objet, objet
569 if (!is_array($cle)) {
573 if ($infos = chercher_champ_dans_tables($cle, $joints, $boucle->sql_serveur
, $checkarrivee)) {
574 return array($infos['table'], $infos['desc'], $cle);
577 // au premier coup, on essaye de decomposer, si possible
580 and is_array($decompose = decompose_champ_id_objet($c))
583 $desc = $boucle->show
;
585 // cas 1 : la cle id_xx est dans la table de depart
586 // -> on cherche uniquement id_objet,objet a l'arrivee
587 if (isset($desc['field'][$c])) {
589 $cle[] = array_shift($decompose); // id_objet
590 $cle[] = array_shift($decompose); // objet
591 return trouver_champ_exterieur($cle, $joints, $boucle, $checkarrivee);
593 // cas 2 : la cle id_xx n'est pas dans la table de depart
594 // -> il faut trouver une cle de depart zzz telle que
595 // id_objet,objet,zzz soit a l'arrivee
597 $depart = liste_champs_jointures((isset($desc['table']) ?
$desc['table'] : ''), $desc);
598 foreach ($depart as $d) {
600 $cle[] = array_shift($decompose); // id_objet
601 $cle[] = array_shift($decompose); // objet
603 if ($ext = trouver_champ_exterieur($cle, $joints, $boucle, $checkarrivee)) {
614 * Cherche a ajouter la possibilite d'interroger un champ sql dans une boucle.
616 * Cela construira les jointures necessaires
617 * si une possibilite est trouve et retournera le nom de
618 * l'alias de la table contenant ce champ
619 * (L2 par exemple pour 'spip_mots AS L2' dans le FROM),
621 * @uses trouver_champ_exterieur()
622 * @uses calculer_jointure()
624 * @param string $champ
625 * Nom du champ cherche (exemple id_article)
626 * @param object $boucle
627 * Informations connues de la boucle
628 * @param array $jointures
629 * Liste des tables parcourues (articles, mots) pour retrouver le champ sql
630 * et calculer la jointure correspondante.
631 * En son absence et par defaut, on utilise la liste des jointures connues
632 * par SPIP pour la table en question ($boucle->jointures)
634 * flag pour savoir si le critere est conditionnel ou non
635 * @param bool|string $checkarrivee
636 * false : peu importe la table, si on trouve le/les champs, c'est bon.
637 * string : nom de la table jointe où on veut trouver le champ.
641 function trouver_jointure_champ($champ, &$boucle, $jointures = false, $cond = false, $checkarrivee = false) {
642 if ($jointures === false) {
643 $jointures = $boucle->jointures
;
645 // TODO : aberration, on utilise $jointures pour trouver le champ
646 // mais pas poour construire la jointure ensuite
647 $arrivee = trouver_champ_exterieur($champ, $jointures, $boucle, $checkarrivee);
649 $desc = $boucle->show
;
650 array_pop($arrivee); // enlever la cle en 3eme argument
651 $cle = calculer_jointure($boucle, array($desc['id_table'], $desc), $arrivee, '', $cond);
656 spip_log("trouver_jointure_champ: $champ inconnu");