Extbase / Fluid: Hierarchische Selectbox

TYPO3Die Standard-ViewHelper von Fluid bieten schon für eine ganze Menge Anwendungen die richtigen Klassen. Wenn Formulare oder andere Elemente aber ein wenig komplexer werden, stößt Fluid schnell an seine Grenzen. Zum Glück ist Fluid modular aufgebaut und kann leicht durch eigene ViewHelper erweitert werden. So ist es dann auch mit wenig Aufwand möglich, zum Beispiel eine hierarchische Selectbox mit Option Groups erzeugen zu lassen – nützlich zum Beispiel für die Auswahl aus Kategoriebäumen…

Der ViewHelper dafür kann einfach von dem Standard-SelectViewHelper aus dem Fluid Paket abgeleitet werden. Für unseren erweiterten SelectViewHelper müssen wir lediglich zwei Methoden überschreiben und zwei eigene Methoden implementieren. Zuerst legen wir in unserer Extbase-Extension im Verzeichnis Classes/ViewHelpers/ eine neue Datei mit dem schönen Namen SelectViewHelper an. Dort werden wir den neuen ViewHelper implementieren. Der Code der neuen ViewHelper-Klasse sieht dann so aus:

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
class Tx_MyExt_ViewHelpers_SelectViewHelper extends Tx_Fluid_ViewHelpers_Form_SelectViewHelper {
	/**
	 * Überschreibt die Methode aus dem Fluid-ViewHelper. Es wird geprüft, ob es in dem Option-Objekt eine Methode getChildren() gibt.
	 * Diese sollte Kind-Objekte aus dem Kategoriebaum zurückgeben. Werden Kind-Objekte gefunden, wird eine Option Group
	 *  gerendert, ansonsten die ganz normalen Option-Tags.
	 */
	protected function renderOptionTags($options) {
		$output = '';
		foreach ($options as $value => $option) {
			$children = array();
			if(method_exists($option, 'getChildren') && is_callable(array($option, 'getChildren'))) {
				$children = $option->getChildren();
			}
			if(!empty($children)) {
				$output .= $this->renderOptionGroupTag($this->getLabel($option), $children);
			} else {
				$value = $option->getUid();
				$isSelected = $this->isSelected($value);
				$output.= $this->renderOptionTag($value, $this->getLabel($option), $isSelected) . chr(10);
			}
		}
		return $output;
	}
 
	/**
	 * Auch hier muss eine Methode aus der Fluid-Klasse überschrieben werden, um nicht beim auslesen der Options
	 * die Objekte zu verlieren. Im Fluid-ViewHelper werden an dieser Stelle aus den Objekten bereits Strings gemacht.
	 * Das muss verhindert werden!
	 */
	protected function getOptions() {
		if (!is_array($this->arguments['options']) && !($this->arguments['options'] instanceof Traversable)) {
			return array();
		}
		$options = array();
		foreach ($this->arguments['options'] as $key => $value) {
			if (is_object($value)) {
				if ($this->arguments->hasArgument('optionValueField')) {
					$key = Tx_Extbase_Reflection_ObjectAccess::getProperty($value, $this->arguments['optionValueField']);
					if (is_object($key)) {
						if (method_exists($key, '__toString')) {
							$key = (string)$key;
						} else {
							throw new Tx_Fluid_Core_ViewHelper_Exception('Identifying value for object of class "' . get_class($value) . '" was an object.' , 1247827428);
						}
					}
				} elseif ($this->persistenceManager->getBackend()->getIdentifierByObject($value) !== NULL) {
					$key = $this->persistenceManager->getBackend()->getIdentifierByObject($value);
				} elseif (method_exists($value, '__toString')) {
					$key = (string)$value;
				} else {
					throw new Tx_Fluid_Core_ViewHelper_Exception('No identifying value for object of class "' . get_class($value) . '" found.' , 1247826696);
				}
			}
			$options[$key] = $value;
		}
		if ($this->arguments['sortByOptionLabel']) {
			asort($options);
		}
		return $options;
	}
 
	/**
	 * eine eigene Methode, um aus den Option-Objekten ein String-Label zu holen.
	 */
	protected function getLabel($option) {
		$label = '';
		if ($this->arguments->hasArgument('optionLabelField')) {
			$label = Tx_Extbase_Reflection_ObjectAccess::getProperty($option, $this->arguments['optionLabelField']);
			if (is_object($label)) {
				if (method_exists($label, '__toString')) {
					$label = (string)$label;
				} else {
					throw new Tx_Fluid_Core_ViewHelper_Exception('Label value for object of class "' . get_class($label) . '" was an object without a __toString() method.' , 1247827553);
				}
			}
		} elseif (method_exists($option, '__toString')) {
			$label = (string)$option;
		} elseif ($this->persistenceManager->getBackend()->getIdentifierByObject($option) !== NULL) {
			$label = $this->persistenceManager->getBackend()->getIdentifierByObject($option);
		}
		return $label;
	}
 
	/**
	 * Diese Methode rendert ein optgroup-Tag mit untergeordneten Kind-Optionen 
	 */
	protected function renderOptionGroupTag($label, $options) {
		return '<optgroup label="' . htmlspecialchars($label) . '">' . $this->renderOptionTags($options) . '</optgroup>';
	}
}

Ein entsprechende Fluid-Template kann dann z.B. so aussehen:

1
2
3
4
5
6
7
8
9
10
11
{namespace myext=Tx_MyExt_ViewHelpers}
<f:layout name="default" />
<f:section name="content">
	<f:form method="post" controller="MyController" action="create" name="newEntry" object="{newEntry}">
		<label for="title">Title <span class="required">*</span></label><br />
		<f:form.textbox property="title" /><br />
		<label for="category">Category</label><br />
		<myext:select property="category" options="{categories}" optionLabelField="title" /><br />
		<f:form.submit class="submit" value="Submit"/>
	</f:form>
</f:section>

Mit der ersten Zeile ({namespace … }) wird user ViewHelper-Verzeichnis bekannt gemacht und ein eigener Namensbereich (myext) vergeben. Über diesen Namensbereich können wir dann unseren eigenen Select-ViewHelper ansprechen ().
Die Attribute für diesen ViewHelper-Tag entsprechen denen des Fluid-Select-ViewHelpers – wir haben ja davon abgeleitet.
Die Objekte, die in der Fluid-Variablen {categories} übergeben werden, sind die Kategorien der ersten Ebene des Baumes (also diejenigen ohne übergeordnete Kategorie). Die Domain Objekte der Kategorien müssen eine Methode getChildren() implementieren, die alle untergeordneten Kategorien in einem Array (oder einem anderen traversierbaren Container) zum jeweiligen Objekt zurückgeben. Der ViewHelper wird nun aus diesen Elternkategorien optgroup-Tags rendern, die auch nicht ausgewählt werden können. Aus den Kategorie-Objekten, die keine untergeordneten Kategorien mehr haben, werden dann entsprechend die auswählbaren Option-Tags gerendert.

Ein Beispiel für die Kategorie-Klasse folgt hier:

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
class Tx_MyExt_Domain_Model_Category extends Tx_Extbase_DomainObject_AbstractEntity {
 
	/**
	 * @var string The category´s title
	 */
	protected $title = '';
	/**
	 * @var Tx_MyExt_Domain_Model_Category The parent category
	 */
	protected $parent = NULL;
 
	/**
	 * Returns the category´s title.
	 * @return string
	 */
	public function getTitle() {
		return $this->title;
	}
 
	/**
	 * Sets the category´s title.
	 * @param string $title The category´s title to set
	 */
	public function setTitle($title) {
		$this->title = $title;
	}
 
	/**
	 * Returns the parent category.
	 * @return Tx_MyExt_Domain_Model_Category
	 */
	public function getParent() {
		return $this->parent;
	}
 
	/**
	 * Sets the parent category.
	 * @param Tx_MyExt_Domain_Model_Category $parent the parent category
	 */
	public function setParent(Tx_MyExt_Domain_Model_Category $parent) {
		$this->parent = $parent;
	}
 
	/**
	 * Returns all child categories of this
	 * @return array an array of Tx_MyExt_Domain_Model_Category objects
	 */
	public function getChildren() {
		$categoryRepository = t3lib_div::makeInstance('Tx_MyExt_Domain_Repository_CategoryRepository');
		$children = $categoryRepository->findAllChildren($this);
		return $children;
	}
}

Und das zugehörige Repository könnte in etwa so aussehen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Tx_MyExt_Domain_Repository_CategoryRepository extends Tx_Extbase_Persistence_Repository {
 
	public function findAll() {
		$query = $this->createQuery();
		// only find the categories without parent categories
		$query->matching($query->equals('parent', 0));
		$query->setOrderings(array('sorting' => Tx_Extbase_Persistence_QueryInterface::ORDER_ASCENDING));
		return $query->execute();
	}
 
	public function findAllChildren(Tx_MyExt_Domain_Model_Category $category) {
		$query = $this->createQuery();
		$query->matching($query->equals('parent', $category));
		$query->setOrderings(array('sorting' => Tx_Extbase_Persistence_QueryInterface::ORDER_ASCENDING));
		return $query->execute();
	}
}

7 Gedanken zu „Extbase / Fluid: Hierarchische Selectbox

  1. Anja Leichsenring

    Ewig alt, hat mir aber trotzdem grade sehr geholfen. Wenn Du das Beispiel mal nach GitHub stellst und mir eine Mail schreibst, steuere ich eine 6.2/7 compatible Version bei.
    Danke fuer die Muehe.

    Antworten
  2. AR

    Wie würde man dies mit namespaces und einer M:N Tabelle?

    Bei mir gibt das nur ne weisse Seite 🙁

    Irgendwie hat er mit der ersten Funktion vom Viewhelper ein Problem.

    wenn ich diesen Block:
    $value = $option->getUid();
    $isSelected = $this->isSelected($value);
    $output.= $this->renderOptionTag($value, $this->getLabel($option), $isSelected) . chr(10);

    auskommentiere wird das frontendausgeführt die Werte aber bleiben leer 🙁

    Antworten
    1. AR

      Typo3 6.1.7
      Extbase 6.1.0
      Fluid 6.1.0
      Extension Builder 2.5.2

      – Die Kategorie hat ein Feld Refernce und ein Feld uid und mann kann Unterkategorien anlegen indem man in der Referenz die UId der Elternkategorie angibt.

      Soll eine Filter werden für Einträge (selctbox)

      Antworten
  3. Ronald

    Falls hier mal noch jemand liest, also ich habe es zum Laufen bekommen. Der eine Fehler da hatte eine andere Ursache. Allerdings funktionierte es dann trotzdem noch nicht ganz. In der findAllChildren() sollte die Zeite return $query->execute(); ersetzte werden durch return $query->execute()->toArray(); Denn ein Query ist nie ein leeres Array. Allerdings wird das aber als Bedingung in der renderOptionTags() notwendig, damit nicht nur optGroups gerendert werden.

    Antworten
  4. Ronald

    Also bei mir das Modell auch etwas anders, aber Anpassungen habe ich vorgenommen. Bei mir werden alle Unterkategorien ebenfalls als OptGroup gerendert. Aber da getChildren doch auch ein Repository ist, ist es doch nie empty?! Habe als bei meiner getChildren()-Methode das return wie folgt geändert: return $children->toArray(); und es funktioniert!

    Antworten
  5. Ronald

    Kriege leider den Fehler:

    Cannot cast object of type “Tx_Extbase_Persistence_QueryResult” to string.

    Und weiß auch nicht, woran es liegt. Die Schritte sind ja sehr einleuchtend. Mein Modell weicht zwar ab, aber die wichtige Methode getChildren habe ich implementiert.

    Antworten
  6. Felix Eggbert

    Super Artikel, ggf. noch Repository-Methoden mit Comment-Blöcken und entsprechenden Parametern ergänzen.

    Gruß, Eggbert

    Antworten

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.