Alles, was Sie schon immer über Gruppierung wissen wollten

Bei der Arbeit mit Daten in Templates müssen Sie Elemente oft gruppieren, in Stapel aufteilen oder anhand einer Bedingung durchlaufen. Latte bietet dafür drei Werkzeuge, von denen jedes zu einer etwas anderen Situation passt.

Der Filter |group gruppiert Elemente nach einem angegebenen Kriterium, der Filter |batch teilt sie in Stapel fester Größe, und der Tag {iterateWhile} durchläuft die Daten Schritt für Schritt und entscheidet selbst, wann er die innere Schleife beendet. Gehen wir sie der Reihe nach durch.

Filter und Funktion group

Das Werkzeug lässt sich in zwei Formen verwenden: als Filter $items|group: … oder als Funktion group($items, …). Semantisch sind sie gleichwertig – wählen Sie nach Lesbarkeit.

Stellen Sie sich eine Datenbanktabelle items vor, deren Einträge zu verschiedenen Kategorien gehören:

id categoryId name
1 1 Apfel
2 1 Banane
3 2 PHP
4 3 Grün
5 3 Rot
6 3 Blau

Eine einfache Auflistung aller Einträge mit einem Latte-Template sähe so aus:

<ul>
{foreach $items as $item}
	<li>{$item->name}</li>
{/foreach}
</ul>

Wollten wir die Einträge jedoch nach Kategorien in Gruppen ordnen, müssen wir sie so aufteilen, dass jede Kategorie ihre eigene Liste hat. Das gewünschte Ergebnis sähe so aus:

<ul>
	<li>Apfel</li>
	<li>Banane</li>
</ul>

<ul>
	<li>PHP</li>
</ul>

<ul>
	<li>Grün</li>
	<li>Rot</li>
	<li>Blau</li>
</ul>

Diese Aufgabe lässt sich mit |group leicht und elegant lösen. Als Parameter geben wir categoryId an, die Einträge werden also anhand des Werts von $item->categoryId in kleinere Arrays aufgeteilt (wäre $item ein Array, würde $item['categoryId'] verwendet):

{foreach ($items|group: categoryId) as $categoryId => $categoryItems}
	<ul>
		{foreach $categoryItems as $item}
			<li>{$item->name}</li>
		{/foreach}
	</ul>
{/foreach}

Wollen Sie Einträge nach komplexeren Kriterien gruppieren, können Sie im Parameter des Filters eine Funktion verwenden. Der Schlüssel jeder Gruppe ist dann der Rückgabewert der Funktion – bei einer Gruppierung nach der Länge des Namens also die Anzahl der Zeichen:

{foreach ($items|group: fn($item) => strlen($item->name)) as $length => $group}
	...
{/foreach}

Wichtig zu wissen: Jede Gruppe (einschließlich $categoryItems) ist kein gewöhnliches Array, sondern ein Objekt, das sich wie ein Iterator verhält – Sie können also nicht über den Index auf Elemente zugreifen, etwa $categoryItems[0]. Die Elemente lassen sich jedoch mit count($categoryItems) zählen, und für den Zugriff auf das erste Element der Gruppe verwenden Sie die Funktion first().

Diese Flexibilität macht |group zu einem außerordentlich nützlichen Werkzeug für die Darstellung von Daten.

Verschachtelte Schleifen

Stellen wir uns vor, unsere Datenbanktabelle hätte eine weitere Spalte subcategoryId, die für jeden Eintrag Unterkategorien festlegt. Wir wollen jede Hauptkategorie in einer eigenen Liste <ul> und jede Unterkategorie innerhalb dieser Hauptkategorie in einer eigenen verschachtelten Liste <ol> anzeigen:

{foreach ($items|group: categoryId) as $categoryItems}
	<ul>
		{foreach ($categoryItems|group: subcategoryId) as $subcategoryItems}
			<ol>
				{foreach $subcategoryItems as $item}
					<li>{$item->name}
				{/foreach}
			</ol>
		{/foreach}
	</ul>
{/foreach}

Zusammen mit Nette Database

Zeigen wir, wie sich die Gruppierung von Daten wirkungsvoll mit Nette Database kombinieren lässt. Nehmen wir an, wir arbeiten mit der Tabelle items aus dem einleitenden Beispiel, die über die Spalte categoryId mit dieser Tabelle categories verknüpft ist:

categoryId name
1 Früchte
2 Sprachen
3 Farben

Die Daten aus der Tabelle items laden wir mit Nette Database Explorer über den Befehl $items = $db->table('items'). Beim Durchlaufen dieser Daten können wir nicht nur auf Attribute wie $item->name und $item->categoryId zugreifen, sondern dank der Beziehung zur Tabelle categories auch über $item->category auf die zugehörige Zeile. Diese Beziehung erlaubt interessante Anwendungen:

{foreach ($items|group: category) as $category => $categoryItems}
	<h1>{$category->name}</h1>
	<ul>
		{foreach $categoryItems as $item}
			<li>{$item->name}</li>
		{/foreach}
	</ul>
{/foreach}

In diesem Fall verwenden wir den Filter |group, um nach der zugehörigen Zeile $item->category zu gruppieren, nicht nur nach der Spalte categoryId. Der Schlüssel ($category) hält dadurch direkt das Objekt ActiveRow der jeweiligen Kategorie, sodass wir ihren Namen mit {$category->name} ausgeben und auf jede weitere Spalte zugreifen können, ohne eine eigene Abfrage an categories zu stellen.

Filter |batch

Der Filter teilt eine Liste von Elementen in Stapel fester Größe. Praktisch ist das für Grid-Layouts, die Anordnung in Spalten oder jede Art visueller Gruppierung.

Stellen wir uns vor, wir wollen die Elemente in Listen anzeigen, von denen jede höchstens drei Elemente enthält:

{foreach ($items|batch: 3) as $batch}
	<ul>
		{foreach $batch as $item}
			<li>{$item->name}</li>
		{/foreach}
	</ul>
{/foreach}

In diesem Beispiel wird die Liste $items in kleinere Gruppen aufgeteilt, von denen jede ($batch) bis zu drei Elemente enthält. Jeder Stapel wird dann in einer eigenen Liste <ul> angezeigt.

Enthält die letzte Gruppe nicht genügend Elemente, um die gewünschte Anzahl zu erreichen, lässt sich mit dem zweiten Parameter des Filters festlegen, womit diese Gruppe aufgefüllt wird. Das eignet sich hervorragend, um Elemente ästhetisch auszurichten, wo eine unvollständige Reihe unordentlich wirken könnte.

{foreach ($items|batch: 3, '—') as $batch}
	...
{/foreach}

Tag {iterateWhile}

Dieselben Aufgaben, die wir mit dem Filter |group gelöst haben, zeigen wir nun mit dem Tag {iterateWhile}. Der wesentliche Unterschied zwischen beiden Ansätzen ist, dass |group zuerst alle Eingabedaten verarbeitet und gruppiert, während {iterateWhile} den Fortgang der Schleife über eine Bedingung steuert und die Iteration nacheinander abläuft.

Rendern wir zuerst die Tabelle mit den Kategorien mithilfe von {iterateWhile}:

{foreach $items as $item}
	<ul>
		{iterateWhile}
			<li>{$item->name}</li>
		{/iterateWhile $item->categoryId === $iterator->nextValue->categoryId}
	</ul>
{/foreach}

Während {foreach} den äußeren Teil des Zyklus kennzeichnet, also das Zeichnen der Listen für jede Kategorie, kennzeichnet der Tag {iterateWhile} den inneren Teil, also die einzelnen Elemente. Die Bedingung im schließenden Tag sagt, dass die Wiederholung so lange fortgesetzt wird, wie das aktuelle und das nächste Element zur selben Kategorie gehören ($iterator->nextValue ist das nächste Element; beim letzten Element endet die innere Schleife, weil es kein nächstes gibt – Latte prüft vor der Auswertung der Bedingung $iterator->hasNext(), sodass es nie zum Vergleich mit null kommt).

Wäre die Bedingung immer wahr, würden alle Elemente innerhalb des ersten <ul> gerendert:

{foreach $items as $item}
	<ul>
		{iterateWhile}
			<li>{$item->name}
		{/iterateWhile true}
	</ul>
{/foreach}

Das Ergebnis sähe so aus:

<ul>
	<li>Apfel</li>
	<li>Banane</li>
	<li>PHP</li>
	<li>Grün</li>
	<li>Rot</li>
	<li>Blau</li>
</ul>

Was bringt es, {iterateWhile} auf diese Weise zu verwenden? Da das <ul> innerhalb des äußeren {foreach} steht, wird bei leerer Eingabe überhaupt nichts gerendert – kein einsames <ul></ul>. Ohne {iterateWhile} müssten Sie denselben Fall mit einem {if} vor dem öffnenden Tag oder über {foreachelse} behandeln.

Geben wir die Bedingung im öffnenden Tag {iterateWhile} an, ändert sich das Verhalten: Die Bedingung (und der Übergang zum nächsten Element) wird am Anfang des inneren Zyklus ausgeführt, nicht am Ende. Während Sie also {iterateWhile} ohne Bedingung immer betreten, betreten Sie {iterateWhile $cond} nur dann, wenn die Bedingung $cond erfüllt ist. Und zugleich wird das nächste Element in $item geschrieben.

Das ist in Situationen nützlich, in denen wir das erste Element jeder Kategorie anders rendern wollen als die übrigen, zum Beispiel so:

<h1>Apfel</h1>
<ul>
	<li>Banane</li>
</ul>

<h1>PHP</h1>
<ul>
</ul>

<h1>Grün</h1>
<ul>
	<li>Rot</li>
	<li>Blau</li>
</ul>

(Das leere <ul></ul> bei der Kategorie PHP veranschaulicht nur die Mechanik – in echtem Code würden Sie das Rendern des <ul> mit einem {if} behandeln.)

Wir passen den ursprünglichen Code so an, dass zuerst das Element als Überschrift gerendert wird und dann die innere Schleife {iterateWhile} die folgenden Elemente derselben Kategorie als Listeneinträge rendert:

{foreach $items as $item}
	<h1>{$item->name}</h1>
	<ul>
		{iterateWhile $item->categoryId === $iterator->nextValue?->categoryId}
			<li>{$item->name}</li>
		{/iterateWhile}
	</ul>
{/foreach}

Innerhalb einer einzigen Schleife können wir mehrere innere Schleifen anlegen und sie sogar verschachteln. So lässt sich auf mehreren Ebenen zugleich gruppieren – zum Beispiel Unterkategorien unter Kategorien.

Nehmen wir an, die Tabelle hat eine weitere Spalte subcategoryId, und neben jeder Kategorie in einem eigenen <ul> soll jede Unterkategorie in einem eigenen <ol> stehen:

{foreach $items as $item}
	<ul>
		{iterateWhile}
			<ol>
				{iterateWhile}
					<li>{$item->name}
				{/iterateWhile $item->subcategoryId === $iterator->nextValue->subcategoryId}
			</ol>
		{/iterateWhile $item->categoryId === $iterator->nextValue->categoryId}
	</ul>
{/foreach}
Version: 3.x