PHP e MySQL: una classe per stampare alberi gerarchici con liste annidate

Lista Modified Preorder Tree TraversalLa soluzione denominata “Modified Preorder Tree Traversal”, che mal si traduce in italiano, è probabilmente la soluzione più efficace per immagazzinare dati di tipo gerarchico in un database. La soluzione tipica alternativa è il modello a liste di adiacenza. Quest’ultimo modello soffre di alcune limitazioni e debolezze, come ad esempio la particolar cura che deve essere presa nella cancellazione di sotto-alberi che può portare a potenziali figli orfani, e la difficoltà di stesura delle query SQL.
In questo articolo mi riferirò quindi esclusivamente al modello Modified Preorder Tree Traversal. Questo modello è molto ben descritto in questo articolo di Mike Hillyer sul sito ufficiale MySQL nel paragrafo denominato: “Nested Set Model”. Il concetto chiave che sta alla base del modello è di non considerare più la struttura gerarchica come un insieme di nodi e di linee, ma come una serie di insiemi e sottoinsiemi annidati. Il concetto sarà più chiaro dopo aver letto l’articolo e la figura esplicativa della struttura, riportata qui sotto.
treeGraphLe varie query necessarie a gestire il database sono state riprese e tradotte in funzioni PHP da Daevid Vincent in questo thread su questo forum. Il lavoro di Daevid si è rivelato davvero prezioso, e mi ha fatto risparmiare davvero molto tempo, però non ero completamente soddisfatto del sistema di stampa HTML dell’albero. Infatti la funzione utilizza “blank space” per la rappresentazione dell’indentazione, mentre sarebbe più corretto ed elegante utilizzare liste annidate di tipo <UL>. Ho fatto quindi alcune modifiche alla funzione per raggiungere questo scopo.

Queste sono le query per creare il database campione e popolarlo con alcuni dati di esempio:

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE nested_category (
 category_id INT AUTO_INCREMENT PRIMARY KEY,
 name VARCHAR(20) NOT NULL,
 lft INT NOT NULL,
 rgt INT NOT NULL
);
 
INSERT INTO nested_category
VALUES(1,'ELECTRONICS',1,20),(2,'TELEVISIONS',2,9),(3,'TUBE',3,4),
(4,'LCD',5,6),(5,'PLASMA',7,8),(6,'PORTABLE ELECTRONICS',10,19),
(7,'MP3 PLAYERS',11,14),(8,'FLASH',12,13),
(9,'CD PLAYERS',15,16),(10,'2 WAY RADIOS',17,18);

E questo il codice della classe modificata:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class preorderModelTree {
 
  var $dbConn;
 
  function preorderModelTree ($resConn) {
    $this->dbConn = $resConn;
  }
 
  function dbQuery($sql){
    $rs = @mysql_query($sql,$this->dbConn);
    return $rs;
  }
 
  function fetchRow($rs) {
    if ( $row = @mysql_fetch_array($rs,MYSQL_ASSOC) ) {
        return $row;
    } else {
        return false;
    }
  }
 
  function print_nested_tree_list($pageLink = "?id=", $id = 1)
  {
    $sth = $this->dbQuery("SELECT node.category_id as id, node.name AS name,
    (COUNT(parent.name) - 1) as indent
    FROM nested_category AS node, nested_category
    AS parent
    WHERE node.lft BETWEEN parent.lft
    AND parent.rgt  
    GROUP BY node.name
    ORDER BY node.lft");
 
    if ($sth) {
    $oldRowIndent = -1;
 
    while($row = $this->fetchRow($sth))
    {
      if($row['id'] == $id){
        echo("<ul><li>". $row['name']);
      }
      elseif($row['indent']>$oldRowIndent){
        echo("<ul><li><a href='".$pageLink.$row['id']."'>". $row['name'] ."</a>");
      }
      elseif($row['indent']==$oldRowIndent){
        echo("</li><li><a href='".$pageLink.$row['id']."'>". $row['name'] ."</a>");
      }
      else {
        echo("</li>");
        echo str_repeat("</ul></li>", $oldRowIndent-$row['indent']);
        echo("<li><a href='".$pageLink.$row['id']."'>". $row['name'] ."</a>");
      }
      $oldRowIndent = $row['indent'];
    }
    echo("</li>");
    echo str_repeat("</ul></li>", $oldRowIndent);
    echo("</ul>");
   }    
  }  
}

Come si evince dal codice, la chiave di tutto è la query ben fatta. Il campo calcolato indent fornisce la misura dell’indentazione che è rappresentata dal numero di gusci in cui la categoria è compresa. Tramite questo valore è possibile costruire il corretto annidamento di elementi <ul> e <li>.

Il pacchetto contenente la classe, il codice sql e un file di esempio è scaricabile qui.

Un ringraziamento particolare per le “imbeccate” ed i consigli, all’amico e collega Alessandro Scoscia.

Conclusioni:

Un ulteriore miglioramento della rappresentazione dell’albero gerarchico potrebbe venire dall’utilizzo di JQuery. Tramite questa libreria javascript si potrebbe implementare un’azione tipo toggle con la quale mostrare o nascondere i sotto-alberi selezionati.

Riferimenti ed approfondimenti:


evilripper ha scritto:

preziossimo articolo!!
grazie per tutte le informazioni!!
ciao