[SPIP][PLUGINS] v3.0-->v3.2
[lhc/web/www.git] / www / ecrire / public / jointures.php
1 <?php
2
3 /***************************************************************************\
4 * SPIP, Systeme de publication pour l'internet *
5 * *
6 * Copyright (c) 2001-2017 *
7 * Arnaud Martin, Antoine Pitrou, Philippe Riviere, Emmanuel Saint-James *
8 * *
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 \***************************************************************************/
12
13 /**
14 * Déduction automatique d'une chaîne de jointures
15 *
16 * @package SPIP\Core\Compilateur\Jointures
17 **/
18
19 if (!defined('_ECRIRE_INC_VERSION')) {
20 return;
21 }
22
23
24 /**
25 * Décomposer un champ id_truc en (id_objet,objet,truc)
26 *
27 * Exemple : décompose id_article en (id_objet,objet,article)
28 *
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)
34 */
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]));
38 }
39
40 return $champ;
41 }
42
43 /**
44 * Mapping d'un champ d'une jointure en deux champs id_objet,objet si nécessaire
45 *
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.
49 *
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
53 * @return array
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
57 */
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'])
61 ) {
62 return array($champ);
63 }
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)) {
68 return $decompose;
69 }
70 }
71
72 return array($champ);
73 }
74
75
76 /**
77 * Calculer et construite une jointure entre $depart et $arrivee
78 *
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,
81 * en general un "Lx"
82 *
83 * @uses calculer_chaine_jointures()
84 * @uses fabrique_jointures()
85 *
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)
92 * @param string $col
93 * Colonne cible de la jointure
94 * @param bool $cond
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.
98 * @return string
99 * Alias de la table de jointure (Lx)
100 */
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
105 $max = 1;
106 $res = false;
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);
110 $max++;
111 }
112 if (!$res) {
113 return "";
114 }
115
116 list($nom, $desc) = $depart;
117
118 return fabrique_jointures($boucle, $res, $cond, $desc, $nom, $col);
119 }
120
121 /**
122 * Fabriquer une jointure à l'aide d'une liste descriptive d'étapes
123 *
124 * Ajoute
125 * - la jointure dans le tableau $boucle->join,
126 * - la table de jointure dans le from
127 * - un modificateur 'lien'
128 *
129 * @uses nogroupby_if()
130 * @uses liste_champs_jointures()
131 *
132 * @param Boucle $boucle
133 * Description de la boucle
134 * @param array $res
135 * Chaîne des jointures
136 * $res = array(
137 * array(table_depart,array(table_arrivee,desc),jointure),
138 * ...
139 * )
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')
143 * @param bool $cond
144 * Flag pour savoir si le critère est conditionnel ou non
145 * @param array $desc
146 * Description de la table de départ
147 * @param string $nom
148 * Nom de la table de départ
149 * @param string $col
150 * Colonne cible de la jointure
151 * @param bool $echap
152 * Écrire les valeurs dans boucle->join en les échappant ou non ?
153 * @return string
154 * Alias de la table de jointure (Lx)
155 */
156 function fabrique_jointures(&$boucle, $res, $cond = false, $desc = array(), $nom = '', $col = '', $echap = true) {
157 static $num = array();
158 $id_table = "";
159 $cpt = &$num[$boucle->descr['nom']][$boucle->descr['gram']][$boucle->id_boucle];
160 foreach ($res as $cle => $r) {
161 list($d, $a, $j) = $r;
162 if (!$id_table) {
163 $id_table = $d;
164 }
165 $n = ++$cpt;
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";
171 } else {
172 $obj = "L$n.$obj";
173 }
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"] =
178 $echap ?
179 array("'$id_table'", "'$j2'", "'$j1'", "'$obj='.sql_quote('$type')")
180 :
181 array($id_table, $j2, $j1, "$obj=" . sql_quote($type));
182 } else {
183 $boucle->join["L$n"] = $echap ? array("'$id_table'", "'$j'") : array($id_table, $j);
184 }
185 $boucle->from[$id_table = "L$n"] = $a[0];
186 }
187
188
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)
194
195 if ($pk = (isset($a[1]) && (count($boucle->from) == 2) && !$cond)) {
196 $pk = nogroupby_if($desc, $a[1], $col);
197 }
198
199 // pas de group by
200 // si une seule jointure
201 // et si l'index de jointure est une primary key a l'arrivee !
202 if (!$pk
203 and (count($boucle->from) == 2)
204 and isset($a[1]['key']['PRIMARY KEY'])
205 and ($j == $a[1]['key']['PRIMARY KEY'])
206 ) {
207 $pk = true;
208 }
209
210 // la clause Group by est en conflit avec ORDER BY, a completer
211 $groups = liste_champs_jointures($nom, $desc, true);
212 if (!$pk) {
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;
217 }
218 }
219 }
220
221 $boucle->modificateur['lien'] = true;
222
223 return "L$n";
224 }
225
226 /**
227 * Condition suffisante pour qu'un Group-By ne soit pas nécéssaire
228 *
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
231 *
232 * @param array $depart
233 * @param array $arrivee
234 * @param string|array $col
235 * @return bool
236 */
237 function nogroupby_if($depart, $arrivee, $col) {
238 $pk = $arrivee['key']['PRIMARY KEY'];
239 if (!$pk) {
240 return false;
241 }
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));
248 }
249
250 /**
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
254 *
255 * sinon on construit une liste des champs a partir de la liste des cles de la table
256 *
257 * @uses split_key()
258 * @param string $nom
259 * @param array $desc
260 * @param bool $primary
261 * @return array
262 */
263 function liste_champs_jointures($nom, $desc, $primary = false) {
264
265 static $nojoin = array('idx', 'maj', 'date', 'statut');
266
267 // si cle primaire demandee, la privilegier
268 if ($primary && isset($desc['key']['PRIMARY KEY'])) {
269 return split_key($desc['key']['PRIMARY KEY']);
270 }
271
272 // les champs declares explicitement pour les jointures
273 if (isset($desc['join'])) {
274 return $desc['join'];
275 }
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'];*/
278
279 // si pas de cle, c'est fichu
280 if (!isset($desc['key'])) {
281 return array();
282 }
283
284 // si cle primaire
285 if (isset($desc['key']['PRIMARY KEY'])) {
286 return split_key($desc['key']['PRIMARY KEY']);
287 }
288
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
293
294 $join = array();
295 foreach ($desc['key'] as $v) {
296 $join = split_key($v, $join);
297 }
298 foreach ($join as $k) {
299 if (in_array($k, $nojoin)) {
300 unset($join[$k]);
301 }
302 }
303
304 return $join;
305 }
306
307 /**
308 * Eclater une cle composee en plusieurs champs
309 *
310 * @param string $v
311 * @param array $join
312 * @return array
313 */
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));
319 }
320 $join[$k] = $k;
321 }
322 return $join;
323 }
324
325 /**
326 * Constuire la chaine de jointures, de proche en proche
327 *
328 * @uses liste_champs_jointures()
329 * @uses trouver_champs_decomposes()
330 *
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)
336 * @param array $vu
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
342 * @return array
343 */
344 function calculer_chaine_jointures(
345 &$boucle,
346 $depart,
347 $arrivee,
348 $vu = array(),
349 $milieu_exclus = array(),
350 $max_liens = 5
351 ) {
352 static $trouver_table;
353 if (!$trouver_table) {
354 $trouver_table = charger_fonction('trouver_table', 'base');
355 }
356
357 if (is_string($milieu_exclus)) {
358 $milieu_exclus = array($milieu_exclus);
359 }
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';
364 }
365
366 list($dnom, $ddesc) = $depart;
367 list($anom, $adesc) = $arrivee;
368 if (!count($vu)) {
369 $vu[] = $dnom; // ne pas oublier la table de depart
370 $vu[] = $anom; // ne pas oublier la table d'arrivee
371 }
372
373 $akeys = array();
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));
377 }
378
379 // enlever les cles d'arrivee exclues par l'appel
380 $akeys = array_diff($akeys, $milieu_exclus);
381
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);
386
387 $v = !$keys ? false : array_intersect(array_values($keys), $akeys);
388
389 if ($v) {
390 return array(array($dnom, array($adesc['table'], $adesc), array_shift($v)));
391 }
392
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
396 // id_objet,objet
397 // si oui on la prend
398 foreach ($akeys as $key) {
399 $v = decompose_champ_id_objet($key);
400 if (is_array($v)) {
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));
405 }
406 }
407 } else {
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));
417 }
418 }
419 }
420 }
421 // si l'on voulait une jointure direct, c'est rate !
422 if ($max_liens <= 1) {
423 return array();
424 }
425
426 // sinon essayer de passer par une autre table
427 $new = $vu;
428 foreach ($boucle->jointures as $v) {
429 if ($v
430 and !in_array($v, $vu)
431 and $def = $trouver_table($v, $boucle->sql_serveur)
432 and !in_array($def['table_sql'], $vu)
433 ) {
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;
437 $new[] = $v;
438 $max_iter = 50; // securite
439 while (count($jointure_directe_possible = calculer_chaine_jointures($boucle, $depart, array($v, $def), $vu,
440 $test_cles, 1))
441 and $max_iter--) {
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;
448 } else {
449 $exclure_fin = array_merge($exclure_fin, $milieu);
450 $test_cles = array_merge($test_cles, $milieu);
451 }
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);
455 if ($r) {
456 array_unshift($r, $jointure_directe_possible);
457
458 return $r;
459 }
460 }
461 }
462 }
463
464 return array();
465 }
466
467 /**
468 * applatit les cles multiples
469 * redondance avec split_key() ? a mutualiser
470 *
471 * @param $keys
472 * @return array
473 */
474 function trouver_cles_table($keys) {
475 $res = array();
476 foreach ($keys as $v) {
477 if (!strpos($v, ",")) {
478 $res[$v] = 1;
479 } else {
480 foreach (preg_split("/\s*,\s*/", $v) as $k) {
481 $res[$k] = 1;
482 }
483 }
484 }
485
486 return array_keys($res);
487 }
488
489
490 /**
491 * Indique si une colonne (ou plusieurs colonnes) est présente dans l'une des tables indiquée.
492 *
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)
511 */
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');
516 }
517
518 if (!is_array($cle)) {
519 $cle = array($cle);
520 }
521
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'])
529 ) {
530 return array(
531 'desc' => $desc,
532 'table' => $desc['table'],
533 'alias' => $k,
534 );
535 }
536 }
537 }
538
539 return false;
540 }
541
542 /**
543 * Cherche une colonne (ou plusieurs colonnes) dans les tables de jointures
544 * possibles indiquées.
545 *
546 * @uses chercher_champ_dans_tables()
547 * @uses decompose_champ_id_objet()
548 * @uses liste_champs_jointures()
549 *
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
562 */
563 function trouver_champ_exterieur($cle, $joints, &$boucle, $checkarrivee = false) {
564
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)) {
570 $cle = array($cle);
571 }
572
573 if ($infos = chercher_champ_dans_tables($cle, $joints, $boucle->sql_serveur, $checkarrivee)) {
574 return array($infos['table'], $infos['desc'], $cle);
575 }
576
577 // au premier coup, on essaye de decomposer, si possible
578 if (count($cle) == 1
579 and $c = reset($cle)
580 and is_array($decompose = decompose_champ_id_objet($c))
581 ) {
582
583 $desc = $boucle->show;
584
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])) {
588 $cle = array();
589 $cle[] = array_shift($decompose); // id_objet
590 $cle[] = array_shift($decompose); // objet
591 return trouver_champ_exterieur($cle, $joints, $boucle, $checkarrivee);
592 }
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
596 else {
597 $depart = liste_champs_jointures((isset($desc['table']) ? $desc['table'] : ''), $desc);
598 foreach ($depart as $d) {
599 $cle = array();
600 $cle[] = array_shift($decompose); // id_objet
601 $cle[] = array_shift($decompose); // objet
602 $cle[] = $d;
603 if ($ext = trouver_champ_exterieur($cle, $joints, $boucle, $checkarrivee)) {
604 return $ext;
605 }
606 }
607 }
608 }
609
610 return "";
611 }
612
613 /**
614 * Cherche a ajouter la possibilite d'interroger un champ sql dans une boucle.
615 *
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),
620 *
621 * @uses trouver_champ_exterieur()
622 * @uses calculer_jointure()
623 *
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)
633 * @param bool $cond
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.
638 *
639 * @return string
640 */
641 function trouver_jointure_champ($champ, &$boucle, $jointures = false, $cond = false, $checkarrivee = false) {
642 if ($jointures === false) {
643 $jointures = $boucle->jointures;
644 }
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);
648 if ($arrivee) {
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);
652 if ($cle) {
653 return $cle;
654 }
655 }
656 spip_log("trouver_jointure_champ: $champ inconnu");
657
658 return '';
659 }