Tutto quello che avreste sempre voluto sapere sul raggruppamento

Quando si lavora con i dati nei template, spesso si può incontrare la necessità di raggrupparli o visualizzarli in modo specifico secondo determinati criteri. Latte offre diversi strumenti potenti per questo scopo.

Il filtro e la funzione |group consentono un raggruppamento efficiente dei dati secondo un criterio specificato, il filtro |batch facilita la divisione dei dati in lotti di dimensioni fisse e il tag {iterateWhile} offre la possibilità di un controllo più complesso del flusso dei cicli con condizioni. Ognuno di questi tag offre opzioni specifiche per lavorare con i dati, rendendoli strumenti indispensabili per la visualizzazione dinamica e strutturata delle informazioni nei template Latte.

Filtro e funzione group

Immaginate una tabella di database items con voci divise in categorie:

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

Un semplice elenco di tutte le voci utilizzando un template Latte sarebbe simile a questo:

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

Tuttavia, se volessimo che le voci fossero organizzate in gruppi per categoria, dovremmo dividerle in modo che ogni categoria abbia il proprio elenco. Il risultato dovrebbe quindi essere il seguente:

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

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

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

Il compito può essere risolto facilmente ed elegantemente usando |group. Come parametro specifichiamo categoryId, il che significa che le voci verranno divise 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}

Il filtro può essere utilizzato in Latte anche come funzione, il che ci dà una sintassi alternativa: {foreach group($items, categoryId) ...}.

Se desiderate raggruppare le voci secondo criteri più complessi, potete utilizzare una funzione nel parametro del filtro. Ad esempio, il raggruppamento delle voci per lunghezza del nome sarebbe simile a questo:

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

È importante notare che $categoryItems non è un array comune, ma un oggetto che si comporta come un iteratore. Per accedere alla prima voce del gruppo, potete usare la funzione first().

Questa flessibilità nel raggruppamento dei dati rende group uno strumento eccezionalmente utile per la presentazione dei dati nei template Latte.

Cicli Annidati

Immaginiamo di avere una tabella di database con un'altra colonna subcategoryId, che definisce le sottocategorie delle singole voci. Vogliamo visualizzare ogni categoria principale in un elenco <ul> separato e ogni sottocategoria in un elenco <ol> annidato separato:

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

Connessione con Nette Database

Vediamo come utilizzare efficacemente il raggruppamento dei dati in combinazione con Nette Database. Supponiamo di lavorare con la tabella items dell'esempio introduttivo, che è collegata tramite la colonna categoryId a questa tabella categories:

categoryId name
1 Fruits
2 Languages
3 Colors

Carichiamo i dati dalla tabella items usando Nette Database Explorer con il comando $items = $db->table('items'). Durante l'iterazione su questi dati, abbiamo la possibilità di accedere non solo ad attributi come $item->name e $item->categoryId, ma grazie al collegamento con la tabella categories, anche alla riga correlata in essa tramite $item->category. Su questo collegamento si può dimostrare un utilizzo interessante:

{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, utilizziamo il filtro |group per raggruppare secondo la riga collegata $item->category, non solo secondo la colonna categoryId. Grazie a ciò, nella variabile chiave $category abbiamo direttamente l'ActiveRow della categoria data, il che ci permette di stampare direttamente il suo nome usando {$category->name}. Questo è un esempio pratico di come il raggruppamento possa rendere più chiari i template e facilitare il lavoro con i dati.

Filtro |batch

Il filtro consente di dividere un elenco di elementi in gruppi con un numero predeterminato di elementi. Questo filtro è ideale per situazioni in cui si desidera presentare i dati in più gruppi più piccoli, ad esempio per una migliore leggibilità o organizzazione visiva sulla pagina.

Immaginiamo di avere un elenco di voci e di volerle visualizzare in elenchi, dove ognuno contiene al massimo tre voci. L'uso del filtro |batch è molto pratico in tal caso:

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

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

Se l'ultimo gruppo non contiene abbastanza elementi per raggiungere il numero desiderato, il secondo parametro del filtro consente di definire con cosa verrà completato questo gruppo. Questo è ideale per allineare esteticamente gli elementi dove una riga incompleta potrebbe apparire disordinata.

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

Tag {iterateWhile}

Gli stessi compiti che abbiamo risolto con il filtro |group, li mostreremo usando il tag {iterateWhile}. La differenza principale tra i due approcci è che group elabora e raggruppa prima tutti i dati di input, mentre {iterateWhile} controlla il flusso dei cicli con condizioni, quindi l'iterazione avviene gradualmente.

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} indica la parte esterna del ciclo, cioè il rendering degli elenchi per ogni categoria, il tag {iterateWhile} indica la parte interna, cioè le singole voci. La condizione nel tag di chiusura dice che la ripetizione continuerà finché l'elemento corrente e quello successivo appartengono alla stessa categoria ($iterator->nextValue è la voce successiva).

Se la condizione fosse sempre soddisfatta, nel ciclo interno verrebbero disegnati tutti gli elementi:

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

Il risultato sarà simile a questo:

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

A cosa serve un tale uso di iterateWhile? Quando la tabella sarà vuota e non conterrà alcun elemento, non verrà stampato un <ul></ul> vuoto.

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

Ciò è utile, ad esempio, in una situazione in cui vogliamo disegnare il primo elemento di ogni categoria in modo diverso, ad esempio così:

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

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

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

Modifichiamo il codice originale in modo da disegnare prima la prima voce e poi, nel ciclo interno {iterateWhile}, disegnare le altre voci della stessa categoria:

{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 ciclo, possiamo creare più cicli interni e persino annidarli. In questo modo si potrebbero raggruppare, ad esempio, sottocategorie, ecc.

Supponiamo che nella tabella ci sia un'altra colonna subcategoryId e oltre al fatto che ogni categoria sarà in un <ul> separato, ogni sottocategoria sarà 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.0