Всичко, което някога сте искали да знаете за групирането
При работа с данни в шаблони често можете да срещнете нуждата от тяхното групиране или специфично показване според определени критерии. Latte за тази цел предлага няколко силни инструмента.
Филтърът и функцията |group позволяват ефективно групиране на
данни според зададен критерий, филтърът |batch пък улеснява
разделянето на данни на предварително зададени партиди, а тагът
{iterateWhile} предоставя възможност за по-сложно управление на
протичането на цикли с условия. Всеки от тези тагове предлага
специфични възможности за работа с данни, което ги прави незаменими
инструменти за динамично и структурирано показване на информация в Latte
шаблони.
Филтър и функция group
Представете си таблица в база данни items с елементи, разделени
на категории:
| id | categoryId | name |
|---|---|---|
| 1 | 1 | Apple |
| 2 | 1 | Banana |
| 3 | 2 | PHP |
| 4 | 3 | Green |
| 5 | 3 | Red |
| 6 | 3 | Blue |
Прост списък на всички елементи с помощта на Latte шаблон би изглеждал така:
<ul>
{foreach $items as $item}
<li>{$item->name}</li>
{/foreach}
</ul>
Ако обаче искахме елементите да бъдат подредени в групи според категорията, трябва да ги разделим така, че всяка категория да има свой собствен списък. Резултатът тогава трябва да изглежда по следния начин:
<ul>
<li>Apple</li>
<li>Banana</li>
</ul>
<ul>
<li>PHP</li>
</ul>
<ul>
<li>Green</li>
<li>Red</li>
<li>Blue</li>
</ul>
Задачата може лесно и елегантно да се реши с помощта на |group.
Като параметър посочваме categoryId, което означава, че елементите
ще се разделят на по-малки масиви според стойността на
$item->categoryId (ако $item беше масив, ще се използва
$item['categoryId']):
{foreach ($items|group: categoryId) as $categoryId => $categoryItems}
<ul>
{foreach $categoryItems as $item}
<li>{$item->name}</li>
{/foreach}
</ul>
{/foreach}
Филтърът може в Latte да се използва и като функция, което ни дава
алтернативен синтаксис: {foreach group($items, categoryId) ...}.
Ако искате да групирате елементи според по-сложни критерии, можете в параметъра на филтъра да използвате функция. Например, групиране на елементи според дължината на името би изглеждало така:
{foreach ($items|group: fn($item) => strlen($item->name)) as $items}
...
{/foreach}
Важно е да се осъзнае, че $categoryItems не е обикновен масив, а обект,
който се държи като итератор. За достъп до първия елемент на групата
можете да използвате функцията first().
Тази гъвкавост в групирането на данни прави group изключително
полезен инструмент за представяне на данни в шаблони Latte.
Вложени цикли
Представете си, че имаме база данни с допълнителна колона
subcategoryId, която дефинира подкатегориите на отделните елементи.
Искаме да покажем всяка основна категория в отделен списък
<ul> и всяка подкатегория в отделен вложен списък
<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}
Връзка с Nette Database
Нека покажем как ефективно да използваме групирането на данни в
комбинация с Nette Database. Да предположим, че работим с таблицата items
от уводния пример, която чрез колоната categoryId е свързана с тази
таблица categories:
| categoryId | name |
|---|---|
| 1 | Fruits |
| 2 | Languages |
| 3 | Colors |
Данните от таблицата items зареждаме с помощта на Nette Database Explorer с
командата $items = $db->table('items'). По време на итерацията над тези
данни имаме възможност да достъпваме не само атрибути като
$item->name и $item->categoryId, но благодарение на връзката с
таблицата categories също и свързания ред в нея чрез
$item->category. На тази връзка може да се демонстрира интересно
използване:
{foreach ($items|group: category) as $category => $categoryItems}
<h1>{$category->name}</h1>
<ul>
{foreach $categoryItems as $item}
<li>{$item->name}</li>
{/foreach}
</ul>
{/foreach}
В този случай използваме филтъра |group за групиране според
свързания ред $item->category, а не само според колоната categoryId.
Благодарение на това в променливата ключ имаме директно ActiveRow
на дадената категория, което ни позволява директно да изписваме
нейното име с {$category->name}. Това е практичен пример за това как
групирането може да изясни шаблоните и да улесни работата с данни.
Филтър |batch
Филтърът позволява да се раздели списък от елементи на групи с предварително определен брой елементи. Този филтър е идеален за ситуации, когато искате да представите данните в няколко по-малки групи, например за по-добра прегледност или визуално подреждане на страницата.
Представете си, че имаме списък с елементи и искаме да ги покажем в
списъци, където всеки съдържа максимум три елемента. Използването на
филтъра |batch в такъв случай е много практично:
<ul>
{foreach ($items|batch: 3) as $batch}
{foreach $batch as $item}
<li>{$item->name}</li>
{/foreach}
{/foreach}
</ul>
В този пример списъкът $items е разделен на по-малки групи, като
всяка група ($batch) съдържа до три елемента. Всяка група след това
се показва в отделен <ul> списък.
Ако последната група не съдържа достатъчно елементи за достигане на желания брой, вторият параметър на филтъра позволява да се дефинира с какво ще бъде допълнена тази група. Това е идеално за естетическо подравняване на елементите там, където непълният ред би могъл да изглежда неподреден.
{foreach ($items|batch: 3, '—') as $batch}
...
{/foreach}
Таг {iterateWhile}
Същите задачи, които решавахме с филтъра |group, ще покажем с
използването на тага {iterateWhile}. Основната разлика между двата
подхода е в това, че group първо обработва и групира всички входни
данни, докато {iterateWhile} управлява протичането на цикли с условия,
така че итерацията протича постепенно.
Първо ще рендираме таблицата с категориите с помощта на iterateWhile:
{foreach $items as $item}
<ul>
{iterateWhile}
<li>{$item->name}</li>
{/iterateWhile $item->categoryId === $iterator->nextValue->categoryId}
</ul>
{/foreach}
Докато {foreach} обозначава външната част на цикъла, т.е.
рендирането на списъци за всяка категория, тагът {iterateWhile}
обозначава вътрешната част, т.е. отделните елементи. Условието в
крайния таг казва, че повторението ще продължи дотогава, докато
текущият и следващият елемент принадлежат към същата категория
($iterator->nextValue е следващият
елемент).
Ако условието беше изпълнено винаги, тогава във вътрешния цикъл ще се рендират всички елементи:
{foreach $items as $item}
<ul>
{iterateWhile}
<li>{$item->name}
{/iterateWhile true}
</ul>
{/foreach}
Резултатът ще изглежда така:
<ul>
<li>Apple</li>
<li>Banana</li>
<li>PHP</li>
<li>Green</li>
<li>Red</li>
<li>Blue</li>
</ul>
За какво е полезно такова използване на iterateWhile? Когато таблицата е
празна и не съдържа никакви елементи, няма да се изпише празно
<ul></ul>.
Ако посочим условие в отварящия таг {iterateWhile}, тогава
поведението се променя: условието (и преходът към следващия елемент) се
изпълнява още в началото на вътрешния цикъл, а не в края. Тоест, докато в
{iterateWhile} без условие се влиза винаги, в {iterateWhile $cond} само
при изпълнение на условието $cond. И същевременно с това в
$item се записва следващият елемент.
Което е полезно например в ситуация, когато искаме първия елемент във всяка категория да рендираме по различен начин, например така:
<h1>Apple</h1>
<ul>
<li>Banana</li>
</ul>
<h1>PHP</h1>
<ul>
</ul>
<h1>Green</h1>
<ul>
<li>Red</li>
<li>Blue</li>
</ul>
Ще променим оригиналния код така, че първо да рендираме първия
елемент и след това във вътрешния цикъл {iterateWhile} да рендираме
другите елементи от същата категория:
{foreach $items as $item}
<h1>{$item->name}</h1>
<ul>
{iterateWhile $item->categoryId === $iterator->nextValue->categoryId}
<li>{$item->name}</li>
{/iterateWhile}
</ul>
{/foreach}
В рамките на един цикъл можем да създаваме повече вътрешни цикли и дори да ги влагаме. Така биха могли да се групират например подкатегории и т.н.
Да предположим, че в таблицата има още една колона subcategoryId и
освен това, че всяка категория ще бъде в отделен <ul>, всяка
подкатегория в отделен <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}