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 :)