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}