Soluzione Grezza all'Ordinamento di un Array di Oggetti Pagina in Modo Gerarchico

A volte sorge la necessità di avere a disposizione un array di oggetti Pagina ordinato in modo gerarchico, ma tutto quanto disponibile è solo una collezione di oggetti Pagina ordinati per titolo per esempio, magari perché erano stati precedentemente recuperati dal database tramite l'oggetto globale $wpdb. In tali occasioni, per ordinare in modo gerarchico un array di questo tipo, dobbiamo essere creativi, perché WordPress non è stato provvisto di una funzione utilità atta allo scopo — o almeno non ho investigato abbastanza da scoprirne una. In verità, mi ha semplicemente divertito cercare una mia soluzione, ed è per lo più per questo che ora sono qui a scriverne.

In poche parole, ho raggiunto il mio obiettivo affrontando il problema in due fasi: la prima consiste nel creare un array temporaneo in cui gli elementi sono array di oggetti Pagina che condividono lo stesso valore di post_parent (pagine figlie), la seconda fase si riduce a combinare l'array temporaneo con un array di oggetti Pagina di primo livello (oggetti Pagina che hanno post_parent = 0) anch'esso nato all'interno dello stesso loop che ha riempito l'array temporaneo.

Non preoccuparti, descriverò ogni fase in dettaglio.

L'Array di Partenza

Il codice oggetto di questo post ha avuto origine dal ciclo di sviluppo di SiteTree 5.3, mentre stavo eseguendo il refactoring di un blocco di codice inteso a produrre una lista di tag <option> per un controllo a tendina (tag <select>). I tag <option> avevano la parte visibile indentata in modo da sembrare una lista gerarchica, che effettivamente rappresentava parte delle Pagine disponibili sul sito web.

La mia intenzione ora è giusto quella di fornire un contesto e una sorgente al nostro array di oggetti Pagina.

Suddetta lista di tag <option> era stata frutto della manipolazione di un array di oggetti Pagina progenie di una chiamata a $wpdb->get_results() molto simile a questa:

global $wpdb;

// È importante che gli oggetti risultanti dalla query
// abbiano almeno un attributo 'ID' e uno 'post_parent'.
$the_query = "SELECT ID, post_title, post_parent
              FROM {$wpdb->posts}
              WHERE post_type = 'page' AND post_status = 'publish' AND
                    post_password = '' 
              ORDER BY menu_order, post_title ASC";

// L'array di oggetti Pagina da ordinare in modo gerarchico.
$pages = $wpdb->get_results( $the_query );

Voglio sottolineare che affinché il codice che sto per descriverti funzioni, gli oggetti Pagina nell'array da riordinare devono essere provvisti almeno degli attributi ID e post_parent, a prescindere che tali oggetti siano o meno istanze della classe WP_Post.

Prima Fase: La Cernita

In questa prima fase eseguiremo un loop attraverso i nostri oggetti Pagina in modo da creare due distinti array. Il primo, $ordered_pages, sarà un semplice array di oggetti tutti aventi post_parent = 0, il secondo, $pages_by_parent, sarà invece un array multidimensionale, i suoi elementi saranno array di oggetti Pagina accomunati dallo stesso valore di post_parent.

È fondamentale per l'attualizzazione della seconda fase che le chiavi di $ordered_pages siano uguali in valore all'attributo ID degli oggetti Pagina contenuti nell'array stesso, e che le prime chiavi dell'array multidimensionale $pages_by_parent siano uguali ai valori che l'attributo post_parent assume per ogni gruppo di pagine figlie contenuto in $pages_by_parent.

// Variabile che conterrà l'array ordinato di oggetti Pagina.
$ordered_pages = array();

// Array temporaneo in cui gli elementi sono array di oggetti Pagina
// aventi tutti lo stesso valore di 'post_parent' (pagine figlie).
$pages_by_parent = array();

foreach ( $pages as $page ) {
    $page_id   = (int) $page->ID;
    $parent_id = (int) $page->post_parent;

    // Quando 'post_parent' è 0, significa che l'oggetto è
    // una Pagina di primo livello.
    if ( $parent_id === 0 ) {
        // Pre-popoliamo $ordered_pages con tutte le Pagine di primo livello.
        $ordered_pages[$page_id] = $page;
    }
    else {
        // Nota che le prime chiavi di $pages_by_parent sono uguali 
        // ai valori che l'attributo 'post_parent' assume per 
        // ogni gruppo di oggetti Pagina.
        $pages_by_parent[$parent_id][$page_id] = $page;
    }
}

Seconda Fase: La Fusione

Per ricapitolare, completare il riordinamento implica fondere $pages_by_parent con $ordered_pages, array che contiene ancora solo oggetti Pagina di primo livello.

Quindi ciò che occorre fare ora è inserire i gruppi di oggetti contenuti in $pages_by_parent nelle opportune posizioni dell'array $ordered_pages, e ripetere l'operazione per ogni livello di nidificazione.

La scelta fatta durante la prima fase riguardo le chiavi dei tue array da fondere è di primaria importanza nell'assicurare che l'uso di risorse del server venga tenuto a bada.

// Ad ogni iterazione, il contenuto di uno o più array
// che costituiscono $pages_by_parent viene unito al contenuto di $ordered_pages.
while( $pages_by_parent ) {
    // Array temporaneo usato per fondere i gruppi di pagine figlie
    // contenuti in $pages_by_parent con gli oggetti Pagina in $ordered_pages.
    $array = array();
    
    foreach( $ordered_pages as $page_id => $page ) {
        $array[$page_id] = $page;
        
        // Controlla se l'attuale oggetto $page ha pagine figlie.
        if ( isset( $pages_by_parent[$page_id] ) ) {
            // Accoda un gruppo di pagine figlie all'array temporaneo.
            foreach ( $pages_by_parent[$page_id] as $child_page_id => $child_page ) {
                $array[$child_page_id] = $child_page;
            }

            // Questo è essenziale affinché il ciclo While possa terminare.
            unset( $pages_by_parent[$page_id] );
        }
    }

    // Ad ogni esecuzione del loop, vengono inseriti in $ordered_pages
    // tanti oggetti Pagina quanti ne è composto ogni livello di nidificazione.
    $ordered_pages = $array;
}

Usando Funzioni PHP Native

Implementare questa seconda fase in modo grezzo è stata solo una preferenza personale, infatti il codice può essere riscritto usando funzioni PHP native senza troppi problemi, anche se, non giurerei che le prestazioni ne possano giovare.

Ti lascio con un esempio di detta alternativa:

// Ad ogni iterazione, vengono inserite in $ordered_pages
// tante pagine figlie quante ne è composto ogni livello di nidificazione.
while( $pages_by_parent ) {
    foreach( $pages_by_parent as $parent_id => $child_pages ) {
        // Controlla se l'attuale gruppo di pagine figlie
        // ha un genitore in $ordered_pages.
        if ( isset( $ordered_pages[$parent_id] ) ) {
            $insert_position = 1 + array_search( $parent_id, 
                                                 array_keys( $ordered_pages ) );
            
            // Distribuiamo $ordered_pages tra due distinti array in funzione
            // del valore appena calcolato di $insert_position.
            // Per eseguire tale operazione usiamo array_slice() in modo da
            // preservare le chiavi numeriche.
            $ordered_pages_head = array_slice( $ordered_pages, 0, 
                                               $insert_position, true );
            $ordered_pages_tail = array_slice( $ordered_pages, $insert_position,
                                               null, true );
            
            // Usare l'operatore + per la fusione ci assicura che
            // le chiavi numeriche vengano preservate.
            $ordered_pages = $ordered_pages_head + $child_pages;

            // Accoda $ordered_pages_tail solo se non è un array vuoto.
            if ( $ordered_pages_tail ) {
                $ordered_pages += $ordered_pages_tail;
            }

            // Questo è essenziale affinché il ciclo While possa terminare.
            unset( $pages_by_parent[$parent_id] );
        }
    }
}