Všechno, co jste kdy chtěli vědět o seskupování

Při práci s daty ve šablonách často potřebujete položky seskupit, rozdělit do dávek nebo je procházet podle podmínky. Latte k tomu nabízí tři nástroje, z nichž každý se hodí na trochu jinou situaci.

Filtr |group a funkce group() seskupí položky podle zadaného kritéria, filtr |batch je rozdělí do dávek pevné velikosti a značka {iterateWhile} prochází data postupně a sama si určuje, kdy přerušit vnitřní smyčku. V textu si je postupně projdeme.

Filtr a funkce group

Nástroj lze používat ve dvou tvarech: jako filtr $items|group: … nebo jako funkci group($items, …). Sémanticky jsou ekvivalentní, vyberte si podle čitelnosti.

Představte si databázovou tabulku items, jejíž položky patří do různých kategorií:

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

Jednoduchý seznam všech položek pomocí Latte šablony by vypadal takto:

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

Pokud bychom ale chtěli, aby položky byly uspořádány do skupin podle kategorie, potřebujeme je rozdělit tak, že každá kategorie bude mít svůj vlastní seznam. Výsledek by pak měl vypadat následovně:

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

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

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

Úkol se dá snadno a elegantně vyřešit pomocí |group. Jako parametr uvedeme categoryId, což znamená, že se položky rozdělí do menších polí podle hodnoty $item->categoryId (pokud by $item bylo pole, použije se $item['categoryId']):

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

Chcete-li seskupovat položky podle složitějších kritérií, můžete v parametru filtru použít funkci. Klíčem každé skupiny pak bude návratová hodnota funkce — například při seskupení podle délky názvu to bude počet znaků:

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

Je důležité si uvědomit, že každá skupina (tedy i $categoryItems) není běžné pole, ale objekt chovající se jako iterátor — nelze proto použít $categoryItems[0] ani count($categoryItems). Pro přístup k první položce skupiny použijte funkci first().

Tato flexibilita činí |group výjimečně užitečným nástrojem pro prezentaci dat.

Vnořené smyčky

Představme si, že máme databázovou tabulku s dalším sloupcem subcategoryId, který definuje podkategorie jednotlivých položek. Chceme zobrazit každou hlavní kategorii v samostatném seznamu <ul> a každou podkategorii v samostatném vnořeném seznamu <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}

Společně s Nette Database

Pojďme si ukázat, jak efektivně využít seskupování dat v kombinaci s Nette Database. Předpokládejme, že pracujeme s tabulkou items z úvodního příkladu, která je prostřednictvím sloupce categoryId spojená s touto tabulkou categories:

categoryId name
1 Fruits
2 Languages
3 Colors

Data z tabulky items načteme pomocí Nette Database Explorer příkazem $items = $db->table('items'). Během iterace nad těmito daty máme možnost přistupovat nejen k atributům jako $item->name a $item->categoryId, ale díky propojení s tabulkou categories také k souvisejícímu řádku v ní přes $item->category. Na tomto propojení lze demonstrovat zajímavé využití:

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

V tomto případě používáme filtr |group k seskupení podle propojeného řádku $item->category, nikoliv jen dle sloupce categoryId. Díky tomu je v klíči ($category) rovnou ActiveRow dané kategorie, což nám umožňuje vypisovat její název pomocí {$category->name} a přistupovat k libovolnému dalšímu sloupci, aniž bychom museli dělat zvláštní dotaz na categories.

Filtr |batch

Filtr rozdělí seznam prvků do dávek o pevně daném počtu. Hodí se třeba pro grid layout, sloupcové rozložení nebo jakékoli vizuální seskupení.

Představme si, že chceme zobrazit položky v seznamech, kde každý obsahuje maximálně tři položky:

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

V tomto příkladu je seznam $items rozdělen do menších skupin, přičemž každá skupina ($batch) obsahuje až tři položky. Každá skupina je poté zobrazena v samostatném <ul> seznamu.

Pokud poslední skupina neobsahuje dostatek prvků k dosažení požadovaného počtu, druhý parametr filtru umožňuje definovat, čím bude tato skupina doplněna. To je ideální pro estetické zarovnání prvků tam, kde by neúplná řada mohla působit neuspořádaně.

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

Značka {iterateWhile}

Stejné úkoly, jako jsme řešili s filtrem |group, si ukážeme s použitím značky {iterateWhile}. Hlavní rozdíl mezi oběma přístupy je v tom, že |group nejprve zpracuje a seskupí všechna vstupní data, zatímco {iterateWhile} řídí průběh cyklu pomocí podmínky a iterace probíhá postupně.

Nejprve vykreslíme tabulku s kategoriemi pomocí {iterateWhile}:

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

Zatímco {foreach} označuje vnější část cyklu, tedy vykreslování seznamů pro každou kategorii, tak značka {iterateWhile} označuje vnitřní část, tedy jednotlivé položky. Podmínka v koncové značce říká, že opakování bude probíhat do té doby, dokud aktuální i následující prvek patří do stejné kategorie ($iterator->nextValue je následující položka; u posledního prvku je null a porovnání pak vyjde false, takže vnitřní cyklus přirozeně skončí).

Kdyby podmínka byla splněná vždy, tak se ve vnitřním cyklu vykreslí všechny prvky:

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

Výsledek bude vypadat takto:

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

K čemu je takové použití {iterateWhile} dobré? Tím, že je <ul> uvnitř vnějšího {foreach}, se při prázdném vstupu nevykreslí vůbec nic — žádný osamělý <ul></ul>. Bez {iterateWhile} byste totéž museli ošetřit {if} před otevřením tagu nebo přes {foreachelse}.

Pokud uvedeme podmínku v otevírací značce {iterateWhile}, tak se chování změní: podmínka (a přechod na další prvek) se vykoná už na začátku vnitřního cyklu, nikoliv na konci. Tedy zatímco do {iterateWhile} bez podmínky se vstoupí vždy, do {iterateWhile $cond} jen při splnění podmínky $cond. A zároveň se s tím do $item zapíše následující prvek.

Hodí se to v situaci, kdy chceme první prvek v každé kategorii vykreslit jiným způsobem než ty ostatní, například takto:

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

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

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

(Prázdné <ul></ul> u kategorie PHP je tu jen ilustrací mechaniky — v reálném kódu byste vykreslení <ul> ošetřili {if}.)

Původní kód upravíme tak, že nejprve vykreslíme první položku a poté ve vnitřním cyklu {iterateWhile} vykreslíme další položky ze stejné kategorie:

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

V rámci jednoho cyklu můžeme vytvářet více vnitřních smyček a dokonce je zanořovat. Tímto způsobem lze seskupovat na více úrovních současně — třeba podkategorie pod kategoriemi.

Dejme tomu, že v tabulce bude ještě další sloupec subcategoryId a kromě toho, že každá kategorie bude v samostatném <ul>, každá podkategorie bude v samostatném <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}
verze: 3.0