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

При работе с данными в шаблонах вы часто сталкиваетесь с необходимостью группировать их или отображать в соответствии с определенными критериями. Для этого Latte предлагает несколько мощных инструментов.

Фильтр и функция |group позволяют эффективно группировать данные по заданным критериям, фильтр |batch облегчает разбивку данных на фиксированные партии, а тег {iterateWhile} предоставляет возможность более сложного управления циклами с помощью условий. Каждый из этих тегов предлагает особые возможности для работы с данными, что делает их незаменимыми инструментами для динамичного и структурированного отображения информации в шаблонах Latte.

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

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

id categoryId name
1 1 Яблоко
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) ...}.

Если вы хотите сгруппировать элементы по более сложным критериям, вы можете использовать функцию в параметре filter. Например, группировка элементов по длине их имени будет выглядеть следующим образом:

{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 Фрукты
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