Tudo o que você sempre quis saber sobre agrupamento

Ao trabalhar com dados em templates, você pode frequentemente encontrar a necessidade de agrupá-los ou exibi-los especificamente de acordo com certos critérios. Latte oferece várias ferramentas poderosas para este propósito.

O filtro e a função |group permitem agrupar dados eficientemente de acordo com um critério especificado, o filtro |batch facilita a divisão de dados em lotes fixos, e a tag {iterateWhile} fornece a capacidade de controlar de forma mais complexa o fluxo de loops com condições. Cada uma dessas tags oferece possibilidades específicas para trabalhar com dados, tornando-as ferramentas indispensáveis para a exibição dinâmica e estruturada de informações nos templates Latte.

Filtro e função group

Imagine uma tabela de banco de dados items com itens divididos em categorias:

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

Uma lista simples de todos os itens usando um template Latte seria assim:

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

No entanto, se quiséssemos que os itens fossem organizados em grupos por categoria, precisaríamos dividi-los de forma que cada categoria tivesse sua própria lista. O resultado deveria então ser o seguinte:

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

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

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

A tarefa pode ser resolvida fácil e elegantemente usando |group. Como parâmetro, especificamos categoryId, o que significa que os itens serão divididos em arrays menores com base no valor de $item->categoryId (se $item fosse um array, seria usado $item['categoryId']):

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

O filtro também pode ser usado em Latte como uma função, o que nos dá uma sintaxe alternativa: {foreach group($items, categoryId) ...}.

Se você quiser agrupar itens com base em critérios mais complexos, pode usar uma função no parâmetro do filtro. Por exemplo, agrupar itens pelo comprimento do nome seria assim:

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

É importante notar que $categoryItems não é um array comum, mas um objeto que se comporta como um iterador. Para acessar o primeiro item do grupo, você pode usar a função first().

Essa flexibilidade no agrupamento de dados torna group uma ferramenta excepcionalmente útil para apresentar dados nos templates Latte.

Loops aninhados

Imagine que temos uma tabela de banco de dados com outra coluna subcategoryId, que define das subcategorias dos itens individuais. Queremos exibir cada categoria principal em uma lista <ul> separada e cada subcategoria em uma lista aninhada <ol> separada:

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

Conexão com Nette Database

Vamos mostrar como usar eficientemente o agrupamento de dados em combinação com a Nette Database. Suponha que estamos trabalhando com a tabela items do exemplo introdutório, que está conectada através da coluna categoryId a esta tabela categories:

categoryId name
1 Fruits
2 Languages
3 Colors

Carregamos os dados da tabela items usando o Nette Database Explorer com o comando $items = $db->table('items'). Durante a iteração sobre esses dados, temos a possibilidade de acessar não apenas atributos como $item->name e $item->categoryId, mas também, graças à conexão com a tabela categories, a linha relacionada nela através de $item->category. Nesta conexão, podemos demonstrar um uso interessante:

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

Neste caso, usamos o filtro |group para agrupar pela linha conectada $item->category, e não apenas pela coluna categoryId. Graças a isso, na variável chave, temos diretamente o ActiveRow da categoria dada, o que nos permite exibir diretamente seu nome usando {$category->name}. Este é um exemplo prático de como o agrupamento pode tornar os templates mais claros e facilitar o trabalho com dados.

Filtro |batch

O filtro permite dividir uma lista de elementos em grupos com um número predeterminado de elementos. Este filtro é ideal para situações em que você deseja apresentar dados em vários grupos menores, por exemplo, para melhor clareza ou organização visual na página.

Imagine que temos uma lista de itens e queremos exibi-los em listas, onde cada uma contém no máximo três itens. O uso do filtro |batch é muito prático nesse caso:

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

Neste exemplo, a lista $items é dividida em grupos menores, onde cada grupo ($batch) contém até três itens. Cada grupo é então exibido em uma lista <ul> separada.

Se o último grupo não contiver elementos suficientes para atingir o número desejado, o segundo parâmetro do filtro permite definir com o que este grupo será preenchido. Isso é ideal para o alinhamento estético de elementos onde uma linha incompleta poderia parecer desorganizada.

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

Tag {iterateWhile}

As mesmas tarefas que resolvemos com o filtro |group, mostraremos usando a tag {iterateWhile}. A principal diferença entre as duas abordagens é que group primeiro processa e agrupa todos os dados de entrada, enquanto {iterateWhile} controla o fluxo dos loops com condições, de modo que a iteração ocorre progressivamente.

Primeiro, renderizamos a tabela com categorias usando iterateWhile:

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

Enquanto {foreach} marca a parte externa do loop, ou seja, a renderização das listas para cada categoria, a tag {iterateWhile} marca a parte interna, ou seja, os itens individuais. A condição na tag de fechamento diz que a repetição continuará enquanto o elemento atual e o seguinte pertencerem à mesma categoria ($iterator->nextValue é o próximo item).

Se a condição fosse sempre verdadeira, todos os elementos seriam renderizados no loop interno:

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

O resultado seria assim:

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

Para que serve tal uso de iterateWhile? Se a tabela estiver vazia e não contiver nenhum elemento, o <ul></ul> vazio não será impresso.

Se especificarmos a condição na tag de abertura {iterateWhile}, o comportamento muda: a condição (e a transição para o próximo elemento) é executada já no início do loop interno, e não no final. Ou seja, enquanto se entra sempre em {iterateWhile} sem condição, entra-se em {iterateWhile $cond} apenas se a condição $cond for atendida. E, ao mesmo tempo, o próximo elemento é atribuído a $item.

Isso é útil, por exemplo, na situação em que queremos renderizar o primeiro elemento de cada categoria de forma diferente, por exemplo, assim:

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

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

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

Modificamos o código original para que primeiro renderizemos o primeiro item e depois, no loop interno {iterateWhile}, renderizemos os outros itens da mesma categoria:

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

Dentro de um único loop, podemos criar vários loops internos e até mesmo aninhá-los. Desta forma, poderíamos agrupar subcategorias, etc.

Digamos que na tabela haja outra coluna subcategoryId e, além de cada categoria estar em um <ul> separado, cada subcategoria estará em um <ol> separado:

{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}
versão: 3.0