Na pewno nie raz okazuje się, że na stronie czy w innej aplikacji trzeba umieszczać i zarządzać danymi hierarchicznymi. Jeżeli korzystamy z Doctrine’a to mamy do dyspozycji NestedSet - bardzo przydatne narzędzie

Zaczynamy

Po pierwsze określamy strukturę tabeli dla danych hierarchicznych:

$$ App_Menus: actAs: NestedSet: hasManyRoots: true rootColumnName: parent_id tableName: menus columns: id: type: integer primary: true autoincrement: true name: string(64) type: integer$$

Powyżej mamy definicję tabeli w której może występować wiele drzeni drzewa, a pole określające dane drzewo nazwane zostało parent_id (w dokumentacji Doctrine, używają root_id jednak w moim przypadku z racji zaszłości historychnych wolę parent_id)

Dzięki temu wpisowi orzymujemy takiego SQL’q:

$$ CREATE TABLE `t_menus` ( `id` bigint(20) NOT NULL auto_increment, `name` varchar(64) default NULL, `parent_id` bigint(20) default NULL, `type` bigint(20) default NULL, `lft` int(11) default NULL, `rgt` int(11) default NULL, `level` smallint(6) default NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8$$

Implementacja:

Dobra wszystko fajnie, ale jak to teraz używać?

Nie ma nic prostrzego, zakładam używanie smartów i to wersji 3, zdaje sobie sprawę że Smarty 3 nie doczekały się jeszcze dobrej dokumentacji, ale zawsze jest kod systemu szablonów - można poczytać :)

Po pierwsze wyciągamy dane z bazy:

$$ public function getTreeFromRoot() { $treeObject = Doctrine_Core::getTable('Model_App_Menus')->getTree(); $rootColumnName = $treeObject->getAttribute('rootColumnName'); foreach ($treeObject->fetchRoots() as $root) { $options = array('root_id' => $root->$rootColumnName); return $treeObject->fetchTree($options)->toHierarchy()->toArray(); } }$$

Wynikiem jest tablica wielowymiarowa z zależnościami

Dla ułatwienia sobie wykorzystania tejże tablicy w systemie szablonów deko sobie poczyśćmy wynik, chodzi głównie o to, że tablica z Doctrine’a zawsze zawiera element tablicowy __children nawet jeżeli jest on pusty.

Trywialna funkcja wywala nam puste tablice:

$$ public function flatArray($array) { foreach ($array as $key => $value) { if(is_array($value)) { if(count($value) != 0) $out[$key] = $this->flatArray($value); } else { $out[$key] = $value; } } return $out; }$$

Wynik możemy przekazać do Smartów i wyświetlić za pomocą małej rekurencji:

$$ {function name=menu level=0} {strip} {foreach $data as $fields} {foreach from=$fields item=field key=key} {if $level neq "0"} {if $key eq "id"}{assign var="ids" value=$field}{/if} {if $key eq "name"} {$field} (lorem ipsum...){/if} {if $key eq "level"}{if $fields|@count eq '7'}{/if}{/if} {/if} {if $key eq "__children"} {menu data=$field level=$level+1} {if $level neq "0"}{/if} {/if} {/foreach} {/foreach} {/strip} {/function} {menu data=$childs}$$

Dzięki temu otrzymamy ładne rzewko w liście.

Zarządzanie:

Budujemy ładną aplikację i chcemy mieć drag-n-drop’owe określenie menu, fajnie ale jak?

Najszybciej :)

Ja jestem strasznie leniwy i średnio lubię javascript’a, więc korzystam z gotowców :) Trzeba zassać sobie mały kodzik do drag-n-dropowego zarządzania drzewami:
Tutaj….

Fajnie działa, jednak zwraca mało ciekawy wynik, zobacz stronę demo ;)

Nie ma problemu, za pomocą Ajaxa obsługujemy i to (funkcja jeszcze nie zoptymalizowana, ale działa):

$$ public function sortmenuAction() { $root_node = $_POST['menu_id']; $pola =$_POST['list']; $childs = 0; foreach ($_POST['list'] as $key => $value) { $run=0; if($childs != 0) { $run = 1; $childs--; $parent_pos = $key - 1; while (strstr($pola[$parent_pos],"_") == false){ $parent_pos--; } $tmp = explode("_",$pola[$parent_pos]); $data = $tmp[0]; if (strstr($value,"_") != false) { $tmp = explode("_",$value); $dzieciak = $tmp[0]; } else{ $dzieciak = $value; } } if($run == 0) { if (strstr($value,"_") != false) { $tmp = explode("_",$value); $data = $tmp[0]; $childs = $tmp[1]; $dzieciak = $data; $data = $root_node; } else { $dzieciak = $value; $data = $root_node; } } $rootMenu = Doctrine_Core::getTable('Model_App_Menus')->findOneById($data); $childMenu = Doctrine_Core::getTable('Model_App_Menus')->findOneById($dzieciak); $childMenu->getNode()->moveAsLastChildOf($rootMenu); } echo "Done"; die(); }$$

Wsio - działa, sortowanie Ajax’em i wyświetlanie - ogólnie problem drzewa załatwiony w 15 minut :)