Everything You Always Wanted to Know About Grouping

When working with data in templates, you often encounter the need to group them or display them specifically according to certain criteria. Latte offers several powerful tools for this purpose.

The filter and function |group allow for efficient data grouping based on specified criteria, the |batch filter facilitates splitting data into fixed-size batches, and the {iterateWhile} tag provides the ability to control loop progression with more complex conditions. Each of these features offers specific options for working with data, making them indispensable tools for dynamic and structured display of information in Latte templates.

Filter and Function group

Imagine a database table items with items divided into categories:

id categoryId name
1 1 Apple
2 1 Banana
3 2 PHP
4 3 Green
5 3 Red
6 3 Blue

A simple list of all items using a Latte template would look like this:

<ul>
{foreach $items as $item}
	<li>{$item->name}</li>
{/foreach}
</ul>

However, if we wanted the items organized into groups by category, we need to divide them so that each category has its own list. The desired result would look like this:

<ul>
	<li>Apple</li>
	<li>Banana</li>
</ul>

<ul>
	<li>PHP</li>
</ul>

<ul>
	<li>Green</li>
	<li>Red</li>
	<li>Blue</li>
</ul>

This task can be easily and elegantly solved using |group. We specify categoryId as the parameter, meaning the items will be divided into smaller arrays based on the value of $item->categoryId (if $item were an array, $item['categoryId'] would be used):

{foreach ($items|group: categoryId) as $categoryId => $categoryItems}
	<ul>
		{foreach $categoryItems as $item}
			<li>{$item->name}</li>
		{/foreach}
	</ul>
{/foreach}

The filter can also be used as a function in Latte, providing an alternative syntax: {foreach group($items, categoryId) ...}.

If you want to group items based on more complex criteria, you can use a function in the filter parameter. For example, grouping items by the length of their name would look like this:

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

It’s important to note that $categoryItems is not a regular array, but an object that behaves like an iterator. To access the first item in the group, you can use the first() function.

This flexibility in data grouping makes group an exceptionally useful tool for presenting data in Latte templates.

Nested Loops

Let's imagine our database table has an additional column subcategoryId, defining subcategories for each item. We want to display each main category in a separate <ul> list and each subcategory within that main category in a separate nested <ol> list:

{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}

Integration with Nette Database

Let's demonstrate how to effectively use data grouping in combination with Nette Database. Assume we are working with the items table from the introductory example, connected via the categoryId column to this categories table:

categoryId name
1 Fruits
2 Languages
3 Colors

We load data from the items table using Nette Database Explorer with the command $items = $db->table('items'). While iterating over this data, we can access not only attributes like $item->name and $item->categoryId, but thanks to the relationship with the categories table, also the related row via $item->category. This relationship allows for interesting applications:

{foreach ($items|group: category) as $category => $categoryItems}
	<h1>{$category->name}</h1>
	<ul>
		{foreach $categoryItems as $item}
			<li>{$item->name}</li>
		{/foreach}
	</ul>
{/foreach}

In this case, we use the |group filter to group by the related row object $item->category, not just the categoryId column. As a result, the key variable $category directly holds the ActiveRow object for that category, allowing us to display its name directly using {$category->name}. This is a practical example of how grouping can simplify templates and facilitate working with related data.

Filter |batch

The |batch filter allows you to divide a list of items into groups (batches) with a predetermined number of items. This filter is ideal for situations where you want to present data in several smaller chunks, for example, for better clarity or visual layout on the page.

Imagine we have a list of items and want to display them in lists, where each list contains a maximum of three items. Using the |batch filter is very practical in such a case:

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

In this example, the $items list is divided into smaller groups, where each group ($batch) contains up to three items. Each batch is then displayed in a separate <ul> list.

If the last group does not contain enough elements to reach the desired number, the second parameter of the filter allows you to define what this group will be supplemented with. This is ideal for aesthetically aligning elements where an incomplete row might look disordered.

{foreach ($items|batch: 3, '—') as $batch}
	...
{/foreach}

Tag {iterateWhile}

We will demonstrate the same tasks addressed with the |group filter using the {iterateWhile} tag. The main difference between the two approaches is that |group first processes and groups all input data, whereas {iterateWhile} controls the loop's progression based on conditions, allowing iteration to proceed sequentially.

First, let's render the table with categories using iterateWhile:

{foreach $items as $item}
	<ul>
		{iterateWhile}
			<li>{$item->name}</li>
		{/iterateWhile $item->categoryId === $iterator->nextValue?->categoryId}
	</ul>
{/foreach}

While {foreach} marks the outer part of the cycle, i.e., drawing lists for each category, the {iterateWhile} tag marks the inner part, i.e., individual items. The condition in the end tag says that repetition will continue as long as the current and next element belong to the same category ($iterator->nextValue is the next item).

If the condition were always true, all elements would be rendered within the first <ul>:

{foreach $items as $item}
	<ul>
		{iterateWhile}
			<li>{$item->name}
		{/iterateWhile true}
	</ul>
{/foreach}

The result would look like this:

<ul>
	<li>Apple</li>
	<li>Banana</li>
	<li>PHP</li>
	<li>Green</li>
	<li>Red</li>
	<li>Blue</li>
</ul>

What's the benefit of using iterateWhile like this? If the $items array is empty, no empty <ul></ul> tags will be printed.

If we specify the condition in the opening {iterateWhile} tag, the behavior changes: the condition (and transition to the next element) is performed at the beginning of the inner cycle, not at the end. Thus, while you always enter {iterateWhile} without conditions, you enter {iterateWhile $cond} only when the condition $cond is met. And at the same time, the next element is written into $item.

This is useful, for instance, when you want to render the first item in each category differently, like this:

<h1>Apple</h1>
<ul>
	<li>Banana</li>
</ul>

<h1>PHP</h1>
<ul>
</ul>

<h1>Green</h1>
<ul>
	<li>Red</li>
	<li>Blue</li>
</ul>

We modify the original code to first render the item as a heading, and then use the inner {iterateWhile} loop to render subsequent items from the same category as list items:

{foreach $items as $item}
	<h1>{$item->name}</h1>
	<ul>
		{iterateWhile $item->categoryId === $iterator->nextValue?->categoryId}
			<li>{$item->name}</li>
		{/iterateWhile}
	</ul>
{/foreach}

Within a single {foreach} loop, you can create multiple inner {iterateWhile} loops and even nest them. This could be used, for example, to group subcategories.

Let's assume the table has another column subcategoryId, and besides having each category in a separate <ul>, each subcategory should be in a separate <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}
version: 3.0