Formular-Select mit Hierarchie aufbauen (aus PHP-Array)

suntrop

Erfahrenes Mitglied
Hallo :-)

Ich probiere schon seit fast 7 Stunden ein Formular-Select aufzubauen. Die Daten kommen aus einer Datenbank:
Code:
id    parent  name
0     0       Eins
1     0       Zwei
2     1       Zwei-Eins
3     2       Zwei-Eins-Eins
4     2       Zwei-Eins-Zwei
5     0       Drei
6     5       Drei-Eins
7     5       Drei-Zwei
8     7       Drei-Zwei-Eins

Also jeder Eintrag hat eine Zuordnung zu einer Eltern-ID (Feld "parent"). Daraus will ich jetzt für ein Formular ein <select> aufbauen. Resultat soll so aussehen:
HTML:
<select>
  <option vlaue="0">Eins</option>
  <option vlaue="1">Zwei</option>
  <option vlaue="2">|-- Zwei-Eins</option>
  <option vlaue="3">|-- --Zwei-Eins-Eins</option>
  <option vlaue="4">|-- --Zwei-Eins-Zwei</option>
  <option vlaue="5">Drei</option>
  <option vlaue="6">|-- Drei-Eins</option>
  <option vlaue="7">|-- Drei-Zwei</option>
  <option vlaue="8">|-- --Drei-Zwei-Eins</option>
Also eine Verschachtelung über mehrere Ebenen hinweg, wobei jede Kind-Ebene etwas eingerückt werden soll (damit die Hierarchie sichtbar ist).

Leider bekomme ich es einfach nicht hin. In meiner Funktion – in der ich die gesamte Datenbank-Tabelle als einfaches Array habe – versuche ich es gerade so:
PHP:
foreach ($arr as $row) {
			//$cat[$row['id']][$row['name']] = $row['name'];
			if ($row['parent'] == 0) {
				$cat[$row['id']][] = $row['name'];
			} else {
				$cat[$row['parent']][$row['id']] = '--' . $row['name'];
			}
		}

Aber das passt mal total nicht. Ich weiß auch gar nicht, wie ich die Reihenfolge einhalten soll. Wenn ich in einer ersten foreach-Schleife ein neues Array aufbaue, dann geht mir die Zuordnung verloren, also die Rheinfolge stimmt nicht mehr. Aber ich weiß nicht, wie ich jetzt vorgehen muss.

Kann mir jemand weiterhelfen? Bin ziemlich ratlos.

Danke und Grüße
- suntrop -
 
Danke für den Link. Das sieht passend aus. Jedoch drehe ich mich im Kreis. :-)
Verstehe ja, dass die Funktion das tun soll, aber ich komme einfach nicht hinter den Trick – ich kann das von der Ordner-Struktur aus dem Beispiel nicht auf meine Situation übertragen.

PHP:
function nested( $arr, $ebene )
{
	foreach ($arr as $row) {
		if ($row['parent'] == 0 && $ebene != 'sub') {
			$cat[$row['id']][$row['id']] = $row['name'];
		} else {
			nested($arr, 'sub');
		}
	}
	return $cat;
}

Das scheint eine Endlos-Schleife zu ergeben (Fatal error: Allowed memory size of 104857600 bytes exhausted).
 
Du hast da einen kleinen Logik Fehler drin.

Code:
id    parent  name
0     0       Eins
1     0       Zwei
2     1       Zwei-Eins
3     2       Zwei-Eins-Eins
4     2       Zwei-Eins-Zwei
5     0       Drei
6     5       Drei-Eins
7     5       Drei-Zwei
8     7       Drei-Zwei-Eins

HTML:
<select>
  <option vlaue="0">Eins</option>
  <option vlaue="1">Zwei</option>
  <option vlaue="2">|-- Zwei-Eins</option>
  <option vlaue="3">|-- --Zwei-Eins-Eins</option>
  <option vlaue="4">|-- --Zwei-Eins-Zwei</option>
  <option vlaue="5">Drei</option>
  <option vlaue="6">|-- Drei-Eins</option>
  <option vlaue="7">|-- Drei-Zwei</option>
  <option vlaue="8">|-- --Drei-Zwei-Eins</option>

Zwei und drei sind Kinder von Eins, in deinem HTML ist das aber nicht so.

Im Prinzip rufst du einfach alle Einträge ab. Dann durchläufst du sie und erstellst Arrays die nach der Parent ID sortiert sind. Dann nutzt du die Funktion von oben und fängst mit 0 an.

Wirklich empfehlen würde ich dir aber Nested Sets, die sind etwas besser geeignet um Bäume darzustellen.
http://www.klempert.de/nested_sets/
 
Das obige Beispiel habe ich getippt, nicht kopiert. In der Datenbank hat "Eins" die ID 1, nicht 0.
Ich werde mir das Beispiel mit den Nested Sets ansehen. Hoffe ich kann die Datenbank umstellen :-) … und verstehe dann auch, was ich machen muss.

Danke nochmals!
 
Du kannst es direkt im SQL umsetzen:
MySQL Hierarchie Baum (Adjacency Tree) auslesen -> 3) Tree-Darstellung

Nachtrag:
Die nested Sets sind zwar einfacher zum auslsen, aber nicht zum warten. Ausser man baut sich ein GUI um die Einträge fit zu halten.

Nachtrag II:
Oder eben in PHP
PHP:
//mein Testarray
$tree[] = array('id'=>1, 'parent'=>0, 'name'=>'Eins');
$tree[] = array('id'=>2, 'parent'=>0, 'name'=>'Zwei');
$tree[] = array('id'=>3, 'parent'=>2, 'name'=>'Zwei-Eins');
$tree[] = array('id'=>4, 'parent'=>3, 'name'=>'Zwei-Eins-Eins');
$tree[] = array('id'=>5, 'parent'=>3, 'name'=>'Zwei-Eins-Zwei');
$tree[] = array('id'=>6, 'parent'=>0, 'name'=>'Drei');
$tree[] = array('id'=>7, 'parent'=>6, 'name'=>'Drei-Eins');
$tree[] = array('id'=>8, 'parent'=>7, 'name'=>'Drei-Zwei');
$tree[] = array('id'=>9, 'parent'=>8, 'name'=>'Drei-Zwei-Eins');


//den TreeArray um die Tree-Informationen ergänzen
addTreeInfos($tree);
//und richtig sortieren
array_sort_by_subarray_item($tree, 'sort');
//und ausgeben
echo '<select>';
foreach($tree as $node){
    echo sprintf('<option vlaue="%d">%s</option>', $node['id'], $node['text']);
}
echo '</select>';

/**
 * Egänzt den Tree-Arry um die Felder 'sort', 'lvl', 'text'
 * @param Array<Node>    $array        Array mit den Treenodes (als Referenz)
 * @param Integer        $parent       Parent-ID
 * @param String         $sort         aktueller Sortierungs-String
 * @param Integer        $lvl          Level
 */
function addTreeInfos(&$array, $parent = 0, $sort = '', $lvl = 0){
    for($i = 0; $i < count($array); $i++){        
        $node = &$array[$i];
        if($node['parent'] == $parent){
            $node['sort'] = trim(sprintf('%s.%04d', $sort, $node['id']), '.');
            $node['lvl'] = $lvl;
            $node['text'] = str_repeat('-- ', $lvl) . $node['name'];
            addTreeInfos($array, $node['id'], $node['sort'], $lvl+1);
        }
    }
    
}



// benutzte zusätzliche Funktionen
/**
 * mpl      by ERB software
 * @author  stefan.erb(at)erb-software.com
 * @link    http://wiki.yaslaw.info/wikka/PhpArraySortBySubarrayItem
 * @since   PHP 5.2
 * Sortiert einen 2-Dimensionalen Array nach einem Key in der 2ten Dimension
 * @param $array        Array der sortiert wird
 * @param $key          Key im Array der sortiert werden soll (key oder index)
 * @param $direction    Sortierrichtung. SORT_ASC oder SORT_DESC
 * @param $sort_flags   SORT_REGULAR, SORT_STRING, SORT_NUMERIC. Siehe sort_flags-Parameter von sort()
 */
function array_sort_by_subarray_item(&$array, $key, $direction = SORT_ASC, $sort_flags = SORT_REGULAR){
    $sort = array();
    foreach($array as $index => $item){
        $sort[$index] = strtoupper($item[$key]);
    }
    return array_multisort($sort, $direction, $sort_flags, $array);
}

ergibt
HTML:
<select>
    <option vlaue="1">Eins</option>
    <option vlaue="2">Zwei</option>
    <option vlaue="3">-- Zwei-Eins</option>
    <option vlaue="4">-- -- Zwei-Eins-Eins</option>
    <option vlaue="5">-- -- Zwei-Eins-Zwei</option>
    <option vlaue="6">Drei</option>
    <option vlaue="7">-- Drei-Eins</option>
    <option vlaue="8">-- -- Drei-Zwei</option>
    <option vlaue="9">-- -- -- Drei-Zwei-Eins</option>
</select>
 
Danke Yaslaw! Ich werde den Code gleich mal rüberkopieren und testen. Das HTML-Ergebnis ist jedenfalls genau, was ich brauche :-)

Ich habe die letzten 5 Stunden auch viel probiert und dachte mir noch folgendes. Ich schaue einfach bei jedem Item nach, ob parent!=0 ist und falls ja, dann gehe ich solange/rekursiv in das parent-Item, bis dort parent==0. Mit jedem Durchlauf erhöhe ich eine $depth Variable und die ergibt wiederum die Anzahl an Bindestrichen.

Für die erste und zweite Ebene funktioniert es, aber nicht für die dritte Ebene. Ich glaube die rekursiven Wiederholungen überfordern mein Hirn :-)

/*
Das ist die Funktion die ich mit meinem Array aus der Datenbank aufrufe.
Falls der Eintrag parent gleich Null ist, dann erzeuge ich ein neues Array mit
der ID als Key.
Falls der Eintrag nicht Null ist, dann wird die Funktion getDepth() aufgerufen,
die dann rekursiv die Verschachtelungstiefe liefern soll.
*/
PHP:
function buildArray($fullArray) {
    foreach ($fullArray as $row) {
        if ($row['parent'] == 0) {
            $newArray[ $row['id'] ] = $row['name'];
        }
        else {
            $depth = getDepth($fullArray, $row['parent']);
            $newArray[ $row['id'] ] = $depth . $row['name'];
        }
    }
    return $newArray;
}


/*
  Die Funktion liefert für jede Verschachtelung ein zusätzliches "-" zurück
  */
function getDepth($fullArray, $id, $depth = 0) {
    $parentID = searchForId($id, $fullArray);
    if ($fullArray[ $parentID ]['parent'] != 0) {
        $depth++;
        getDepth($fullArray, $fullArray[ $parentID ]['parent'], $depth);
    } else {
        return str_repeat('-', ++$depth);
    }
}

/*
  Diese Funktion soll im "großen" Array nach der ID suchen
  */
function searchForId($id, $array) {
   foreach ($array as $key => $val) {
       if ($val['id'] == $id) {
           return $key;
       }
   }
   return null;
}

Leider sieht mein Array so aus:
PHP:
Array
(
    [1] => Test 1
    [2] => Test 2
    [3] => Test 3
    [4] => -Test 3.1
    [5] => -Test 3.2
    [6] => -Test 3.3
    [7] => Test 3.3.1
    [8] => Test 3.3.2
    [9] => Test 4
    [10] => Test 5
    [11] => -Test 5.1
)
… also die Einträge 3.3.1 und 3.3.2 haben keine Einrückung.
Ich werde daran auch noch etwas probieren. Teste aber jetzt erstmal deine Funktion.

danke nochmals!!
 
Super, der Code von Yaslaw funktioniert. :-)
Meine Version noch nicht ganz, wenn ichs noch schaffe poste ich ihn hier.

Grüße
- suntrop -
 
Hallo,

man könnte auch die DOM-Klassen verwenden und würde ohne Rekursion auskommen:
PHP:
// Testarray:
$tree[] = array('id'=>1, 'parent'=>0, 'name'=>'Eins');
$tree[] = array('id'=>2, 'parent'=>0, 'name'=>'Zwei');
$tree[] = array('id'=>3, 'parent'=>2, 'name'=>'Zwei-Eins');
$tree[] = array('id'=>4, 'parent'=>3, 'name'=>'Zwei-Eins-Eins');
$tree[] = array('id'=>5, 'parent'=>3, 'name'=>'Zwei-Eins-Zwei');
$tree[] = array('id'=>6, 'parent'=>0, 'name'=>'Drei');
$tree[] = array('id'=>7, 'parent'=>6, 'name'=>'Drei-Eins');
$tree[] = array('id'=>8, 'parent'=>7, 'name'=>'Drei-Eins-Eins');
$tree[] = array('id'=>9, 'parent'=>8, 'name'=>'Drei-Eins-Eins-Eins');

/* DOM-Dokument und Wurzelelement: */
$domDoc = new DomDocument();
$domDoc->formatOutput = true; /* prettyPrint-Ausgabe */
$element_id0 = $domDoc->createElement('select');
$domDoc->appendChild($element_id0);

/* Array durchlaufen: */
foreach($tree as $element) {
	/* Variablenname der Knoten zusammenstellen: */
	$current = 'element_id'.$element['id'];
	$parent = 'element_id'.$element['parent'];

	/* Strukturtiefe ermitteln: */
	$depth = ($parent == 'element_id0') ? 0 : substr_count($$parent->nodeValue,'- ')+1;
	/* Knoteninhalt: */
	$nodeVal = str_repeat('- ', $depth).$element['name'];

	/* Knoten erzeugen und in Wurzelelement einhängen: */
	$$current = $domDoc->createElement('option', $nodeVal);
	$element_id0->appendChild($$current);

	/* VALUE-Attribut einfügen: */
	$valAttr = $domDoc->createAttribute('value');
	$valAttr->value = $element['id'];
	$$current->appendChild($valAttr);
}

/* HTML-String ausgeben: */
echo $domDoc->saveHTML();
Ausgabe:
HTML:
<select><option value="1">Eins</option>
<option value="2">Zwei</option>
<option value="3">- Zwei-Eins</option>
<option value="4">- - Zwei-Eins-Eins</option>
<option value="5">- - Zwei-Eins-Zwei</option>
<option value="6">Drei</option>
<option value="7">- Drei-Eins</option>
<option value="8">- - Drei-Eins-Eins</option>
<option value="9">- - - Drei-Eins-Eins-Eins</option></select>

edit: Fehlerkorrektur
Die Zeile
PHP:
/* Strukturtiefe ermitteln: */
	$depth = (substr($parent,-1) == '0') ? 0 : substr_count($$parent->nodeValue,'- ')+1;
führt zu Fehlern, wenn auf einen PARENT-Knoten mit dem Index 10, 20 usw. zugegriffen wird. Der Vergleich soll besser mit dem kompletten Namen des Wurzelements erfolgen.
Ich habe das im oben stehenden PHP-Script korrigiert.
 
Zuletzt bearbeitet:
Ich seh noch nicht ganz wie das funktioniert. Mit $$parent und $$current ist nicht mehr ganz einfach lesbar für mich (ja, ich kenne die Schreibweise..).

Wie verhält sich das, wenn die Daten mal nicht sortiert aus der DB kommen? Diese Daten lassen sich nur sortieren wennd ie id genau die richtige Reihenfolge hat.

Wenn die Daten beispielsweise so in der DB stehen:
PHP:
$tree[] = array('id'=>21, 'parent'=>0, 'name'=>'Zwei');
$tree[] = array('id'=>43, 'parent'=>21, 'name'=>'Zwei-Eins');
$tree[] = array('id'=>7, 'parent'=>6, 'name'=>'Drei-Eins');
$tree[] = array('id'=>14, 'parent'=>43, 'name'=>'Zwei-Eins-Eins');
$tree[] = array('id'=>5, 'parent'=>43, 'name'=>'Zwei-Eins-Zwei');
$tree[] = array('id'=>8, 'parent'=>7, 'name'=>'Drei-Eins-Eins');
$tree[] = array('id'=>9, 'parent'=>8, 'name'=>'Drei-Eins-Eins-Eins');
$tree[] = array('id'=>6, 'parent'=>0, 'name'=>'Drei');
$tree[] = array('id'=>1, 'parent'=>0, 'name'=>'Eins');
 
Zurück