Doctrine + NestedSet + Ajax + Smarty + ZF

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}
  <ul class="{if $level eq "0"}sortable{/if}">
    {foreach from=$fields item=field key=key}
      {if $level neq "0"}
        {if $key eq "id"}<li id="list_{$field}">{assign var="ids" value=$field}{/if}
        {if $key eq "name"}<div class="ekgreybox">{$field}  (lorem ipsum...)</div>{/if}
        {if $key eq "level"}{if $fields|@count eq '7'}</li>{/if}{/if}
      {/if}
      {if $key eq "__children"}
        {menu data=$field level=$level+1}
        {if $level neq "0"}</li>{/if}
      {/if}
    {/foreach}
    </ul>
  {/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 :)