Wszystko, co zawsze chciałeś wiedzieć o grupowaniu

Podczas pracy z danymi w szablonach często pojawia się potrzeba ich grupowania lub wyświetlania według określonych kryteriów. W tym celu Latte oferuje kilka zaawansowanych narzędzi.

Filtr i funkcja |group pozwalają na efektywne grupowanie danych w oparciu o określone kryteria, podczas gdy filtr |batch ułatwia dzielenie danych na stałe partie, a tag {iterateWhile} zapewnia możliwość bardziej złożonej kontroli cyklu z warunkami. Każdy z tych tagów oferuje określone opcje pracy z danymi, co czyni je niezbędnymi narzędziami do dynamicznego i uporządkowanego wyświetlania informacji w szablonach Latte.

Filtr i funkcja group

Wyobraźmy sobie tabelę bazy danych items z elementami podzielonymi na kategorie:

id categoryId name
1 1 Jabłko
2 1 Banana
3 2 PHP
4 3 Zielony
5 3 Czerwony
6 3 Niebieski

Prosta lista wszystkich elementów przy użyciu szablonu Latte wyglądałaby następująco:

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

Jeśli jednak chcemy, aby przedmioty były zorganizowane w grupy według kategorii, musimy podzielić je tak, aby każda kategoria miała własną listę. Wynik wyglądałby wtedy następująco:

<ul>
	<li>Apple</li>
	<li>Banana</li>
</ul>

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

<ul>
	<li>Green</li>
	<li>Red</li>
	<li>Blue</li>
</ul>

Zadanie to można łatwo i elegancko rozwiązać za pomocą |group. Jako parametr podajemy categoryId, co oznacza, że elementy zostaną podzielone na mniejsze tablice w oparciu o wartość $item->categoryId (gdyby $item była tablicą, użylibyśmy $item['categoryId']):

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

Filtr może być również użyty jako funkcja w Latte, dając nam alternatywną składnię: {foreach group($items, categoryId) ...}.

Jeśli chcesz pogrupować elementy według bardziej złożonych kryteriów, możesz użyć funkcji w parametrze filtra. Na przykład grupowanie elementów według długości ich nazwy wyglądałoby następująco:

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

Należy zauważyć, że $categoryItems nie jest zwykłą tablicą, ale obiektem, który zachowuje się jak iterator. Aby uzyskać dostęp do pierwszego elementu w grupie, można użyć funkcji first() function.

Ta elastyczność w grupowaniu danych sprawia, że group jest wyjątkowo przydatnym narzędziem do prezentacji danych w szablonach Latte.

Zagnieżdżone pętle

Załóżmy, że mamy tabelę bazy danych z inną kolumną subcategoryId, która definiuje podkategorie dla każdego elementu. Chcemy wyświetlić każdą główną kategorię na osobnej liście <ul> a każdą podkategorię na osobnej zagnieżdżonej liście <ol> liście:

{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}

Połączenie z bazą danych Nette

Pokażmy, jak efektywnie wykorzystać grupowanie danych w połączeniu z Nette Database. Załóżmy, że pracujemy z tabelą items z początkowego przykładu, która jest połączona przez kolumnę categoryId z tabelą categories:

categoryId name
1 Fruits
2 Języki
3 Kolory

Wczytujemy dane z tabeli items za pomocą polecenia Nette Database Explorer $items = $db->table('items'). Podczas iteracji po tych danych mamy możliwość nie tylko dostępu do atrybutów takich jak $item->name i $item->categoryId, ale dzięki połączeniu z tabelą categories, także do powiązanego z nią wiersza poprzez $item->category. To połączenie może wykazać interesujące zastosowania:

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

W tym przypadku używamy filtra |group do grupowania według połączonego wiersza $item->category, a nie tylko według kolumny categoryId. Daje nam to ActiveRow danej kategorii w kluczu zmiennej, co pozwala nam bezpośrednio wyświetlić jej nazwę za pomocą {$category->name}. Jest to praktyczny przykład tego, jak grupowanie może uprościć szablony i ułatwić obsługę danych.

Filtr |batch

Filtr umożliwia podzielenie listy elementów na grupy o z góry określonej liczbie elementów. Ten filtr jest idealny w sytuacjach, gdy chcesz przedstawić dane w kilku mniejszych grupach, na przykład dla lepszej przejrzystości lub wizualnej organizacji na stronie.

Wyobraźmy sobie, że mamy listę elementów i chcemy wyświetlić je na listach, z których każda zawiera maksymalnie trzy elementy. Użycie filtra |batch jest w takim przypadku bardzo praktyczne:

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

W tym przykładzie lista $items jest podzielona na mniejsze grupy, z których każda ($batch) zawiera maksymalnie trzy elementy. Każda grupa jest następnie wyświetlana na osobnej <ul> liście.

Jeśli ostatnia grupa nie zawiera wystarczającej liczby elementów, aby osiągnąć pożądaną liczbę, drugi parametr filtra pozwala określić, czym ta grupa zostanie uzupełniona. Jest to idealne rozwiązanie do estetycznego wyrównania elementów, w przypadku których niekompletny wiersz może wyglądać na nieuporządkowany.

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

Tag {iterateWhile}

Zademonstrujemy te same zadania, którymi zajmowaliśmy się z filtrem |group przy użyciu tagu {iterateWhile}. Główna różnica między tymi dwoma podejściami polega na tym, że group najpierw przetwarza i grupuje wszystkie dane wejściowe, podczas gdy {iterateWhile} kontroluje postęp cykli z warunkami, więc iteracja odbywa się sekwencyjnie.

Najpierw rysujemy tabelę z kategoriami za pomocą iterateWhile:

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

Podczas gdy {foreach} oznacza zewnętrzną część cyklu, tj. rysowanie list dla każdej kategorii, znacznik {iterateWhile} oznacza część wewnętrzną, tj. poszczególne elementy. Warunek w znaczniku end mówi, że powtarzanie będzie kontynuowane tak długo, jak bieżący i następny element należą do tej samej kategorii ($iterator->nextValue jest następnym elementem).

Gdyby warunek był zawsze spełniony, wszystkie elementy byłyby rysowane w wewnętrznym cyklu:

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

Wynik będzie wyglądał następująco:

<ul>
	<li>Apple</li>
	<li>Banana</li>
	<li>PHP</li>
	<li>Green</li>
	<li>Red</li>
	<li>Blue</li>
</ul>

Jaki jest pożytek z iterateWhile w ten sposób? Gdy tabela jest pusta i nie zawiera żadnych elementów, nie jest wypisywana pusta wartość <ul></ul> jest drukowany.

Jeśli określimy warunek w otwierającym tagu {iterateWhile}, zachowanie ulegnie zmianie: warunek (i przejście do następnego elementu) jest wykonywany na początku wewnętrznego cyklu, a nie na końcu. Tak więc, podczas gdy zawsze wchodzisz na {iterateWhile} bez warunków, wchodzisz na {iterateWhile $cond} tylko wtedy, gdy warunek $cond jest spełniony. W tym samym czasie następny element jest zapisywany w $item.

Jest to przydatne na przykład w sytuacji, gdy chcemy wyrenderować pierwszy element w każdej kategorii w inny sposób, na przykład w następujący sposób:

<h1>Apple</h1>
<ul>
	<li>Banana</li>
</ul>

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

<h1>Green</h1>
<ul>
	<li>Red</li>
	<li>Blue</li>
</ul>

Modyfikujemy oryginalny kod w taki sposób, że najpierw renderujemy pierwszy element, a następnie w wewnętrznym cyklu {iterateWhile} renderujemy pozostałe elementy z tej samej kategorii:

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

W ramach jednego cyklu możemy utworzyć wiele wewnętrznych pętli, a nawet je zagnieżdżać. W ten sposób można na przykład grupować podkategorie.

Załóżmy, że tabela ma jeszcze jedną kolumnę subcategoryId, a oprócz tego każda kategoria znajduje się w osobnej kolumnie. <ul>a każda podkategoria w osobnej <ol>:

{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}
wersja: 3.0