Wszystko, co chcieliście wiedzieć o grupowaniu

Podczas pracy z danymi w szablonach często można napotkać potrzebę ich grupowania lub specyficznego wyświetlania według określonych kryteriów. Latte oferuje w tym celu kilka potężnych narzędzi.

Filtr i funkcja |group umożliwiają efektywne grupowanie danych według podanego kryterium, filtr |batch ułatwia podział danych na ustalone partie, a znacznik {iterateWhile} zapewnia możliwość bardziej złożonego sterowania przebiegiem pętli z warunkami. Każdy z tych znaczników oferuje specyficzne możliwości pracy z danymi, co czyni je niezbędnymi narzędziami do dynamicznego i strukturalnego wyświetlania informacji w szablonach Latte.

Filtr i funkcja group

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

id categoryId name
1 1 Apple
2 1 Banana
3 2 PHP
4 3 Green
5 3 Red
6 3 Blue

Prosta lista wszystkich pozycji za pomocą szablonu Latte wyglądałaby tak:

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

Jeśli jednak chcielibyśmy, aby pozycje były uporządkowane w grupy według kategorii, musimy je podzielić tak, aby każda kategoria miała swoją własną listę. Wynik powinien wyglądać 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 można łatwo i elegancko rozwiązać za pomocą |group. Jako parametr podajemy categoryId, co oznacza, że pozycje zostaną podzielone na mniejsze tablice według wartości $item->categoryId (jeśli $item byłoby tablicą, użyłoby się $item['categoryId']):

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

Filtr można w Latte użyć również jako funkcję, co daje nam alternatywną składnię: {foreach group($items, categoryId) ...}.

Jeśli chcesz grupować pozycje według bardziej złożonych kryteriów, możesz w parametrze filtra użyć funkcji. Na przykład, grupowanie pozycji według długości nazwy wyglądałoby tak:

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

Ważne jest, aby pamiętać, że $categoryItems nie jest zwykłą tablicą, ale obiektem, który zachowuje się jak iterator. Aby uzyskać dostęp do pierwszej pozycji grupy, możesz użyć funkcji first().

Ta elastyczność w grupowaniu danych czyni group wyjątkowo użytecznym narzędziem do prezentacji danych w szablonach Latte.

Zagnieżdżone pętle

Wyobraźmy sobie, że mamy tabelę bazy danych z dodatkową kolumną subcategoryId, która definiuje podkategorie poszczególnych pozycji. Chcemy wyświetlić każdą główną kategorię w osobnej liście <ul> i każdą podkategorię w osobnej zagnieżdżonej liście <ol>:

{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 Nette Database

Pokażmy, jak efektywnie wykorzystać grupowanie danych w połączeniu z Nette Database. Załóżmy, że pracujemy z tabelą items z przykładu wprowadzającego, która jest za pośrednictwem kolumny categoryId połączona z tą tabelą categories:

categoryId name
1 Fruits
2 Languages
3 Colors

Dane z tabeli items wczytamy za pomocą Nette Database Explorer poleceniem $items = $db->table('items'). Podczas iteracji nad tymi danymi mamy możliwość dostępu nie tylko do atrybutów jak $item->name i $item->categoryId, ale dzięki połączeniu z tabelą categories również do powiązanego wiersza w niej przez $item->category. Na tym połączeniu można zademonstrować ciekawe wykorzystanie:

{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. Dzięki temu w zmiennej klucza mamy bezpośrednio ActiveRow danej kategorii, co pozwala nam bezpośrednio wypisywać jej nazwę za pomocą {$category->name}. Jest to praktyczny przykład, jak grupowanie może uczynić szablony bardziej przejrzystymi i ułatwić pracę z danymi.

Filtr |batch

Filtr umożliwia podział listy elementów na grupy o z góry określonej liczbie elementów. Ten filtr jest idealny w sytuacjach, gdy chcesz prezentować dane w wielu mniejszych grupach, na przykład dla lepszej przejrzystości lub wizualnego uporządkowania na stronie.

Wyobraźmy sobie, że mamy listę pozycji i chcemy je wyświetlić w listach, gdzie każda zawiera maksymalnie trzy pozycje. 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, przy czym każda grupa ($batch) zawiera do trzech pozycji. Każda grupa jest następnie wyświetlana w osobnej liście <ul>.

Jeśli ostatnia grupa nie zawiera wystarczającej liczby elementów do osiągnięcia wymaganej liczby, drugi parametr filtra pozwala zdefiniować, czym ta grupa zostanie uzupełniona. Jest to idealne do estetycznego wyrównania elementów tam, gdzie niekompletny rząd mógłby wyglądać nieuporządkowanie.

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

Znacznik {iterateWhile}

Te same zadania, które rozwiązywaliśmy za pomocą filtra |group, pokażemy z użyciem znacznika {iterateWhile}. Główna różnica między oboma podejściami polega na tym, że group najpierw przetwarza i grupuje wszystkie dane wejściowe, podczas gdy {iterateWhile} steruje przebiegiem pętli z warunkami, więc iteracja odbywa się stopniowo.

Najpierw wyrenderujemy 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ęść pętli, czyli renderowanie list dla każdej kategorii, znacznik {iterateWhile} oznacza wewnętrzną część, czyli poszczególne pozycje. Warunek w końcowym znaczniku mówi, że powtarzanie będzie trwało, dopóki bieżący i następny element należą do tej samej kategorii ($iterator->nextValue jest następnym elementem).

Gdyby warunek był zawsze spełniony, to w wewnętrznej pętli wyrenderowałyby się wszystkie elementy:

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

Wynik będzie wyglądał tak:

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

Do czego jest dobre takie użycie iterateWhile? Gdy tabela będzie pusta i nie będzie zawierać żadnych elementów, nie wypisze się puste <ul></ul>.

Jeśli podamy warunek w otwierającym znaczniku {iterateWhile}, to zachowanie się zmieni: warunek (i przejście do następnego elementu) wykona się już na początku wewnętrznej pętli, a nie na końcu. Czyli podczas gdy do {iterateWhile} bez warunku wejdzie się zawsze, do {iterateWhile $cond} tylko przy spełnieniu warunku $cond. A jednocześnie do $item zapisze się następny element.

Co przydaje się na przykład w sytuacji, gdy będziemy chcieli pierwszy element w każdej kategorii wyrenderować w inny sposób, na przykład tak:

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

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

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

Oryginalny kod zmodyfikujemy tak, że najpierw wyrenderujemy pierwszą pozycję, a następnie w wewnętrznej pętli {iterateWhile} wyrenderujemy kolejne pozycje 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 jednej pętli możemy tworzyć więcej wewnętrznych pętli i nawet je zagnieżdżać. W ten sposób można by grupować na przykład podkategorie itd.

Załóżmy, że w tabeli będzie jeszcze dodatkowa kolumna subcategoryId i oprócz tego, że każda kategoria będzie w osobnym <ul>, każda podkategoria w osobnym <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