eRIZ’s weblog

PHP, webdesign, Linux, Windows i inne, bo nie samym chlebem człowiek żyje
Serdecznie zapraszam do udziału w ANKIECIE

PHP feat. MySQL: Sposób na drzewka

aktualizacja

W Sieci można natknąć się na wiele przepisów, w jakis sposób stworzyć drzewka zapisywane w DB. Niektóre mają więcej, niektóre mniej wad… Ale poszukiwałem jakiegoś w miarę uniwersalnego rozwiązania, które by najlepiej pasowało do moich potrzeb.

Założenia

Potrzebujemy drzewka kategorii. Ot, takie najprostsze, np:

  • kategoria
    • podkategoria1
    • podkategoria2
  • kategoria

Ponieważ zachodziłem w głowę, co by tu można było zrobić, budowanie drzewka zachodziło bez problemów, kod się walidował, postanowiłem - na dzień dobry - poćwiczyć na tablicach i metodą notatek/prób i błędów dojść do rozwiązania.

Przygotowania

Bazowałem na poniższej tablicy:

  1. $d[] = array('asdasd1-', 1);
  2. $d[] = array('asdasd2-', 1);
  3. $d[] = array('asdasd3-', 2);
  4. $d[] = array('asdasd4-', 2);
  5. $d[] = array('asdasd5-', 1);
  6. $d[] = array('asdasd6-', 2);
  7. $d[] = array('asdasd7-', 2);
  8. $d[] = array('asdasd8-', 1);
  9. $d[] = array('asdasd9-', 2);
  10. $d[] = array('asdasd10-', 1);

Pierwsze pole w tablicy każdego rekordu, to nazwa, a drugie - stopień zagłębienia w strukturze. O tym później.

Początkowo, próbowałem obrabiać to za pomocą zwykłej pętli for. Errr, nie. Dlaczego? O ile w przypadku tablic nie będzie raczej problemów wydajnościowych przy liczeniu elementów tablicy, to podczas korzystania z bazy już mogą się pojawić takie problemy. Wiem, że rzadko mogą się zdarzyć takie duuuuże drzewa, ale… Każda ms jest ważna. ;]

Więc co zrobić? Pozostaje już tylko pętla while.

Po co więc nam drugi parametr w tablicy? Otóż - jak już wcześniej wspomniałem - posłuży on nam do określenia, jak głęboko w hierarchicznej strukturze drzewa znajduje się dany wpis. Stąd - aby poprawnie rozpoczynać i kończyć listy wyliczeniowe - musimy sprawdzać, czy następny element jest większy bądź mniejszy od obecnie przetwarzanego. Upraszczając, jeśli liczba jest większa od bieżącej - rozpocznij nową podkategorię, jeśli mniejsza - zamknij podkategorię, jeśli taka sama - wypisz element.

W przypadku pętli for tego problemu za bardzo nie ma, ponieważ możemy użyć konstrukcji $d[$a+1], aby dostać się do następnego elementu. Przy while musimy sobie pomóc.

Po prostu, będziemy pobierać dwa rekordy za jednym razem.

…i praktyka.

Aby nie pobierać kilkukrotnie tego samego rekordu, utworzymy sobie dwa bufory.

  1. $start = true;
  2. $buff1 = null;
  3. $buff2 = null;

Z każdym przejściem pętli będą się one przesuwały cyklicznie o jeden (z drugiego do pierwszego; do pierwszego zapisany nowy element).

Początek pętli wygląda mniej więcej tak:

  1. while(list($key,$r) = each($d)){
  2.  
  3.     if($start){
  4.         $buff1 = $r;
  5.         $buff2 = $d[1];next($d);
  6.         $start = false;
  7.     }else{
  8.         $buff1 = $buff2;
  9.         $buff2 = $r;
  10.     }

Zmienną pomocniczą jest tu $start, która służy do sprawdzenia, czy jest to pierwszy przebieg pętli. Umożliwia to prawidłową wymianę danych pomiędzy buforami.

W celu zwiększenia przejrzystości kodu (dla celów tego wpisu!), tworzymy referencje do odpowiednich elementów tablicy, które zawierają dane o stopniu zagłębienia.

  1. $curr = &$buff1[1];
  2. $next = &$buff2[1];

I teraz obrabiamy poszczególne elementy.

  1. //rozpocznij element i wypisz wartość
  2. echo '<li>'.$buff1[0];
  3.  
  4. //jeśli istnieje następny element...
  5. if($next!==null){
  6.     //jeśli następny element jest głębiej, utwórz nową listę
  7.     if($next>$curr){
  8.         echo '<ul>';
  9.     }
  10.  
  11.     //jeśli następny element jest płycej, kończymy listę...
  12.     if($next<$curr){
  13.         //...odpowiednią ilość razy - przeliczanie w przypadku bardziej zagłębionych list
  14.         for($x=0;$x<($curr-$next);$x++){
  15.             echo '</li></ul>';
  16.         }
  17.     }
  18.  
  19.     //jeśli następny element jest na takiej samej głębokości, uzupełnij element listy
  20.     if($next==$curr){
  21.         echo '</li>';
  22.     }
  23.  
  24. }else{
  25.     //jeśli to ostatni...
  26.     echo '</li>';
  27. }

Oczywiście, nie zapomnij o ujęciu całego kodu w znaczniki <ul></ul>.

MySQL

W kodzie za bardzo się różnić nie będzie w porównaniu to edycji tablicowej. Wystarczy edycja, nazwijmy to “nagłówka pętli”.

  1. while($r = $q->getRow()){
  2.  
  3.     if($start){
  4.         $buff1 = $r;
  5.         $buff2 = $q->getRow();
  6.         $start = false;
  7.     }else{
  8.         $buff1 = $buff2;
  9.         $buff2 = $r;
  10.     }

Oczywiście, funkcję $q->getRow() zamień na odpowiednią, używaną w Twoim skrypcie. Konieczna będzie również zmiana odwołań do poszczególnych tablic ($d[0], itp. zamień na odpowiadające Twojej tabeli w bazie).

Sama tabela może wyglądać mniej więcej tak:

  • ID unsigned INT - dla potrzeb zarządzania
  • depth unsigned INT - głębokość w hierarchii
  • title char(255) - tytuł
  • page unsigned INT - ewentualne odniesienie do podstrony w serwisie

Dlaczego by nie sprzężyć tej tabeli z zawartością stron? To temat, który został już rozklepany.

O czym powinniśmy pamiętać?

Używaj kolejno następujacych po sobie poziomów zagłębienia. Np. jeśli nadrzędnym jest 1, to następny powinien być 2, a nie wyższy. Dlaczego? Raz: aby uniknąć chaosu, dwa: wówczas wymagałoby to użycia kolejnej pętli w kodzie (analogicznie jak to ma miejsce w przypadku </ul></li>).

Aktualizacja

Powyższy kod działa, OK. Ale ucina ostatni element… Gdyby liczba wpisów w tablicy/tabeli była nieparzysta, to wszystko by pięknie działało. Jest parzysta, ups - ucięło… Posiedziałem przy serniku i herbacie, udało mi się z tym poradzić.

Przyda się drugi rodzaj pętli while, ten rzadziej wykorzystywany. Cała pętla wygląda tak (reszta bez zmian):

  1. do{
  2.     list($key,$r) = each($d);
  3.  
  4.     if($start){
  5.         $buff1 = $r;
  6.         $buff2 = $d[1];next($d);
  7.         $start = false;
  8.     }else{
  9.         $buff1 = $buff2;
  10.         $buff2 = $r;
  11.     }
  12.  
  13.     if($buff1==null){
  14.         break;
  15.     }
  16.  
  17.     $curr = $buff1[1];
  18.     $next = $buff2[1];
  19.  
  20.     echo '<li>'.$buff1[0];
  21.  
  22.     if($next!==null){
  23.         if($next>$curr){
  24.             echo '<ul>';
  25.         }
  26.  
  27.         if($next<$curr){
  28.             for($x=0;$x<($curr-$next);$x++){
  29.                 echo '</li></ul>';
  30.             }
  31.         }
  32.  
  33.         if($next==$curr){
  34.             echo '</li>';
  35.         }
  36.  
  37.     }else{
  38.         echo '</li>';
  39.     }
  40.  
  41.  
  42. }while($buff1!=null);

EOF;

5 komentarzy

dopisz swój :: trackback :: RSS z komentarzami

RSS z komentarzami :: trackback

Skomentuj

Możesz używać znaczników XHTML. Dozwolone są następujące tagi: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>

Wszystkie komentarze przechodzą przez moderację oraz filtry antyspamowe. Nie zostanie opublikowany komentarz, jeśli:

  • Jego treść obraża kogokolwiek.
  • W treści znajdują się wulgaryzmy i słownictwo ogólnie uznane za nieprzyzwoite.
  • Mam wątpliwości co do autora wpisu (Wszelkie anonimy są kasowane - niezależnie od zawartości - wpisz prawdziwy e-mail. Jeśli usunąłem, Twoim zdaniem, komentarz niesłusznie - daj znać). Zdarza się, iż sprawdzam kim jest komentujący.
  • Zawiera jakąkolwiek formę reklamy.

Szufladka