Всичко, което винаги сте искали да знаете за групирането

Когато работите с данни в шаблони, често се сблъсквате с необходимостта да ги групирате или да ги показвате конкретно по определени критерии. За тази цел Latte предлага няколко мощни инструмента.

Филтърът и функцията |group позволяват ефективно групиране на данните въз основа на определени критерии, докато филтърът |batch улеснява разделянето на данните на фиксирани партиди, а етикетът {iterateWhile} предоставя възможност за по-сложен контрол на циклите с условия. Всеки от тези тагове предлага специфични възможности за работа с данни, което ги прави незаменими инструменти за динамично и структурирано показване на информация в шаблоните Latte.

Филтър и функция group

Представете си таблица от базата данни items с елементи, разделени на категории:

id categoryId name
1 1 Apple
2 1 Банан
3 2 PHP
4 3 Зелен
5 3 Червено
6 3 Синьо

Един прост списък на всички елементи, използващ шаблон 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

Нека да покажем как ефективно да използваме групирането на данни в комбинация с Nette Database. Да предположим, че работим с таблицата items от първоначалния пример, която е свързана чрез колоната categoryId с тази таблица categories:

categoryId name
1 Fruits
2 Езици
3 Цветове

Зареждаме данни от таблицата 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} маркира вътрешната част, т.е. отделните елементи. Условието в тага end казва, че повторението ще продължи, докато текущият и следващият елемент принадлежат към една и съща категория ($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}
версия: 3.0