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

Při práci s daty ve šablonách můžete často narazit na potřebu jejich seskupování nebo specifického zobrazení podle určitých kritérií. Latte pro tento účel nabízí hned několik silných nástrojů.

Filtr a funkce |group umožňují efektivní seskupení dat podle zadaného kritéria, filtr |batch zase usnadňuje rozdělení dat do pevně daných dávek a značka {iterateWhile} poskytuje možnost složitějšího řízení průběhu cyklů s podmínkami. Každá z těchto značek nabízí specifické možnosti pro práci s daty, čímž se stávají nepostradatelnými nástroji pro dynamické a strukturované zobrazení informací v Latte šablonách.

Filtr a funkce group

Představte si databázovou tabulku items s položkami rozdělenou do 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}

Filtr lze v Latte použít i jako funkci, což nám dává alternativní syntaxi: {foreach group($items, categoryId) ...}.

Chcete-li seskupovat položky podle složitějších kritérií, můžete v parametru filtru použít funkci. Například, seskupení položek podle délky názvu by vypadalo takto:

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

Je důležité si uvědomit, že $categoryItems není běžné pole, ale objekt, který se chová jako iterátor. Pro přístup k první položce skupiny můžete použít funkci first().

Tato flexibilita v seskupování dat činí group výjimečně užitečným nástrojem pro prezentaci dat v šablonách Latte.

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}

Spojení 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 v proměnné klíči přímo ActiveRow dané kategorie, což nám umožňuje přímo vypisovat její název pomocí {$category->name}. Toto je praktický příklad, jak může seskupování zpřehlednit šablony a usnadnit práci s daty.

Filtr |batch

Filtr umožňuje rozdělit seznam prvků do skupin s předem určeným počtem prvků. Tento filtr je ideální pro situace, kdy chcete data prezentovat ve více menších skupinách, například pro lepší přehlednost nebo vizuální uspořádání na stránce.

Představme si, že máme seznam položek a chceme je zobrazit v seznamech, kde každý obsahuje maximálně tři položky. Použití filtru |batch je v takovém případě velmi praktické:

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

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ěhu cyklů s podmínkami, takže 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).

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é? Když bude tabulka prázdná a nebude obsahovat žádné prvky, nevypíše se prázdné <ul></ul>.

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.

Což se hodí například v situaci, kdy budeme chtít první prvek v každé kategorii vykreslit jiným způsobem, 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>

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. Takto by se daly seskupovat třeba podkategorie atd.

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á každý podkategorie 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