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}