[PHP] Recursieve functie loopt niet hele array door

Pagina: 1
Acties:
  • 109 views sinds 30-01-2008
  • Reageer

Onderwerpen


Acties:
  • 0 Henk 'm!

  • Reveller
  • Registratie: Augustus 2002
  • Laatst online: 05-12-2022
Ik heb de volgende array en functies om een uitklappend navigatie menu te maken:
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
58
59
60
61
$nodes[1]  = array('pid'=>0, 'title'=>'Home');
$nodes[2]  = array('pid'=>1, 'title'=>'Groenten');
$nodes[3]  = array('pid'=>1, 'title'=>'Fruit');
$nodes[4]  = array('pid'=>1, 'title'=>'Vlees');
$nodes[5]  = array('pid'=>2, 'title'=>'Zomer');
$nodes[6]  = array('pid'=>2, 'title'=>'Winter');
$nodes[7]  = array('pid'=>3, 'title'=>'Rood');
$nodes[8]  = array('pid'=>3, 'title'=>'Geel');
$nodes[9]  = array('pid'=>7, 'title'=>'Appel');
$nodes[10] = array('pid'=>7, 'title'=>'Kers');
$nodes[11] = array('pid'=>7, 'title'=>'Aardbei');
$nodes[12] = array('pid'=>8, 'title'=>'Banaan');
$nodes[13] = array('pid'=>1, 'title'=>'Drinken');

/**
 * Get all node id's from current node to rootnode
 */
function path_to_top($activeItem)
{
  global $nodes;
  
  $unfoldItems = array($activeItem); 
  while ($unfoldItems[count($unfoldItems)-1] != 0)
    $unfoldItems[] = $nodes[$unfoldItems[count($unfoldItems)-1]]['pid'];

  return $unfoldItems;
}

/**
 * Build navigation menu
 */
function buildMenu($activeItem, $curItem=1) 
{
  global $nodes;

  $unfoldItems = path_to_top($activeItem);
  $first       = true; 

  foreach ($nodes as $itemKey => $item)
  {
    if ($item['pid'] == $curItem)
    {
      if ($first) 
      {
        $output.= '<ul>';
        $first = false;
      } 
      
      $output.= '<li>'.$item['title'];

      if (in_array($itemKey, $unfoldItems))
        $output.= buildMenu($activeItem, $itemKey);

      $output.= '</li>';
    } 
  } 
     
  if (!$first) $output.= '</ul>';
  
  return $output;
}

Als ik buildMenu(12) aanroep, verwacht ik de volgende output:
code:
1
2
3
4
5
6
7
Groenten
Fruit
  Rood
  Geel
    Banaan
Vlees
Drinken

Maar ik krijg:
code:
1
2
3
4
5
Groenten
Fruit
  Rood
  Geel
    Banaan

Met andere woorden - de functie doorloopt het pad Fruit > Geel > Banaan en stopt er dan mee. De functie komt niet eens toe aan het door de loop halen van Vlees en Drinken - de nodes die nog eronder moeten. Ik zit nu al uren te turen om menuBuild, maar snap niet waarom hij niet gewoon de hele array doorloopt. Wie ziet de voud wel en helpt mij op weg?

"Real software engineers work from 9 to 5, because that is the way the job is described in the formal spec. Working late would feel like using an undocumented external procedure."


Acties:
  • 0 Henk 'm!

  • mocean
  • Registratie: November 2000
  • Laatst online: 04-09 10:34
Een 'Recursieve functie' werkt alleen als ie 'ergens' zichcelf aanroept. En dat zie ik niet in je code. Kijk daar eens naar!

Ik moet beter kijken :-)

[ Voor 21% gewijzigd door mocean op 24-01-2005 22:19 ]

Koop of verkoop je webshop: ecquisition.com


Acties:
  • 0 Henk 'm!

Verwijderd

Ga eens gewoon debuggen. Leuk dat je zometeen een CMS hebt, maar ik zie liever dat je zelf meer tijd besteedt aan zulke dingen. Hoort er ook gewoon bij.

Maar ik zal je een tip geven: globals kunnen heel evil zijn.

Maak er een class van met de nodes array als property en de 2 functies als methoden en het werkt gewoon, of geef de nodes array mee als argument, dat werkt ook prima.

Acties:
  • 0 Henk 'm!

Verwijderd

Wat Cheatah zegt: `foreach' in combinatie met een global rammelt. Waarschijnlijk vanwege de implementatie ervan in de PHP interpreter; de iteratiestatus over de array wordt blijkbaar bijgehouden in de variabele in plaats van op de stack.

Het volgende script illustreert het probleem en de oplossing (de array meekopiëren op de stack):
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
<?
  $thelist = array(1, 2, 3, 4, 5, 6);

  function recurseList($depth=3, $offset=0) {
    global $thelist;

    foreach ($thelist as $listitem) {
      echo sprintf("%s%s<br>", str_repeat("&nbsp;", $offset), $listitem);
      if ($depth > 1)
        recurseList($depth-1, $offset + 2);
    }
  }

  recurseList();

  echo "<br><br>";

  function recurseList2($thelist, $depth=3, $offset=0) {
    foreach ($thelist as $listitem) {
      echo sprintf("%s%s<br>", str_repeat("&nbsp;", $offset), $listitem);
      if ($depth > 1)
        recurseList2($thelist, $depth-1, $offset + 2);
    }
  }

  recurseList2($thelist);
?>


Daar kon je op zich niet veel aan doen zonder het te weten, want het is tamelijk obscuur gedrag.
edit:
Het staat overigens wel op php.net; zie het tweede commentaar op deze pagina.


Maar als een andere opmerking over je algoritme heb ik dat het niet erg efficiënt is. Het is namelijk van de orde O(N^M), met N = het aantal menuitems en M = de diepte van het diepste menuitem. Dit komt omdat je nu elke keer door de hele lijst aan het zoeken bent naar kinderen.

Dit kun je verminderen tot een O(N) algoritme door een andere datastructuur te pakken, namelijk een boom in plaats van een tabel. Je moet alleen wel eerst die boom hebben.

Als je menudata uit een relationele database moet komen, zul je er dus eerst een vertaalslag overheen moeten halen om tot die boomstructuur te komen. Als je je menudata in een PHP file hebt staan zet kun je de boomstructuur direct met geneste arrays inprogrammeren.

Een tussenoplossing zou kunnen zijn om een lookup table toe te voegen die bijhoudt welke kinderen een bepaald menuitem allemaal heeft. Deze tabel vul je éénmaal op basis van de relationele tabel, en kun je daarna gebruiken om snel de kinderen van een item te bepalen:

Hier een voorbeeld van het maken en gebruik van zo'n tabel. De tabel mapt menuitem id's naar een lijst (array) van kinderen van dat menuitem.

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
/* Create lookup table */
$childTable = array();

foreach ($nodes as $id=>$node) {
  $parentid = $node["pid"];
  
  if (isset($childTable[$parentid]))
    $childTable[$parentid][] = $id;
  else
    $childTable[$parentid] = array($id);
}

/* Menu building function */
function buildMenu($activeItem, $parentItem=1) {
  global $nodes, $childTable;
  
  $unfoldItems = path_to_top($activeItem); // Niet zo handig, zie verderop
  
  if (isset($childTable[$parentItem])) { // Controleer of deze parent wel kinderen heeft  
    $childcode = "";
  
    // "Teken" alle kinderen
    foreach ($childTable[$parentItem] as $childId) {
      $child = $nodes[$childId];

      // Als deze op het pad naar de top ligt gaan we dieper met de kinderen
      if (in_array($childId, $unfoldItems))
        $children = buildMenu($activeItem, $childId);
      else
        $children = "";

      $childcode .= sprintf("<li>%s%s</li>", $child["title"], $children);
    }
    
    return sprintf("<ul>%s</ul>", $childcode);
  }
  else return "";  
}


Deze code gebruikt nog steeds globals: niet de netste oplossing, maar omdat er in de recursieve functie niet meer over de array geloopt wordt heeft het geen last van hetzelfde probleem als je oorspronkelijke code.

En nog een losse tip: als je een recursieve functie hebt die standaardparameters hoort te krijgten bij de eerste aanroep, maak daar dan niet optionele argumenten van. Het is netter om een aparte functie te maken die de recursieve functie aanroep met de standaardparameters. Dus niet:

PHP:
1
function buildMenu($activeItem, $curItem=1) { ... }

Maar:
PHP:
1
2
3
function buildMenu($activeItem) { ... buildSubMenu($activeItem, 1); ... }

function buildSubMenu($activeItem, $curItem) { ... }


Ik heb een paar keer goed moeten lezen voor ik doorhad waar de parameters voor waren, en dat is over het algemeen geen goed teken voor de leesbaarheid van je code. Bovendien: $activeItem verandert eigenlijk nooit. Dus verandert path_to_top ook niet; je kunt het opzoeken van dit pad dus mooi in de "beginfunctie" doen, en het daarna klaar voor gebruik doorgeven aan de recursieve. Dat scheelt je weer een berekening per aanroep.

[ Voor 18% gewijzigd door Verwijderd op 25-01-2005 00:13 ]


Acties:
  • 0 Henk 'm!

  • Reveller
  • Registratie: Augustus 2002
  • Laatst online: 05-12-2022
Na de reaktie van OneOfBorg gelezen te hebben (dank je!), heb ik nagedacht over de manier waarop de $nodes array gestructureerd moet worden om deze zo efficient mogelijk te kunnen doorlopen. Omdat ik de nodes uit een database haal, kan ik de opbouw van de array makkelijk aanpassen:
PHP:
1
2
3
4
5
6
7
8
9
10
11
12
function nodes_set()
{
  $result = mysql_query("SELECT * FROM nodes ORDER BY pid, weight ASC");

  while ($row = mysql_fetch_object($result))
    $nodes[$row->pid][] = array($row->nid, $row->title); 

  // was in de topicstart nog:
  // $nodes[$node['nid']] = Array('pid' => $node['pid'], 'title' => $node['title']);

  return $nodes;
}

Ik heb nodes_set() aangepast aan de hand van wat OneOfBorg voorstelde:
Maar als een andere opmerking over je algoritme heb ik dat het niet erg efficiënt is. Het is namelijk van de orde O(N^M) [...] Dit kun je verminderen tot een O(N) algoritme door een andere datastructuur te pakken, namelijk een boom in plaats van een tabel.
OneOfBorg sprak van een tussenoplossing om twee verschillend gestructureerde array's (zie functie hierboven) naast elkaar te gebruiken, maar liever pas ik de methode dan meteen goed toe. Ik zie alleen niet hoe ik nu zijn functie moet aanpassen zodat bij volledig op de nieuwe $nodes-array draait. Tot nu toe heb ik deze:
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
function path_to_top($activeItem)
{
  global $nodes;
  
  $unfoldItems = array($activeItem); 
  while ($unfoldItems[count($unfoldItems)-1] != 0)
    $unfoldItems[] = $nodes[$unfoldItems[count($unfoldItems)-1]]['pid'];

  return $unfoldItems;
}

function build_menu($active_node)
{
  $unfold_nodes = path_to_top($active_node);
  return _build_menu($active_node, $unfold_nodes, 1);
}

function _build_menu($active_node, $unfold_nodes, $current_node)
{
  global $nodes, $childTable;
  
  if (isset($childTable[$current_node]))
  {
    $childcode = "";
  
    foreach ($childTable[$current_node] as $childId)
    {
      $child = $nodes[$childId];

      if (in_array($childId, $unfold_nodes))
        $children = _build_menu($active_node, $unfold_nodes, $childId);
      else
        $children = "";

      $childcode .= sprintf("<li>%s%s</li>", $child["title"], $children);
    }
    
    return sprintf("<ul>%s</ul>", $childcode);
  }
  else return "";  
}

Ik probeer aan de hand van zijn aanwijzingen mijn script aan te passen, maar loop vast bij het herschrijven van path_to_top(), zoadat deze werkt met de nieuwe $nodes array. Pas dan kan ik verder met het herschrijven van _build_menu. Wie helpt mij op weg? En is deze array in alle gevallen (bijvoorbeeld het maken van een cookie crumb trail) efficienter of is de beste optie toch om beide array's naast elkaar te draaien?

"Real software engineers work from 9 to 5, because that is the way the job is described in the formal spec. Working late would feel like using an undocumented external procedure."


Acties:
  • 0 Henk 'm!

Verwijderd

Euhm, waarom recursief?
Als je nu gewoon iets maakt dat hij 'pid'-aantal spatied print en dan een streep.bolletje dan krijg je toch ook dit effect?

Of je maakt de functie zodanig dat hij de vorige 'pid' pakt en daar de huidige vanaf trekt.
Heb je 2 dan zet hij er </ul></ul>, heb je -2 dan zet hij er <ul><ul>

Veel plezier overigens.

Acties:
  • 0 Henk 'm!

Verwijderd

Op zich zou het zo al moeten werken, toch?

De path_to_top functie kan op de "oude" array bijven werken, en bij het renderen kan de "lookup" array gebruikt worden.

Kleine opmerking: $active_node is een inerte parameter van de functie _build_menu geworden. Hij wordt namelijk nergens gebruikt (behalve dan doorgegeven in de recursieve aanroep). Dus kun je hem net zo goed weglaten :).

Je kunt natuurlijk ook direct een boom maken van de gegevens uit de database. De eerste stap is dan een structuur te bedenken voor de boom. In PHP zal het ding uit geneste arrays komen te bestaan. Ik stel het volgende voor (in een klein pseudotaaltje dat ik net bedacht heb):

code:
1
2
3
4
5
6
7
8
NODELIST := LIST of NODERECORD

NODERECORD := RECORD (
  id : int
  title : string
  children : NODELIST
  parent : ref to NODERECORD 
)


Opmerking hierbij: in PHP worden de abstracte datatypes `LIST's en `RECORD's beide geïmplementeerd met arrays, dus je loopt wel het risico dat je straks door de arrays het datatype niet meer ziet (haha O-)). Maar dat wordt gewoon een beetje opletten. En de referentie naar de ouder zit er in om het bepalen van het wortelpad straks makkelijker te maken.

De functie die de relationele data kan vertalen in deze boomstructuur ziet er als volgt uit. Het gebruikt wederom een indextabel om de knopen in de boom snel terug te kunnen vinden.

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
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
function getNodesAsTree($activeItem) {
  /**
   * Preconditie: dit algoritme werkt ALLEEN als alle kinderen een
   * hogere ID hebben dan hun ouder.
   *
   * Bovendien bepaalt geeft het meteen de knoop met het gewenste id
   * terug, omdat in binnen deze functie gebruik gemaakt kan worden
   * van de lookup tabel, en daarbuiten niet meer.
   *
   * De functie geeft een associatieve array terug met de 
   * velden "tree" en "activeitem".
   */
   
  $result = mysql_query("SELECT * FROM nodes ORDER BY pid ASC");
  
  $tree = array();
  $indexTbl = array();
  
  while ($node = mysql_fetch_object($result)) {
    // Controleer eerst of de parent al in de boom zit; zo niet,
    // dan hoort dit item de root te zijn (pid = 0). Als het inderdaad
    // de root is, dan voegen we die gewoon toe.
    if (!isset($indexTbl[$node->pid])) {
        
      if ($node->pid != 0) trigger_error("FOUT! Boomstructuur voldoet niet aan preconditie", E_USER_ERROR);       
      else {
        // Maak een nieuw record aan en voeg het "by-reference" in in beide arrays
        // We moeten hier een beetje goochelen met variabelelocaties omdat referenties
        // in PHP ook niet al te fris werken.
        $indexTbl[$node->nid] = array(
          "id" => $node->nid,
          "title" => $node->title,
          "parent" => null,
          "children" => array() // Nog geen kinderen
        );
        $tree[] =& $indexTbl[$node->nid];
      }
        
    }
    else {
      // De ouder zit al in de boom; haal hem op
      $parent =& $indexTbl[$node->pid];
      
      // Maak een nieuw record aan voor dit kind en voeg toe aan de lijst van kinderen
      // van de parent
      $indexTbl[$node->nid] = array(
        "id" => $node->nid,
        "title" => $node->title,
        "parent" => &$indexTbl[$node->pid],
        "children" => array() // Nog geen kinderen
      );
      $parent["children"][] =& $indexTbl[$node->nid];      
    }
  }
  
  return array("tree" => &$tree, "activeitem" => &$indexTbl[$activeItem]);
}

function getRootPath(&$node) {
  if (is_null($node))
    return array();
  else
    return array_merge(array($node["id"]), getRootPath($node["parent"]));
}

function buildMenu(&$nodeTree, &$activeNode) {
  $rootpath = getRootPath($activeNode);
  
  return _buildMenu(&$nodeTree, &$rootpath, $nodeTree[0]);
}

function _buildMenu(&$nodeTree, &$rootPath, &$currentNode) {
  /* We tekenen hier maar weer de _kinderen_ van currentNode.
     Dit betekent wel dat de root niet getekend wordt (`Home', in
     jouw geval), maar in je oorspronkelijke algoritme was dat ook
     niet dus had heb ik maar zo gehouden. */
  
  $childcode = "";
  foreach ($currentNode["children"] as $key=>$child) {
    $child =& $currentNode["children"][$key];
    if (in_array($child["id"], $rootPath))
      $children = _buildMenu($nodeTree, $rootPath, $child);
    else
      $children = "";
      
    $childcode .= sprintf("<li>%s%s</li>", $child["title"], $children);
  }
  
  if ($childcode != "")
    return sprintf("<ul>%s</ul>", $childcode);
  else
    return "";
}

/**
 * Te gebruiken met:
 */
$setup = getNodesAsTree(12);
echo buildMenu($setup["tree"], $setup["activeitem"]);


N.B: Ik heb deze code uit mijn hoofd getypt en ik kan hem waar ik nu ben niet testen. Ik zal er vanavond nog eens naar kijken om te zien of hij ook daadwerkelijk klopt.

Maar bij nader inzien vind ik deze methode toch niet zoveel toevoegen (behalve dan verwarring en gegoochel). Ik zou het denk ik bij een platte tabel houden, en een lookup-table voor de recursieve structuur. Ik zie het nu zelf ook wat helderder, en in principe wàs dat al het abstracte datatype `boom', alleen was het in de implementatie verdeeld over twee tabellen. Nu zit het in een recursieve lijst maar fijner werkt dat niet. Eerder minder fijn, omdat je geen directe lookups van items meer kan doen, en dat kon in de oplossing met twee tabellen wel.

[ Voor 53% gewijzigd door Verwijderd op 25-01-2005 20:27 ]


Acties:
  • 0 Henk 'm!

  • Reveller
  • Registratie: Augustus 2002
  • Laatst online: 05-12-2022
OneOfBorg, dank je wel voor weer een uitgebreide reaktie. Ik ben benieuwd naar de follow-up, maar deel je twijfels over deze methode. Misschien is wat extra uitleg op zijn plaats. Uiteindelijk wil ik een set van functies schrijven die, afhankelijk van wat de gebruiker wil, een bepaalde navigatie bouwen:
  • een cookie trail:
    code:
    1
    
    Home > Fruit > Geel > Banaan
  • een uitklapbare verticale navigatie (waar we hier mee bezig zijn):
    code:
    1
    2
    3
    4
    5
    6
    7
    
    Groenten
    Fruit
      Geel
        Banaan
      Rood
    Vlees
    Drinken
  • een html tabel met "tabjes" voor elk van de topcategorien (Groenten, Vlees, Fruit, Drinken)
    code:
    1
    2
    3
    
    +----------+ +-------+
    | Groenten | | Fruit |
    +          +--------------------------
  • een tabel met tabjes en een subnavigatie:
    code:
    1
    2
    3
    4
    5
    
    +----------+ +-------+
    | Groenten | | Fruit |
    +          +--------------------------+
    | Zomer | Winter                      |
    +-------------------------------------+
  • een linker navigatie met alleen de kinderen van de huidige node (zoals op de site van de EUR)
En eventueel nog andere vormen waar ik later op zou komen. In ieder geval wil ik een functie maken voor elke vorm van navigatie, en zoek dus een array die in alle gevallen makkelijk gebruikt kan worden om de goede output te genereren. Wellicht is het in dat geval het beste om inderdaad 2 array's te houden - de originele uit de TS en dan jouw childTable?

"Real software engineers work from 9 to 5, because that is the way the job is described in the formal spec. Working late would feel like using an undocumented external procedure."


Acties:
  • 0 Henk 'm!

  • Yo-han
  • Registratie: December 2001
  • Laatst online: 18-08 20:16

Yo-han

nope.

Probeer de functie array_walk_recursive eens! Zou moeten werken als ik het probleem goed inschat...

Acties:
  • 0 Henk 'm!

Verwijderd

Reveller schreef op dinsdag 25 januari 2005 @ 18:50:
OneOfBorg, dank je wel voor weer een uitgebreide reaktie. Ik ben benieuwd naar de follow-up, maar deel je twijfels over deze methode.
De code was correct, afgezien van een typfout en een probleem met het verkijgen van referenties in PHP (waar ik al over gezegd heb dat het niet fris is). Regels 47 en 79/80 heb ik aangepast in mijn vorige post.
En eventueel nog andere vormen waar ik later op zou komen. In ieder geval wil ik een functie maken voor elke vorm van navigatie, en zoek dus een array die in alle gevallen makkelijk gebruikt kan worden om de goede output te genereren. Wellicht is het in dat geval het beste om inderdaad 2 array's te houden - de originele uit de TS en dan jouw childTable?
Ik denk dat je het beste een klasse hiervoor kan maken. Die klasse representeert dan een recursieve menustructuur, met alle interfacemethodes die je zou willen erop en eraan. Hoe de implementatie daarvan dan uiteindelijk is, hoef je je niet zwaar mee bezig te houden, zolang hij maar voldoet aan de specificatie. Je kunt dan kiezen voor de twee-tabellen methode, of voor de alles-in-één-boom methode, of een hele andere. En zolang je de interface hetzelfde houdt, kun je zelfs nog wisselen tussen de implementaties als je wilt. Je kunt dan gewoon kiezen voor wat het fijnste en snelste werkt. Een lookup table is in elk geval niet te versmaden, heb ik gemerkt[1].

Daarna kun je een andere klasse maken voor elke menuvorm die je wilt maken. Die maakt dan gebruik van de eerste klasse om de menustructuur op te halen, en kan hem daarna visualiseren zoals hij dat zelf wil. Het voordeel is separation of concerns: de visualisatiecode hoeft niets te weten van de representatie, opslag of bron van de menugegevens, en de structuurcode hoeft niets te weten van de uiteindelijke visualisatie.


[1] Die lookup tables zijn er in twee smaken: in mijn eerste voorbeeld werd er een lookup table gebruikt om op te zoeken welke kinderen een bepaald menuitem had. In mijn tweede voorbeeld werd een lookup table gebruikt om snel bij een bepaalde knoop in de boom uit te kunnen komen. Verder kun je een lookup table en een boom ook min of meer combineren, door de knopen in een lijst te zetten die geïndexeerd wordt met hun id, en records bevat met referenties naar andere records. Dat is min of meer wat gedaan wordt in mijn functie getNodesAsTree.

Er is eigenlijk zoveel keus dat het lastig is om te zeggen wat het beste is. Ik denk dat het beste wat je kan doen is, op een rijtje zetten welke operaties je uiteindelijk allemaal op je boomstructuur wilt gaan uitvoeren, en welke implementatie die operaties het beste/snelste ondersteunt.

[ Voor 22% gewijzigd door Verwijderd op 25-01-2005 20:45 ]


Acties:
  • 0 Henk 'm!

  • Reveller
  • Registratie: Augustus 2002
  • Laatst online: 05-12-2022
Schrik niet van de hoeveelheid code :) Die staat er slechts om een compleet overzicht te geven. Mijn vraag is klein en waarschijnlijk eenvoudig voor iemand die eenzelfde probleempje ooit gehad heeft. Met onderstaande code wil ik het mogelijk maken een tabel met tabjes (en subnav) te outputten. De vraag heeft te maken met regel 53 en staat onder de code:
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
58
59
60
61
62
63
64
65
66
67
68
69
70
$nodes[1]  = array('pid'=>0, 'title'=>'Home'    , 'level' => 0);
$nodes[2]  = array('pid'=>1, 'title'=>'Groenten', 'level' => 1);
$nodes[3]  = array('pid'=>1, 'title'=>'Fruit'   , 'level' => 1);
$nodes[4]  = array('pid'=>1, 'title'=>'Vlees'   , 'level' => 1);
$nodes[5]  = array('pid'=>2, 'title'=>'Zomer'   , 'level' => 2);
$nodes[6]  = array('pid'=>2, 'title'=>'Winter'  , 'level' => 2);
$nodes[7]  = array('pid'=>3, 'title'=>'Rood'    , 'level' => 2);
$nodes[8]  = array('pid'=>3, 'title'=>'Geel'    , 'level' => 2);
$nodes[9]  = array('pid'=>7, 'title'=>'Appel'   , 'level' => 3);
$nodes[10] = array('pid'=>7, 'title'=>'Kers'    , 'level' => 3);
$nodes[11] = array('pid'=>7, 'title'=>'Aardbei' , 'level' => 3);
$nodes[12] = array('pid'=>8, 'title'=>'Banaan'  , 'level' => 3);
$nodes[13] = array('pid'=>1, 'title'=>'Drinken' , 'level' => 1); 

function path_to_top($activeItem)
{
  global $nodes;
  
  $unfoldItems = array($activeItem); 

  while ($unfoldItems[count($unfoldItems)-1] != 0)
    $unfoldItems[] = $nodes[$unfoldItems[count($unfoldItems)-1]]['pid'];

  return $unfoldItems;
}

function build_tabs($active_node)
{
  $unfold_nodes = path_to_top($active_node);
  return _build_tabs($active_node, $unfold_nodes);
}

function _build_tabs($active_node, $unfold_nodes)
{
  global $nodes;

  foreach ($nodes as $itemkey => $node)
  {
    if ($node['level'] == 1)
    {
      if (in_array($itemkey, $unfold_nodes))
      {
        $toprow[] = '<td>! '.$node['title'].'</td>';
      }
      else
      {
        $toprow[] = '<td>'.$node['title'].'</td>';
      }
    }
    
    if ($node['level'] == 2)
    {
      if (in_array($itemkey, $unfold_nodes))
      {
        $subrow[] = '<td>! '.$node['title'].'</td>';
      }
      else
      {
        $subrow[] = '<td>'.$node['title'].'</td>';
      }    
    }
  }

  $tabjes = '<table border="1"><tr>'.implode('', $toprow).'</tr></table>';
  $subnav = '<table border="1"><tr>'.implode('', $subrow).'</tr></table>';
  
  return '<table border="1"><tr><td>'.$tabjes.'</td></tr><tr><td>'.$subnav.'</td></tr></table>';
}

build_tabs(12);

de array $unfold_nodes ziet er als volgt uit:
code:
1
2
3
4
5
6
7
8
Array
(
    [0] => 12
    [1] => 8
    [2] => 3
    [3] => 1
    [4] => 0
)

De tabel die gegenereerd wordt ziet er als volgt uit:
Afbeeldingslocatie: http://www.danandan.luna.nl/tabjes.gif
Mijn vraag is waarom "Zomer" (id 5) en "Winter" (id 6) hier ook in staan. Ik controleer expliciet of de itemkey in de array $unfold_items voorkomt, en ik zie in die array nergens 5 of 6 staan. Ik zit er nu al een lange tijd op te turen, maar zie niet waar het fout gaat. Wie ziet het wel?

[ Voor 24% gewijzigd door Reveller op 26-01-2005 22:21 ]

"Real software engineers work from 9 to 5, because that is the way the job is described in the formal spec. Working late would feel like using an undocumented external procedure."


Acties:
  • 0 Henk 'm!

  • pjotrk
  • Registratie: Mei 2004
  • Laatst online: 15-07 18:43
De controle is inderdaad aanwezig op regel 53 echter:
code:
1
2
3
4
      else 
      { 
        $subrow[] = '<td>'.$node['title'].'</td>'; 
      }
:)

De controle die op regel 53 uitgevoerd wordt controleert volgens mij overigens niet of de node uitgeklapt moet worden (volgens mij dus wanneer de parent in het path_to_top lijstje zit), maar of deze node actief is (als de node zelf in het path_to_top lijstje zit).

[ot]
Daarnaast, maar minder belangrijk en enigsinds offtopic binnen P&W, zou ik een htmllist gebruiken voor de weergave van het tabmenu, deze zijn vaak wat flexibeler dan tables bij het weergeven van menustructuren. zie ook:
http://www.alistapart.com/articles/taminglists/
http://www.alistapart.com/articles/slidingdoors/
http://www.alistapart.com/articles/slidingdoors2/
[/ot]


Voorbeeld: (beperking tot 2 rijen eruit gehaald, maar die is er weer eenvoudig in terug te stoppen)
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
<style type="text/css">  
  div.tabs {width: 100%}
  div.tabs ul {margin: 0; list-style: none; clear: left}      
  div.tabs li {float: left; margin: 0; border: 1px solid #000}
  div.tabs li.active {background-color: #ddd;} 
</style>

<?
function build_tabs($active_node, $levels) 
{ 
  $unfold_nodes = path_to_top($active_node); 
  return _build_tabs($active_node, $unfold_nodes, $levels); 
} 

function _build_tabs($active_node, $unfold_nodes, $levels) 
{ 
  global $nodes;
  
  $tabRows = array();
  $tabsOutput = '';
  
  foreach ($nodes as $itemkey => $node) 
  {
    if ($node['level'] > 0 and $node['level'] <= $levels)
    {
      if (in_array($node['pid'], $unfold_nodes))
      {       
        $isActiveNode = in_array($itemkey, $unfold_nodes);
        
        $tabItemHTML  = $isActiveNode ? '<li class="active">':'<li>';
        $tabItemHTML .= $node['title'] . '</li>';
        
        $tabRows[$node['level']][] = $tabItemHTML;
      }
    }
  } 

  foreach ($tabRows as $tabItems) 
    $tabsOutput .= '<ul>'.implode('', $tabItems).'</ul>';

  return '<div class="tabs">'.$tabsOutput.'</div>'; 
} 

echo build_tabs(12, 2); 
?>
Pagina: 1