Tudo o que você sempre quis saber sobre agrupamento

Ao trabalhar com dados em modelos, você frequentemente se depara com a necessidade de agrupá-los ou exibi-los especificamente de acordo com determinados critérios. Para essa finalidade, o Latte oferece várias ferramentas poderosas.

O filtro e a função |group permitem o agrupamento eficiente de dados com base em critérios especificados, enquanto o filtro |batch facilita a divisão de dados em lotes fixos e a tag {iterateWhile} oferece a possibilidade de controle de ciclo mais complexo com condições. Cada uma dessas tags oferece opções específicas para trabalhar com dados, tornando-as ferramentas indispensáveis para a exibição dinâmica e estruturada de informações nos modelos Latte.

Filtro e função group

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

id categoryId name
1 1 Maçã
2 1 Banana
3 2 PHP
4 3 Verde
5 3 Vermelho
6 3 Azul

Uma lista simples de todos os itens usando um modelo Latte teria a seguinte aparência:

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

Entretanto, se quisermos que os itens sejam organizados em grupos por categoria, precisaremos dividi-los de modo que cada categoria tenha sua própria lista. O resultado seria 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 de forma fácil e elegante usando |group. Especificamos categoryId como parâmetro, o que significa que os itens serão divididos em matrizes menores com base no valor de $item->categoryId (se $item fosse uma matriz, usaríamos $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 como uma função no Latte, o que nos dá uma sintaxe alternativa: {foreach group($items, categoryId) ...}.

Se você quiser agrupar itens de acordo com critérios mais complexos, poderá usar uma função no parâmetro filter. Por exemplo, o agrupamento de itens pelo tamanho do nome teria a seguinte aparência:

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

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

Essa flexibilidade no agrupamento de dados torna o group uma ferramenta excepcionalmente útil para apresentar dados em modelos Latte.

Loops aninhados

Digamos que tenhamos uma tabela de banco de dados com outra coluna subcategoryId que define subcategorias para cada item. Queremos exibir cada categoria principal em uma lista <ul> e cada subcategoria em uma lista aninhada <ol> lista aninhada:

{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 o banco de dados Nette

Vamos mostrar como usar efetivamente o agrupamento de dados em combinação com o Nette Database. Suponha que estejamos trabalhando com a tabela items do exemplo inicial, que está conectada por meio da coluna categoryId a esta tabela categories:

categoryId name
1 Frutas
2 Idiomas
3 Cores

Carregamos os dados da tabela items usando o comando $items = $db->table('items') do Nette Database Explorer. Durante a iteração desses dados, temos a oportunidade não apenas de acessar atributos como $item->name e $item->categoryId, mas, graças à conexão com a tabela categories, também à linha relacionada nela por meio de $item->category. Essa conexão pode demonstrar usos interessantes:

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

Nesse caso, usamos o filtro |group para agrupar pela linha conectada $item->category, não apenas pela coluna categoryId. Isso nos dá o ActiveRow da categoria dada na chave variável, o que nos permite exibir diretamente seu nome usando {$category->name}. Esse é um exemplo prático de como o agrupamento pode simplificar os modelos e facilitar o manuseio de dados.

Filtro |batch

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

Imagine que temos uma lista de itens e queremos exibi-los em listas, cada uma contendo 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, cada grupo ($batch) contendo até três itens. Cada grupo é então exibido em uma lista <ul> lista separada.

Se o último grupo não contiver elementos suficientes para atingir o número desejado, o segundo parâmetro do filtro permite que você defina com o que esse grupo será complementado. Isso é ideal para alinhar esteticamente elementos em que uma linha incompleta pode parecer desordenada.

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

Tag {iterateWhile}

Demonstraremos as mesmas tarefas que abordamos com o filtro |group 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 progresso dos ciclos com condições, de modo que a iteração ocorre sequencialmente.

Primeiro, desenhamos uma 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 ciclo, ou seja, desenhar listas para cada categoria, a tag {iterateWhile} marca a parte interna, ou seja, itens individuais. A condição na tag end diz que a repetição continuará enquanto o elemento atual e o próximo pertencerem à mesma categoria ($iterator->nextValue é o próximo item).

Se a condição fosse sempre atendida, todos os elementos seriam desenhados no ciclo interno:

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

O resultado terá a seguinte aparência:

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

Qual é a utilidade do iterateWhile dessa forma? Quando a tabela estiver vazia e não contiver elementos, nenhum <ul></ul> é 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 no início do ciclo interno, não no final. Assim, enquanto você sempre entra em {iterateWhile} sem condições, você entra em {iterateWhile $cond} somente quando a condição $cond é atendida. E, ao mesmo tempo, o próximo elemento é gravado em $item.

Isso é útil, por exemplo, em uma situação em que queremos renderizar o primeiro elemento em cada categoria de forma diferente, 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 de modo que primeiro renderizamos o primeiro item e, em seguida, no ciclo interno {iterateWhile}, renderizamos 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}

Em um ciclo, podemos criar vários loops internos e até mesmo aninhá-los. Dessa forma, as subcategorias poderiam ser agrupadas, por exemplo.

Suponha que a tabela tenha outra coluna subcategoryId e que, além de cada categoria estar em uma coluna separada, cada subcategoria esteja em uma coluna separada. <ul>cada subcategoria em um <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}
versão: 3.0