Tutto quello che avreste sempre voluto sapere sul raggruppamento

Lavorando con i dati nei template capita spesso di dover raggruppare gli elementi, dividerli in lotti oppure scorrerli in base a una condizione. Latte offre tre strumenti per farlo, ognuno adatto a una situazione leggermente diversa.

Il filtro |group raggruppa gli elementi secondo un criterio, il filtro |batch li divide in lotti di dimensione fissa e il tag {iterateWhile} scorre i dati passo dopo passo decidendo da sé quando interrompere il ciclo interno. Li esamineremo uno alla volta.

Filtro e funzione group

Lo strumento si può usare in due forme: come filtro $items|group: … oppure come funzione group($items, …). Dal punto di vista semantico sono equivalenti: scegliete in base alla leggibilità.

Immaginate una tabella di database items, i cui elementi appartengono a categorie diverse:

id categoryId name
1 1 Mela
2 1 Banana
3 2 PHP
4 3 Verde
5 3 Rosso
6 3 Blu

Un semplice elenco di tutti gli elementi con un template Latte avrebbe questo aspetto:

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

Se però volessimo gli elementi organizzati in gruppi per categoria, dobbiamo dividerli in modo che ogni categoria abbia il proprio elenco. Il risultato desiderato sarebbe questo:

<ul>
	<li>Mela</li>
	<li>Banana</li>
</ul>

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

<ul>
	<li>Verde</li>
	<li>Rosso</li>
	<li>Blu</li>
</ul>

Questo compito si risolve in modo semplice ed elegante con |group. Indichiamo categoryId come parametro: gli elementi verranno cioè divisi in array più piccoli in base al valore di $item->categoryId (se $item fosse un array, verrebbe usato $item['categoryId']):

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

Se volete raggruppare gli elementi secondo criteri più complessi, potete usare una funzione come parametro del filtro. La chiave di ogni gruppo sarà allora il valore restituito dalla funzione: per esempio, raggruppando per lunghezza del nome, sarà il numero di caratteri:

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

È importante notare che ogni gruppo (compreso $categoryItems) non è un array normale, ma un oggetto che si comporta come un iteratore: non potete quindi accedere agli elementi per indice, per esempio $categoryItems[0]. Potete però contare gli elementi con count($categoryItems) e, per accedere al primo elemento del gruppo, usare la funzione first().

Questa flessibilità fa di |group uno strumento eccezionalmente utile per presentare i dati.

Cicli annidati

Immaginiamo che la nostra tabella di database abbia un'ulteriore colonna subcategoryId, che definisce le sottocategorie di ogni elemento. Vogliamo mostrare ogni categoria principale in un elenco <ul> separato e ogni sottocategoria di quella categoria principale in un elenco <ol> annidato a parte:

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

Insieme a Nette Database

Mostriamo come usare efficacemente il raggruppamento dei dati insieme a Nette Database. Supponiamo di lavorare con la tabella items dell'esempio introduttivo, collegata tramite la colonna categoryId a questa tabella categories:

categoryId name
1 Frutta
2 Linguaggi
3 Colori

Carichiamo i dati dalla tabella items con Nette Database Explorer usando il comando $items = $db->table('items'). Scorrendo questi dati possiamo accedere non solo ad attributi come $item->name e $item->categoryId, ma anche, grazie alla relazione con la tabella categories, alla riga collegata tramite $item->category. Questa relazione permette applicazioni interessanti:

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

In questo caso usiamo il filtro |group per raggruppare secondo la riga collegata $item->category, non solo secondo la colonna categoryId. Di conseguenza la chiave ($category) contiene direttamente l'oggetto ActiveRow di quella categoria, il che ci permette di mostrarne il nome con {$category->name} e di accedere a qualsiasi altra colonna senza eseguire una query separata su categories.

Filtro |batch

Il filtro divide un elenco di elementi in lotti di dimensione fissa. Torna utile per i layout a griglia, per la disposizione in colonne o per qualsiasi tipo di raggruppamento visivo.

Immaginiamo di voler mostrare gli elementi in elenchi contenenti al massimo tre elementi ciascuno:

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

In questo esempio l'elenco $items viene diviso in gruppi più piccoli, dove ogni gruppo ($batch) contiene fino a tre elementi. Ogni lotto viene poi mostrato in un elenco <ul> separato.

Se l'ultimo gruppo non contiene abbastanza elementi per raggiungere il numero desiderato, il secondo parametro del filtro permette di definire con cosa completarlo. È l'ideale per allineare esteticamente gli elementi, quando una riga incompleta rischierebbe di apparire disordinata.

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

Tag {iterateWhile}

Mostriamo gli stessi compiti risolti con il filtro |group usando il tag {iterateWhile}. La differenza principale tra i due approcci è che |group elabora e raggruppa prima tutti i dati in ingresso, mentre {iterateWhile} governa l'avanzamento del ciclo tramite una condizione e l'iterazione procede in sequenza.

Per prima cosa disegniamo la tabella con le categorie usando {iterateWhile}:

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

Mentre {foreach} delimita la parte esterna del ciclo, cioè il disegno degli elenchi di ciascuna categoria, il tag {iterateWhile} delimita la parte interna, cioè i singoli elementi. La condizione nel tag di chiusura dice che la ripetizione continuerà finché l'elemento corrente e il successivo appartengono alla stessa categoria ($iterator->nextValue è l'elemento successivo; per l'ultimo elemento il ciclo interno termina perché non esiste un elemento successivo: Latte controlla $iterator->hasNext() prima di valutare la condizione, quindi non si arriva mai al confronto con null).

Se la condizione fosse sempre vera, tutti gli elementi verrebbero disegnati dentro il primo <ul>:

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

Il risultato sarebbe questo:

<ul>
	<li>Mela</li>
	<li>Banana</li>
	<li>PHP</li>
	<li>Verde</li>
	<li>Rosso</li>
	<li>Blu</li>
</ul>

Qual è il vantaggio di usare {iterateWhile} in questo modo? Poiché il <ul> si trova dentro il {foreach} esterno, quando l'ingresso è vuoto non viene disegnato proprio nulla: nessun <ul></ul> solitario. Senza {iterateWhile} dovreste gestire lo stesso caso con un {if} prima del tag di apertura oppure con {foreachelse}.

Se indichiamo la condizione nel tag di apertura {iterateWhile}, il comportamento cambia: la condizione (e il passaggio all'elemento successivo) viene eseguita all'inizio del ciclo interno, non alla fine. Mentre quindi in {iterateWhile} senza condizione si entra sempre, in {iterateWhile $cond} si entra solo quando la condizione $cond è soddisfatta. E allo stesso tempo in $item viene scritto l'elemento successivo.

Questo è utile nelle situazioni in cui vogliamo disegnare il primo elemento di ogni categoria diversamente dagli altri, per esempio così:

<h1>Mela</h1>
<ul>
	<li>Banana</li>
</ul>

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

<h1>Verde</h1>
<ul>
	<li>Rosso</li>
	<li>Blu</li>
</ul>

(Il <ul></ul> vuoto della categoria PHP illustra soltanto il meccanismo: in codice reale gestireste il disegno del <ul> con un {if}.)

Modifichiamo il codice originale in modo da disegnare prima l'elemento come intestazione e usare poi il ciclo interno {iterateWhile} per disegnare gli elementi successivi della stessa categoria come voci dell'elenco:

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

All'interno di un unico ciclo possiamo creare più cicli interni e perfino annidarli. In questo modo potete raggruppare su più livelli contemporaneamente, per esempio le sottocategorie sotto le categorie.

Supponiamo che la tabella abbia un'altra colonna subcategoryId e che, oltre a ogni categoria in un <ul> separato, ogni sottocategoria vada in un <ol> separato:

{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}
versione: 3.x