Ignorare gli articoli iniziali (come 'a', 'an' o 'the') quando si ordinano le query?


13

Attualmente sto provando a produrre un elenco di titoli musicali e vorrei che l'ordinamento ignori (ma visualizzi ancora) l'articolo iniziale del titolo.

Ad esempio, se avessi un elenco di bande, questo verrà visualizzato in ordine alfabetico in WordPress in questo modo:

  • Black Sabbath
  • LED Zeppelin
  • Fluido Rosa
  • Gli scarafaggi
  • The Kinks
  • Le pietre rotolanti
  • Lucertola magra

Vorrei invece visualizzarlo in ordine alfabetico ignorando l'articolo iniziale 'The', in questo modo:

  • Gli scarafaggi
  • Black Sabbath
  • The Kinks
  • LED Zeppelin
  • Fluido Rosa
  • Le pietre rotolanti
  • Lucertola magra

Mi sono imbattuto in una soluzione in un post di blog dello scorso anno , che suggerisce il seguente codice in functions.php:

function wpcf_create_temp_column($fields) {
  global $wpdb;
  $matches = 'The';
  $has_the = " CASE 
      WHEN $wpdb->posts.post_title regexp( '^($matches)[[:space:]]' )
        THEN trim(substr($wpdb->posts.post_title from 4)) 
      ELSE $wpdb->posts.post_title 
        END AS title2";
  if ($has_the) {
    $fields .= ( preg_match( '/^(\s+)?,/', $has_the ) ) ? $has_the : ", $has_the";
  }
  return $fields;
}

function wpcf_sort_by_temp_column ($orderby) {
  $custom_orderby = " UPPER(title2) ASC";
  if ($custom_orderby) {
    $orderby = $custom_orderby;
  }
  return $orderby;
}

e quindi racchiudendo la query con add_filterprima e remove_filterdopo.

Ho provato questo, ma continuo a ricevere il seguente errore sul mio sito:

Errore del database WordPress: [Colonna sconosciuta 'title2' nella 'clausola d'ordine']

SELEZIONA wp_posts. * DA wp_posts DOVE 1 = 1 E wp_posts.post_type = 'release' AND (wp_posts.post_status = 'publishing' O wp_posts.post_status = 'private') ORDER BY UPPER (title2) ASC

Non mentirò, sono abbastanza nuovo nella parte php di WordPress, quindi non sono sicuro del motivo per cui sto ricevendo questo errore. Vedo che ha qualcosa a che fare con la colonna "title2", ma ho capito che la prima funzione dovrebbe occuparsene. Inoltre, se c'è un modo più intelligente per farlo, sono tutto orecchi. Ho cercato su Google in questo sito, ma non ho trovato molte soluzioni.

Il mio codice che utilizza i filtri appare così se è di aiuto:

<?php 
    $args_post = array('post_type' => 'release', 'orderby' => 'title', 'order' => 'ASC', 'posts_per_page' => -1, );

    add_filter('post_fields', 'wpcf_create_temp_column'); /* remove initial 'The' from post titles */
    add_filter('posts_orderby', 'wpcf_sort_by_temp_column');

    $loop = new WP_Query($args_post);

    remove_filter('post_fields', 'wpcf_create_temp_column');
    remove_filter('posts_orderby', 'wpcf_sort_by_temp_column');

        while ($loop->have_posts() ) : $loop->the_post();
?>

1
una soluzione alternativa potrebbe essere quella di memorizzare il titolo che si desidera ordinare come metadati post e ordinarlo su quel campo anziché titolo.
Milo

Non sono sicuro di come procedere. La memorizzazione in una nuova colonna non comporterebbe un errore simile a quello che sto attualmente ricevendo?
rpbtz,

1
non useresti alcuno di quel codice, puoi interrogare e ordinare su post meta con parametri meta query .
Milo,

Risposte:


8

Il problema

Penso che ci sia un refuso lì dentro:

Il nome del filtro posts_fieldsnon lo è post_fields.

Questo potrebbe spiegare perché il title2campo è sconosciuto, perché la sua definizione non viene aggiunta alla stringa SQL generata.

Alternativa - Filtro singolo

Possiamo riscriverlo per usare solo un singolo filtro:

add_filter( 'posts_orderby', function( $orderby, \WP_Query $q )
{
    // Do nothing
    if( '_custom' !== $q->get( 'orderby' ) )
        return $orderby;

    global $wpdb;

    $matches = 'The';   // REGEXP is not case sensitive here

    // Custom ordering (SQL)
    return sprintf( 
        " 
        CASE 
            WHEN {$wpdb->posts}.post_title REGEXP( '^($matches)[[:space:]]+' )
                THEN TRIM( SUBSTR( {$wpdb->posts}.post_title FROM %d )) 
            ELSE {$wpdb->posts}.post_title 
        END %s
        ",
        strlen( $matches ) + 1,
        'ASC' === strtoupper( $q->get( 'order' ) ) ? 'ASC' : 'DESC'     
    );

}, 10, 2 );

dove ora puoi attivare l'ordinamento personalizzato con il _customparametro orderby:

$args_post = array
    'post_type'      => 'release', 
    'orderby'        => '_custom',    // Activate the custom ordering 
    'order'          => 'ASC', 
    'posts_per_page' => -1, 
);

$loop = new WP_Query($args_post);

while ($loop->have_posts() ) : $loop->the_post();

Alternativa - Ricorsiva TRIM()

Implementiamo l'idea ricorsiva di Pascal Birchler , commentata qui :

add_filter( 'posts_orderby', function( $orderby, \WP_Query $q )
{
    if( '_custom' !== $q->get( 'orderby' ) )
        return $orderby;

    global $wpdb;

    // Adjust this to your needs:
    $matches = [ 'the ', 'an ', 'a ' ];

    return sprintf( 
        " %s %s ",
        wpse_sql( $matches, " LOWER( {$wpdb->posts}.post_title) " ),
        'ASC' === strtoupper( $q->get( 'order' ) ) ? 'ASC' : 'DESC'     
    );

}, 10, 2 );

dove possiamo ad esempio costruire la funzione ricorsiva come:

function wpse_sql( &$matches, $sql )
{
    if( empty( $matches ) || ! is_array( $matches ) )
        return $sql;

    $sql = sprintf( " TRIM( LEADING '%s' FROM ( %s ) ) ", $matches[0], $sql );
    array_shift( $matches );    
    return wpse_sql( $matches, $sql );
}

Ciò significa che

$matches = [ 'the ', 'an ', 'a ' ];
echo wpse_sql( $matches, " LOWER( {$wpdb->posts}.post_title) " );

genererà:

TRIM( LEADING 'a ' FROM ( 
    TRIM( LEADING 'an ' FROM ( 
        TRIM( LEADING 'the ' FROM ( 
            LOWER( wp_posts.post_title) 
        ) )
    ) )
) )

Alternativa - MariaDB

In generale, mi piace usare MariaDB invece di MySQL . Quindi è molto più semplice perché MariaDB 10.0.5 supporta REGEXP_REPLACE :

/**
 * Ignore (the,an,a) in post title ordering
 *
 * @uses MariaDB 10.0.5+
 */
add_filter( 'posts_orderby', function( $orderby, \WP_Query $q )
{
    if( '_custom' !== $q->get( 'orderby' ) )
        return $orderby;

    global $wpdb;
    return sprintf( 
        " REGEXP_REPLACE( {$wpdb->posts}.post_title, '^(the|a|an)[[:space:]]+', '' ) %s",
        'ASC' === strtoupper( $q->get( 'order' ) ) ? 'ASC' : 'DESC'     
    );
}, 10, 2 );

Penso che questo dovrebbe risolvere il problema meglio della mia soluzione
Pieter Goosen

Avevi assolutamente ragione: cambiare post_fields in posts_fields ha risolto il problema e ora sta ordinando esattamente come lo voglio. Grazie! Mi sento un po 'stupido ora visto che quello era il problema. Questo è quello che ottengo per la codifica alle 4 del mattino, immagino. Esaminerò anche la soluzione a filtro singolo. Sembra davvero una buona idea. Grazie ancora.
rpbtz,

Lo segnerò come la risposta corretta in quanto questa è la più strettamente correlata alle mie domande iniziali, anche se per quanto posso dire le altre risposte sono anche valide soluzioni.
rpbtz,

Anche l'alternativa a filtro singolo ha funzionato come un fascino. Ora posso mantenere il codice del filtro functions.phpe chiamarlo orderbyquando ne ho bisogno. Ottima soluzione - grazie :-)
rpbtz

1
Sono contento di sentire che ha funzionato per te - ho aggiunto il metodo ricorsivo. @rpbtz
birgire

12

Un modo più semplice potrebbe essere quello di passare attraverso e cambiare la lumaca permalink su quei post che ne hanno bisogno (sotto il titolo nella schermata di scrittura dei post) e quindi usarlo solo per ordinare al posto del titolo.

vale a dire. post_namenon utilizzare post_titleper l'ordinamento ...

Ciò significherebbe anche che il tuo permalink potrebbe essere diverso se utilizzi% postname% nella struttura del permalink, che potrebbe essere un bonus aggiuntivo.

per esempio. http://example.com/rolling-stones/ non dàhttp://example.com/the-rolling-stones/

EDIT : codice per aggiornare le lumache esistenti, rimuovendo i prefissi indesiderati dalla post_namecolonna ...

global $wpdb;
$posttype = 'release';
$stripprefixes = array('a-','an-','the-');

$results = $wpdb->get_results("SELECT ID, post_name FROM ".$wpdb->prefix."posts" WHERE post_type = '".$posttype."' AND post_status = 'publish');
if (count($results) > 0) {
    foreach ($results as $result) {
        $postid = $result->ID;
        $postslug = $result->post_name;
        foreach ($stripprefixes as $stripprefix) {
            $checkprefix = strtolower(substr($postslug,0,strlen($stripprefix));
            if ($checkprefix == $stripprefix) {
                $newslug = substr($postslug,strlen($stripprefix),strlen($postslug));
                // echo $newslug; // debug point
                $query = $wpdb->prepare("UPDATE ".$wpdb->prefix."posts SET post_name = '%s' WHERE ID = '%d'", $newslug, $postid);
                $wpdb->query($query);
            }
        }
    }
}

Ottima soluzione - molto semplice ed efficiente per l'ordinamento.
BillK,

La soluzione di errore di battitura di @birgire ha funzionato come un fascino, ma questa sembra un'alternativa decente. Per ora andrò con l'altro in quanto ci sono alcuni post con un articolo iniziale e la modifica di tutte le lumache del permalink potrebbe richiedere del tempo. Mi piace la semplicità di questa soluzione, però. Grazie :-)
rpbtz,

1
da quando ti è piaciuto, ha aggiunto un po 'di codice che dovrebbe cambiare tutte le lumache, se desiderato / necessario. :-)
Majick

6

MODIFICARE

Ho migliorato un po 'il codice. Tutti i blocchi di codice vengono aggiornati di conseguenza. Solo una nota però prima di saltare agli aggiornamenti nella RISPOSTA ORIGINALE , ho impostato il codice per lavorare con il seguente

  • Tipo di post personalizzato -> release

  • Tassonomia personalizzata -> game

Assicurati di impostare questo in base alle tue esigenze

RISPOSTA ORIGINALE

Oltre alle altre risposte e al refuso sottolineato da @birgire, ecco un altro approccio.

Innanzitutto, imposteremo il titolo come un campo personalizzato nascosto, ma rimuoveremo prima le parole del genere theche vorremmo escludere. Prima di farlo, dobbiamo prima creare una funzione di supporto per rimuovere le parole vietate dai nomi dei termini e dai titoli dei post

/**
 * Function get_name_banned_removed()
 *
 * A helper function to handle removing banned words
 * 
 * @param string $tring  String to remove banned words from
 * @param array  $banned Array of banned words to remove
 * @return string $string
 */
function get_name_banned_removed( $string = '', $banned = [] )
{
    // Make sure we have a $string to handle
    if ( !$string )
        return $string;

    // Sanitize the string
    $string = filter_var( $string, FILTER_SANITIZE_STRING );

    // Make sure we have an array of banned words
    if (    !$banned
         || !is_array( $banned )
    )
        return $string; 

    // Make sure that all banned words is lowercase
    $banned = array_map( 'strtolower', $banned );

    // Trim the string and explode into an array, remove banned words and implode
    $text          = trim( $string );
    $text          = strtolower( $text );
    $text_exploded = explode( ' ', $text );

    if ( in_array( $text_exploded[0], $banned ) )
        unset( $text_exploded[0] );

    $text_as_string = implode( ' ', $text_exploded );

    return $string = $text_as_string;
}

Ora che lo abbiamo coperto, diamo un'occhiata al pezzo di codice per impostare il nostro campo personalizzato. Devi rimuovere questo codice completamente non appena hai caricato una pagina una volta. Se hai un sito enorme con un sacco di post, puoi impostare posts_per_pagequalcosa su 100e eseguire gli script un paio di volte fino a quando tutti i post non hanno il campo personalizzato impostato su tutti i post

add_action( 'wp', function ()
{
    add_filter( 'posts_fields', function ( $fields, \WP_Query $q ) 
    {
        global $wpdb;

        remove_filter( current_filter(), __FUNCTION__ );

        // Only target a query where the new custom_query parameter is set with a value of custom_meta_1
        if ( 'custom_meta_1' === $q->get( 'custom_query' ) ) {
            // Only get the ID and post title fields to reduce server load
            $fields = "$wpdb->posts.ID, $wpdb->posts.post_title";
        }

        return $fields;
    }, 10, 2);

    $args = [
        'post_type'        => 'release',       // Set according to needs
        'posts_per_page'   => -1,              // Set to execute smaller chucks per page load if necessary
        'suppress_filters' => false,           // Allow the posts_fields filter
        'custom_query'     => 'custom_meta_1', // New parameter to allow that our filter only target this query
        'meta_query'       => [
            [
                'key'      => '_custom_sort_post_title', // Make it a hidden custom field
                'compare'  => 'NOT EXISTS'
            ]
        ]
    ];
    $q = get_posts( $args );

    // Make sure we have posts before we continue, if not, bail
    if ( !$q ) 
        return;

    foreach ( $q as $p ) {
        $new_post_title = strtolower( $p->post_title );

        if ( function_exists( 'get_name_banned_removed' ) )
            $new_post_title = get_name_banned_removed( $new_post_title, ['the'] );

        // Set our custom field value
        add_post_meta( 
            $p->ID,                    // Post ID
            '_custom_sort_post_title', // Custom field name
            $new_post_title            // Custom field value
        );  
    } //endforeach $q
});

Ora che i campi personalizzati sono impostati su tutti i post e il codice sopra è stato rimosso, dobbiamo assicurarci di impostare questo campo personalizzato su tutti i nuovi post o ogni volta che aggiorniamo il titolo del post. Per questo useremo il transition_post_statusgancio. Il seguente codice può essere inserito in un plugin ( che raccomando ) o nel tuofunctions.php

add_action( 'transition_post_status', function ( $new_status, $old_status, $post )
{
    // Make sure we only run this for the release post type
    if ( 'release' !== $post->post_type )
        return;

    $text = strtolower( $post->post_title );   

    if ( function_exists( 'get_name_banned_removed' ) )
        $text = get_name_banned_removed( $text, ['the'] );

    // Set our custom field value
    update_post_meta( 
        $post->ID,                 // Post ID
        '_custom_sort_post_title', // Custom field name
        $text                      // Custom field value
    );
}, 10, 3 );

RICHIEDI I TUOI POSTI

È possibile eseguire le query normalmente senza filtri personalizzati. Puoi interrogare e ordinare i tuoi post come segue

$args_post = [
    'post_type'      => 'release', 
    'orderby'        => 'meta_value', 
    'meta_key'       => '_custom_sort_post_title',
    'order'          => 'ASC', 
    'posts_per_page' => -1, 
];
$loop = new WP_Query( $args );

Mi piace questo approccio (forse è sufficiente rimuovere la parola vietata dall'inizio del titolo)
birgire

@birgire Sono andato solo con questo perché la mia conoscenza di SQL è scarsa come un topo da chiesa, hahahaha. Grazie per l'errore di battitura
Pieter Goosen

1
Il mouse spiritoso può essere molto più agile dell'elefante SQL hardcoded ;-)
birgire

0

Le risposte di birgire funzionano bene quando si ordina solo per questo campo. Ho apportato alcune modifiche per farlo funzionare quando si ordina da più campi (non sono sicuro che funzioni correttamente quando l'ordinamento del titolo è quello principale):

add_filter( 'posts_orderby', function( $orderby, \WP_Query $q )
{
// Do nothing
if( '_custom' !== $q->get( 'orderby' ) && !isset($q->get( 'orderby' )['_custom']) )
    return $orderby;

global $wpdb;

$matches = 'The';   // REGEXP is not case sensitive here

// Custom ordering (SQL)
if (is_array($q->get( 'orderby' ))) {
    return sprintf( 
        " $orderby, 
        CASE 
            WHEN {$wpdb->posts}.post_title REGEXP( '^($matches)[[:space:]]+' )
                THEN TRIM( SUBSTR( {$wpdb->posts}.post_title FROM %d )) 
            ELSE {$wpdb->posts}.post_title 
        END %s
        ",
        strlen( $matches ) + 1,
        'ASC' === strtoupper( $q->get( 'orderby' )['_custom'] ) ? 'ASC' : 'DESC'     
    );
}
else {
    return sprintf( 
        "
        CASE 
            WHEN {$wpdb->posts}.post_title REGEXP( '^($matches)[[:space:]]+' )
                THEN TRIM( SUBSTR( {$wpdb->posts}.post_title FROM %d )) 
            ELSE {$wpdb->posts}.post_title 
        END %s
        ",
        strlen( $matches ) + 1,
        'ASC' === strtoupper( $q->get( 'order' ) ) ? 'ASC' : 'DESC'     
    );
}

}, 10, 2 );
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.