Tak więc ostatnio gdy starałem się o pracę w pewnej firmie, dostałem jako zlecenie stworzenie menu o strukturze drzewa z nieograniczonymi gałęziami, liśćmi. Każdy kto programuje chwilkę w końcu natknie się na ten problem i będzie szukał rozwiązania w internecie. Tak samo było zemną, no więc założenia do projektu:

  • struktura drzewa
  • nieograniczona ilość gałęzi i liści
  • przenoszenie gałęzi wraz z zawartością
  • usuwanie gałęzi wraz z zawartością

Są to podstawowe założenia do projektu jakie są nam potrzebne, tak więc wyruszając w przygodę z google i znalezieniem jakiejś ciekawej struktury dla sql, na ircu od adi^R dostałem ciekawego linka depesz.com. Gościu opisuje jak rozwiązać ten problem z poziomu sql’a. No tak więc mamy już jeden problem z głowy (poniżej opiszę wszystko po kolei), został nam drugi, algorytm który będzie tą strukturę drzewa tworzył w php i zrobi z nam tego tablicę taką żeby móc ja ładnie użyć z smartami (tak przeszedłem na smarty z ITX, dlaczego? a tak jakoś). No i tu były schody, ponieważ twórcy smartów nie przewidzieli, lub nie chcieli przewidzieć rekurencji w ich skrypcie. Co nam pozostaje? W rekurencji includowanie tego samego pliku przy każdym kolejnym wykonaniu operacji, lub znaleźć plugin który to zrobi za nas.

Dobra po kolei, zacznijmy od kodu sql który jest na potrzebny i niezbędny do życia, więc na pewno potrzebne nam są dwie tabele oto one:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE TABLE "kategorie" (
   "id" serial PRIMARY KEY,
   "name" text, -- ważne name nie jest unique
   "parent_id" integer,
   FOREIGN KEY ("parent_id") REFERENCES "kategorie" ("id") ON DELETE cascade
);
 
CREATE TABLE "powiazania" (
    "id" serial PRIMARY KEY,
    "parent_id" integer NOT NULL,
    "child_id" integer NOT NULL,
    "depth" integer NOT NULL,
    FOREIGN KEY ("parent_id") REFERENCES "kategorie" ("id") ON DELETE cascade,
    FOREIGN KEY ("child_id") REFERENCES "kategorie" ("id") ON DELETE cascade
);

Poprawiłem tutaj trochę po fabryce, czyli po Hubercie Lubaczewskim, autorem tego skryptu. Bo tak mi pasowało zresztą gdy porównacie mój kod sql (mowa o tych 2 tabelach) zobaczycie różnice.

Następne co nam będzie potrzebne to funkcja oraz triger która dodaje za nas powiązania:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CREATE LANGUAGE plpgsql; 
 
CREATE OR REPLACE FUNCTION tree_objects_ai() RETURNS TRIGGER AS
 $BODY$
 DECLARE
 BEGIN
  INSERT INTO powiazania (parent_id, child_id, depth) VALUES (NEW.id, NEW.id, 0);
  INSERT INTO powiazania (parent_id, child_id, depth) SELECT x.parent_id, NEW.id, x.depth + 1 FROM powiazania x WHERE x.child_id = NEW.parent_id;
  RETURN NEW;
 END;
 $BODY$
LANGUAGE 'plpgsql';
 
-- Dzięki tej procedurze, nie ważne czy mamy zagnieżdżenie 2, 3, 9, 231 stopniowe,
-- i tak za każdym razem wykonujemy tylko 1 insert
 
CREATE TRIGGER tree_objects_ai AFTER INSERT ON kategorie FOR EACH ROW EXECUTE PROCEDURE tree_objects_ai();
 
-- Tworzymy trigera który po każdym INSERT'cie wykona procedure trg_powiazania_i, dlaczego trigger i procedura
-- mają taką samą nazwę? Ponoć ułatwia analizę bazy danych... Ja tak tak to czemu nie ;]

Nie będę tutaj się rozpisywał ocb z tymi procedurami, trigerami i powiązaniami pomiędzy tabelami, wszystko możecie sobie doczytać na stronie Huberta.

To był funkcja która tworzy nam powiązania a teraz funkcja która przerzuca gałęzie drzewa pomiędzy sobą.

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
CREATE OR REPLACE FUNCTION tree_objects_au() RETURNS TRIGGER AS
  $BODY$
  DECLARE
  BEGIN
    IF NOT OLD.parent_id IS DISTINCT FROM NEW.parent_id THEN
      RETURN NEW;
    END IF;
    IF OLD.parent_id IS NOT NULL THEN
      DELETE FROM powiazania WHERE id IN (
        SELECT r2.id FROM powiazania r1 JOIN powiazania r2 ON r1.child_id = r2.child_id
        WHERE r1.parent_id = NEW.id AND r2.depth > r1.depth
      );
    END IF;
    IF NEW.parent_id IS NOT NULL THEN
      INSERT INTO powiazania (parent_id, child_id, depth)
      SELECT r1.parent_id, r2.child_id, r1.depth + r2.depth + 1
      FROM
      powiazania r1,
      powiazania r2
      WHERE
      r1.child_id = NEW.parent_id AND
      r2.parent_id = NEW.id;
      END IF;
      RETURN NEW;
  END;
  $BODY$
  LANGUAGE 'plpgsql';
 
CREATE TRIGGER tree_objects_au AFTER UPDATE ON kategorie FOR EACH ROW EXECUTE PROCEDURE tree_objects_au();
 
-- triger i procedura dzięki której przerzucamy bez problemu gałęzie

Dobra mamy już cały SQL potrzebny i niezbędny do zbudowania takiego menu opartego na wielu poziomach.
A teraz trochę php:

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
<?php
/**
 * @li gnu/agpl v3 or leter
 * @code utf8
 * @version 0.1
 * @author Przemysław Czekaj
 * @date 09.07.09
 * @file controlers/tpl.php
**/
 
require_once('models/view.php');
 
class ShowData
{
  private $tpl;
  private $obj;
 
  public function __construct($tepel)
  {
    $this->tpl = $tepel;
    $this->obj = new GetData;
    $this->parseKat();
    $this->parseTree();
  }
 
 
  private function parseKat()
  {
    $data = $this->obj->_get_kat();
    $this->tpl->assign('kategorie',$data);
  }
 
  private function setSort(&$sort)
  {
    if($sort != 'ASC' && $sort != 'DESC')
     $sort = 'ASC';
  }
 
  private function parseTree()
  {
    $sort_kat = $_POST['sort_kat'];
    $sort_list = $_POST['sort_list'];
    $this->setSort($sort_kat);
    $this->setSort($sort_list);
    $mast = $this->obj->_get_master_kat($sort_kat);
    $n = count($mast);
    for($i = 0; $i < $n; $i++)
    {
      $tablica['element'][$i]['name'] = $mast[$i]['name'];
      $tablica['element'][$i] = $this->obj->_get_tree($mast[$i]['id'], $tablica['element'][$i], $sort_list);
    }
 
    $this->tpl->assign('tree',$tablica);
  }
}
 
?>

Co to jest? Jest to kontroler templatki, nie chciało mi się tworzyć jakiegoś MVC to zrobiłem pseudo MVC.
Pasowało by Wam pokazać jak to tworzy te drzewo co? No to jeszcze jeden plik z modelu, odpowiedzialny za widok.

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
<?php
/**
 * @li gnu/agpl v3 or leter
 * @code utf8
 * @version 0.1
 * @author Przemysław Czekaj
 * @date 09.07.09
 * @file models/view.php
**/
 
class GetData
{
 
  public function _get_kat()
  {
    $kat = Doctrine::getTable('Kategoria')->findAll();
    return $kat->toArray();
  }
 
  public function _get_master_kat($sort)
  {
    $kat = Doctrine_Query::create()->query("SELECT k.name FROM Kategoria k WHERE NOT EXISTS (SELECT * FROM Powiazanie p WHERE k.id = p.child_id AND p.depth = 1) ORDER BY k.id $sort");
    return $kat->toArray();
  }
 
  public function _get_sub_kat($id_kat, $sort)
  {
    $kat = Doctrine_Query::create()->query("SELECT k.name FROM Kategoria k JOIN k.Powiazania p ON k.id = p.child_id WHERE p.parent_id = $id_kat AND p.depth = 1 ORDER BY k.id $sort");
    return $kat->toArray();
  }
 
  public function _get_tree($id, $tab, $sort)
  {
    $sub = $this->_get_sub_kat($id, $sort);
    $nn = count($sub);
    for($j = 0; $j < $nn; $j++)
    {
      $tab['element'][$j]['name'] = $sub[$j]['name'];
      $tab['element'][$j] = self::_get_tree($sub[$j]['id'],$tab['element'][$j], $sort);
    }
    return $tab;  
  }
 
}
 
?>

No i tak, teraz jeszcze przydałby by się modele dla doctrine, już pokazuję:

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
<?php
/**
 * @li gnu/agpl v3 or leter
 * @code utf8
 * @version 0.1
 * @author Przemysław Czekaj
 * @date 07.07.09
 * @file models/doctrine/kategorie.php
**/ 
 
class Kategoria extends Doctrine_Record
{
 
  public function setTableDefinition()
  {
    $this->setTableName('kategorie');
    $this->hasColumn('name','string');
    $this->hasColumn('parent_id','integer');
  }
 
  public function setUp()
  {
    $this->hasMany('Powiazanie as Powiazania', array (
        'local' => 'id',
        'foreign' => 'parent_id',
        )
    );
 
    $this->hasMany('Powiazanie as Powiazanias', array (
        'local' => 'id',
        'foreign' => 'child_id',
        )
    );
 
    $this->hasMany('Kategoria as Kategorie', array (
        'local' => 'parent_id',
        'foreign' => 'id'
        )
    );
  }
}
 
?>

Nie ma tu nic ciekawego, trochę małe zamieszanie wprowadziłem z powiązaniami… przez co nie wiadomo z jakiej przyczyny nie chciał mi działać joinLeft w doctrine, dlatego zmuszony byłem użyć query().

Kolejny model:

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
<?php
/**
 * @li gnu/agpl v3 or leter
 * @code utf8
 * @version 0.1
 * @author Przemysław Czekaj
 * @date 07.07.09
 * @file models/doctrine/powiazania.php
**/ 
 
class Powiazanie extends Doctrine_Record
{
  public function setTableDefinition()
  {
    $this->setTableName('powiazania');
    $this->hasColumn('parent_id','integer');
    $this->hasColumn('child_id','integer');
    $this->hasColumn('depth','integer');
  }
 
 
  public function setUp()
  {
    $this->hasMany('Kategoria as Kategorie', array (
        'local' => 'parent_id',
        'foreign' => 'id',
        )
    );
 
    $this->hasMany('Kategoria as Kategories', array (
        'local' => 'child_id',
        'foreign' => 'id',
        )
    );
  }
}
 
?>

Jak będę dopisywał to do swojego “cms’a” to obiecuję poprawić powiązania ;)

Dobra, no to mamy praktycznie wszystko co nam było potrzebne, jeszcze dorzucę dwie rzeczy:

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
<?php
/**
 * @li gnu/agpl v3 or leter
 * @code utf8
 * @version 0.1
 * @author Przemysław Czekaj
 * @date 07.07.09
 * @file models/create.php
**/ 
 
class Dodawanie
{
 
  public function MasterKat($kat)
  {
    $query = new Kategoria;
    $query->name = $kat;
    $query->save();
  }
 
  public function SubKat($kat,$parent)
  {
    $query = new Kategoria;
    $query->name = $kat;
    $query->parent_id = $parent;
    $query->save();
  }
 
}
?>
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
<?php
/**
 * @li gnu/agpl v3 or leter
 * @code utf8
 * @version 0.3
 * @author Przemysław Czekaj
 * @date 09.07.09
 * @file models/delete.php
**/ 
 
class Niszczenie
{
  public function czyIstniejeKat($kat)
  {
    $query = Doctrine_Query::create()
              ->from('Kategoria')
              ->where('name = ?',$kat)
              ->orWhere('id = ?',(int)$kat);
    $result = $query->execute();
    $result = $result->toArray();
    if(is_array($result[0]))
      return true;
    else
      return false;
  }
 
   public function czyDoPotomka($id_kat,$id_pod)
   {
     $query = Doctrine_Query::create()->query("SELECT k.id, k.name, k.parent_id FROM Kategoria k JOIN k.Powiazania p ON k.id = p.child_id WHERE p.parent_id = $id_kat AND p.depth <> 0");
     $result = $query->toArray();
     $n = count($result);
     for($i = 0; $i < $n; $i++)
     {
       if($result[$i]['id'] == (int)$id_pod)
         throw new Exception ('Nie można przenosić głównej kategorii do sub kategorii danej kategorii'); 
     }
   } //nie chce działać...
 
  public function usunKategorie($kat)
  {
    $query = Doctrine_Query::create()
            ->delete('Kategoria')
            ->where('name = ?',$kat)
            ->orWhere('id = ?',(int)$kat);
    $result = $query->execute();
    return $result;
  }
 
}
 
?>
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
<?php
/**
 * @li gnu/agpl v3 or leter
 * @code utf8
 * @version 0.1
 * @author Przemysław Czekaj
 * @date 09.07.09
 * @file models/move.php
**/ 
 
class Posuwanie // xD
{
 
  public function MoveKat($kat,$parent)
  {
    $query = Doctrine_Query::create()
              ->update('Kategoria')
              ->set('parent_id','?',(int)$parent)
              ->where('id = '.(int)$kat);
    $result = $query->execute();
    return $result;
  }
 
}
?>

Tak więc mamy tutaj 3 modele, do tworzenia kategorii, do usuwania kategorii, oraz do przenoszenia kategorii.

Teraz pora na kontrolery, tak więc zobaczmy:

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
<?php
/**
 * @li gnu/agpl v3 or leter
 * @code utf8
 * @version 0.1
 * @author Przemysław Czekaj
 * @date 07.07.09
 * @file controlers/dodaj.php
**/ 
 
require_once('controlers/validate.php');
 
final class HandlerDodaj
{
  private $handle;
  private $ref;
 
  public function __construct($handle)
  {
    $this->handle = $handle;
  }
 
  public function handled_event($ref)
  {
    $this->ref = $ref;
    $this->ref = 'valAdd'.ucfirst($this->ref);
    if(method_exists('Validate',$this->ref))
    {
      $val = new Validate;
      if(is_callable(array($val,$this->ref),true))
      {
        $val->{$this->ref}();
        return true;
      }
      throw new Exception ('Nie można wywołać zdarzenia');
 
      return true;
    }
    throw new Exception ('Brak obsługi zdarzenia');
  }
}
?>
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
<?php
/**
 * @li gnu/agpl v3 or leter
 * @code utf8
 * @version 0.1
 * @author Przemysław Czekaj
 * @date 07.07.09
 * @file controlers/przenies.php
**/ 
 
require_once('controlers/validate.php');
 
final class HandlerPrzenies
{
  private $handle;
  private $ref;
 
  public function __construct($handle)
  {
    $this->handle = $handle;
  }
 
  public function handled_event($ref)
  {
    $this->ref = $ref;
    $this->ref = 'valPrzenies'.ucfirst($this->ref);
    if(method_exists('Validate',$this->ref))
    {
      $val = new Validate;
      if(is_callable(array($val,$this->ref),true))
      {
        $val->{$this->ref}();
        return true;
      }
      throw new Exception ('Nie można wywołać zdarzenia');
 
      return true;
    }
    throw new Exception ('Brak obsługi zdarzenia');
  }
}
?>
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
<?php
/**
 * @li gnu/agpl v3 or leter
 * @code utf8
 * @version 0.1
 * @author Przemysław Czekaj
 * @date 07.07.09
 * @file controlers/usun.php
**/ 
 
require_once('controlers/validate.php');
 
final class HandlerUsun
{
  private $handle;
  private $ref;
 
  public function __construct($handle)
  {
    $this->handle = $handle;
  }
 
  public function handled_event($ref)
  {
    $this->ref = $ref;
    $this->ref = 'valDelete'.ucfirst($this->ref);
    if(method_exists('Validate',$this->ref))
    {
      $val = new Validate;
      if(is_callable(array($val,$this->ref),true))
      {
        $val->{$this->ref}();
        return true;
      }
      throw new Exception ('Nie można wywołać zdarzenia');
 
      return true;
    }
    throw new Exception ('Brak obsługi zdarzenia');
 
    return true;
  }
} 
?>
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
<?php
/**
 * @li gnu/agpl v3 or leter
 * @code utf8
 * @version 0.1
 * @author Przemysław Czekaj
 * @date 07.07.09
 * @file controlers/validate.php
**/
 
require_once('models/delete.php');
require_once('models/create.php');
require_once('models/move.php');
 
final class Validate
{
 
  private $check;
  private $add;
 
  public function __construct()
  {
    $this->check = new Niszczenie;
  }
 
  public function valDeleteKat()
  {
    $nazwa_kat = trim($_POST['nazwa_kategorii']);
    $nazwa_pod = trim($_POST['nazwa_podkategorii']);
    if($nazwa_kat != '')
      $this->doIt($nazwa_kat);
    else
      $this->doIt($nazwa_pod);
  }
 
  public function valPrzeniesKat()
  {
    $nazwa_kat = trim($_POST['nazwa_kategorii']);
    $nazwa_pod = trim($_POST['nazwa_podkategorii']);
    if($nazwa_kat == 0 || $nazwa_pod == 0)
      throw new Exception ('Musisz wybrać skąd dokąd ma być przeniesiona kategoria');
    if ($nazwa_kat == $nazwa_pod)
      throw new Exception ('Nie ma sensu przenoszenia kategorii na ją samą');
    $this->check->czyDoPotomka($nazwa_kat,$nazwa_pod);
 
    if($this->check->czyIstniejeKat($nazwa_kat) && $this->check->czyIstniejeKat($nazwa_pod))
    {
      $przenies = new Posuwanie;
      $przenies->MoveKat($nazwa_kat,$nazwa_pod);
      return true;
    }
    throw new Exception ('Nie istnieje podana kategoria');
 
  }
 
  private function doIt($what)
  {
    if($this->check->czyIstniejeKat($what))
    {
      $this->check->usunKategorie($what);
      return true;
    }
    throw new Exception ('Nie ma podanej kategorii');
  }
 
  private function doItAgain($what,$i = 1,$child = '')
  {
    $this->valKat($what);
    switch($i)
    {
      case 1:
          $this->add->MasterKat($what);
        break;
      case 2:
          $this->add->SubKat($what,$child);
        break;
    }
  }
 
  private function valKat($string)
  {
    if($string == '')
      throw new Exception ('Nazwa kategorii nie może być pusta');
    elseif(!mb_check_encoding($string,'UTF-8'))
      throw new Exception ('Należy ustawić kodowanie w przeglądarce na UTF-8');
    elseif(strlen($string) < 3)
      throw new Exception ('Nazwa kategorii nie może być krótsza niż 3 znaki');
    elseif(!preg_match('/[a-zA-Z0-9ĄąĆćĘꣳŃńÓóŚśŹźŻż]/',$string))
      throw new Exception ('Nazwa kategorii może składać się wyłącznie z liter i cyfr');
    else
      return true;
  }
 
  public function valAddKat()
  {
    $nazwa_kat = trim($_POST['nazwa_kategorii']);
    $nazwa_pod = trim($_POST['nazwa_podkategorii']);
    $this->add = new Dodawanie;
    if($nazwa_pod != 0)
    {
      $this->doItAgain($nazwa_kat,2,$nazwa_pod);      
    }
    else
    {
      $this->doItAgain($nazwa_kat);
    }
  }
 
}
?>

Wszystkie powyższe kontrolery zostały oparte o dyspozytora:

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
<?php
/**
 * @li gnu/agpl v3 or leter
 * @code utf8
 * @version 0.1
 * @author Przemysław Czekaj
 * @date 07.07.09
 * @file controlers/dispatcher.php
**/
 
require_once('controlers/dodaj.php');
require_once('controlers/usun.php');
require_once('controlers/przenies.php');
require_once('controlers/tpl.php');
 
final class Dispatcher
{
  private $handle;
  private $tpl;
 
  public function __construct($tepel, $link)
  {
    $this->tpl = $tepel;
    $this->handle = $link;
    $this->handle_the_event();
    $this->display();
  }
 
  public function handle_the_event()
  {
    try // posprawdzajamy co nam tam chce uzytkownik powysyłać
    {
      $name = "Handler".ucfirst($this->handle);
      if ($this->handle != '')
      {
        if (class_exists("$name"))
        {
          $handlerObj = new $name($tpl->handle);
          $handlerObj->handled_event($_GET['co']);
          return true;
        }
        throw new Exception ('Nie można obsłużyć');
      }
    }
    catch(Exception $e) // połapiemy błędy
    {
      $this->tpl->assign('typ_msg','error');
      $this->tpl->assign('wiadomosc',$e->getMessage());
    }
  }
 
  private function display() // poparsujmy troche templatke
  {
    $disTpl = new ShowData($this->tpl);
  }
}
 
?>

Najważniejsze pliki zostały Wam przedstawione, a teraz troche opisu, żeby móc utworzyć drzewo, musimy użyć Rekurencji, została ona użyta w pliku: models/view.php, linia 39. Odwołanie się do samego siebie przez self::, dlaczego po co i jak? Już tłumaczę:
Do stworzenia drzewa użyłem dwóch rzeczy:
1) compiler defun – plugin do smartów – link
2) gotowego przykładu z jquery z budową drzewiastą – link

Jak musi wyglądać tablica w php żeby mogła być przerobiona za pomocą tego pluginu do smartów? Otóż tak:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$tree = array('element'=>array(array('name' => 'test1',
                                     'element' => array(array('name' => 'test1.1'),
                                                        array('name' => 'test1.2',
                                                              'element' => array(array('name' => 'test1.2.1'),
                                                                                 array('name' => 'test1.2.2')
                                                                                )
                                                             ),
                                                        array('name' => 'test1.3',
                                                              'element' => array(array('name' => 'test1.3.1')
                                                                                )
                                                             )
                                                       )
                                    )
                              )
             );

Prawda że nie fajna co? Ni i właśnie za pomocą tej rekurencj którą przedstawiłem powyżej stworzyliśmy taką o to tablice, dzięki której piknie nam wszystko działa.

Teraz jak powinna wyglądać w tpl cała struktura? o tak:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 <ul id="black" class="treeview-black">
 {defun name="testrecursion" list=$tree.element}
 {foreach from=$list item=element}
   {if $element.element}
   <li>
    <span> {$element.name}</span>
    <ul>
      {fun name="testrecursion" list=$element.element}
    </ul>
   </li>
   {else}
    <li> {$element.name}</li>
   {/if}
 {/foreach}
 {/defun}
 </ul>

Dlaczego tak a nie inaczej? Ponieważ użyłem 3 przykładu z struktury drzewiastej w jQuery. Nie wiem co tu jeszcze opisywać, dam Wam do pobrania wszystkie pliki, link poniżej.

Do pobrania całość: link