Ignorujesz początkowe artykuły (takie jak „a”, „an” lub „the”) podczas sortowania zapytań?

13

Obecnie próbuję wygenerować listę tytułów muzycznych i chciałbym, aby sortowanie zignorowało (ale nadal wyświetlało) początkowy artykuł tytułu.

Na przykład, jeśli mam listę pasm, będzie wyświetlana alfabetycznie w WordPress w następujący sposób:

  • Czarny Sabat
  • Led Zeppelin
  • Pink Floyd
  • The Beatles
  • The Kinks
  • The Rolling Stones
  • Thin Lizzy

Zamiast tego chciałbym, aby był wyświetlany alfabetycznie, ignorując początkowy artykuł „The”, w następujący sposób:

  • The Beatles
  • Czarny Sabat
  • The Kinks
  • Led Zeppelin
  • Pink Floyd
  • The Rolling Stones
  • Thin Lizzy

Znalazłem rozwiązanie we wpisie na blogu z zeszłego roku , które sugeruje następujący kod w 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;
}

a następnie owijanie zapytania add_filterprzed i remove_filterpo.

Próbowałem tego, ale wciąż pojawia się następujący błąd w mojej witrynie:

Błąd bazy danych WordPress: [Nieznana kolumna „tytuł2” w „klauzuli porządku”]

WYBIERZ wp_posts. * FROM wp_posts GDZIE 1 = 1 ORAZ wp_posts.post_type = 'release' ORAZ (wp_posts.post_status = 'opublikuj' LUB wp_posts.post_status = 'prywatny') ORDER BY UPPER (title2) ASC

Nie będę kłamał, jestem całkiem nowy w części php WordPressa, więc nie jestem pewien, dlaczego otrzymuję ten błąd. Widzę, że ma to coś wspólnego z kolumną „title2”, ale rozumiałem, że pierwsza funkcja powinna się tym zająć. Ponadto, jeśli istnieje mądrzejszy sposób na zrobienie tego, jestem cały w uszach. Przeszukuję go i szukam w tej witrynie, ale tak naprawdę nie znalazłem wielu rozwiązań.

Mój kod używający filtrów wygląda tak, jeśli jest to jakaś pomoc:

<?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();
?>
rpbtz
źródło
1
alternatywnym rozwiązaniem może być przechowywanie tytułu, który chcesz posortować, jako post metadanych i porządku w tym polu zamiast tytułu.
Milo,
Nie jestem pewien, jak to kontynuować. Czy zapisanie tego w nowej kolumnie nie spowodowałoby błędu podobnego do tego, który obecnie otrzymuję?
rpbtz
1
nie używałbyś żadnego z tego kodu, możesz wyszukiwać i sortować według meta post z parametrami meta zapytania .
Milo,

Odpowiedzi:

8

Problem

Myślę, że jest tam literówka:

Nazwa filtra posts_fieldsnie jest post_fields.

To może wyjaśniać, dlaczego title2pole jest nieznane, ponieważ jego definicja nie jest dodawana do generowanego ciągu SQL.

Alternatywa - pojedynczy filtr

Możemy przepisać go, aby używał tylko jednego filtra:

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 );

gdzie możesz teraz aktywować niestandardowe zamówienie za pomocą _customparametru 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();

Alternatywa - rekurencyjna TRIM()

Zaimplementujmy rekurencyjny pomysł Pascala Birchlera , skomentowany tutaj :

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 );

gdzie możemy na przykład skonstruować funkcję rekurencyjną jako:

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 );
}

To znaczy że

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

wygeneruje:

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

Alternatywa - MariaDB

Ogólnie lubię używać MariaDB zamiast MySQL . Jest to o wiele łatwiejsze, ponieważ MariaDB 10.0.5 obsługuje 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 );
birgire
źródło
Myślę, że to powinno rozwiązać problem lepiej niż moje rozwiązanie
Pieter Goosen
Miałeś całkowitą rację - zmiana pól post_field na posts_fields naprawiła problem i teraz sortuje się dokładnie tak, jak tego chcę. Dziękuję Ci! Czuję się teraz trochę głupio, ponieważ to był problem. To właśnie dostaję do kodowania o 4 nad ranem. Zajmę się również rozwiązaniem z pojedynczym filtrem. To naprawdę dobry pomysł. Dzięki jeszcze raz.
rpbtz
Oznaczę to jako prawidłową odpowiedź, ponieważ ta jest najbardziej związana z moimi początkowymi pytaniami, chociaż o ile wiem, pozostałe odpowiedzi również są poprawnymi rozwiązaniami.
rpbtz
Alternatywa dla pojedynczego filtra również działała jak urok. Mogę teraz przechowywać kod filtra functions.phpi wywoływać go, orderbykiedy go potrzebuję. Świetne rozwiązanie - dziękuję :-)
rpbtz
1
Cieszę się, że to zadziałało - dodałem metodę rekurencyjną. @rpbtz
birgire
12

Łatwiejszym sposobem może być przejście i zmiana ślimaka permalink na tych postach, które go potrzebują (pod tytułem na ekranie pisania postów), a następnie po prostu użyj tego do zamawiania zamiast tytułu.

to znaczy. post_namenie używaj post_titledo sortowania ...

Oznaczałoby to również, że twój bezpośredni link może być inny, jeśli użyjesz% postname% w swojej strukturze bezpośredniego łącza, co może być dodatkową premią.

na przykład. http://example.com/rolling-stones/ nie dajehttp://example.com/the-rolling-stones/

EDYCJA : kod aktualizujący istniejące ślimaki, usuwający niechciane prefiksy z post_namekolumny ...

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);
            }
        }
    }
}
majick
źródło
Świetne rozwiązanie - bardzo proste i wydajne w sortowaniu.
BillK
Rozwiązanie literówki z @birgire działało jak urok, ale wydaje się, że to przyzwoita alternatywa. Pójdę teraz na ten drugi, ponieważ jest sporo zapytanych postów z początkowym artykułem i zmiana wszystkich ślimaków permalink może zająć trochę czasu. Podoba mi się jednak prostota tego rozwiązania. Dzięki :-)
rpbtz
1
skoro ci się podobało, dodano kod, który powinien zmienić wszystkie ślimaki, jeśli chcesz / potrzebujesz. :-)
majick
6

EDYTOWAĆ

Trochę poprawiłem kod. Wszystkie bloki kodu są odpowiednio aktualizowane. Tylko uwaga, zanim wskoczyłem do aktualizacji w ORYGINALNEJ ODPOWIEDZI , skonfigurowałem kod do pracy z następującymi

  • Niestandardowy typ postu -> release

  • Niestandardowa taksonomia -> game

Upewnij się, że ustawiłeś to zgodnie ze swoimi potrzebami

ORYGINALNA ODPOWIEDŹ

Oprócz innych odpowiedzi i literówki wskazanej przez @birgire, oto inne podejście.

Najpierw ustawimy tytuł jako ukryte pole niestandardowe, ale najpierw usuniemy takie słowa the, które chcielibyśmy wykluczyć. Zanim to zrobimy, musimy najpierw utworzyć funkcję pomocnika, aby usunąć zakazane słowa z nazw terminów i tytułów postów

/**
 * 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;
}

Teraz, gdy już to omówiliśmy, spójrzmy na fragment kodu, aby ustawić nasze niestandardowe pole. Musisz usunąć ten kod całkowicie, gdy tylko załadujesz dowolną stronę. Jeśli masz ogromną witrynę z mnóstwem postów, możesz ustawić posts_per_pagecoś 100i uruchomić skrypty kilka razy, dopóki wszystkie posty nie będą miały niestandardowego pola dla wszystkich postów

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
});

Teraz, gdy pola niestandardowe są ustawione na wszystkie posty, a powyższy kod został usunięty, musimy upewnić się, że ustawiliśmy to niestandardowe pole na wszystkie nowe posty lub za każdym razem, gdy aktualizujemy tytuł postu. Do tego użyjemy transition_post_statushaka. Poniższy kod może przejść do wtyczki ( którą polecam ) lub do twojejfunctions.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 );

PYTANIE O SWOJE POCZTY

Możesz uruchamiać swoje zapytania normalnie, bez żadnych niestandardowych filtrów. Możesz wyszukiwać i sortować swoje posty w następujący sposób

$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 );
Pieter Goosen
źródło
Podoba mi się to podejście (może wystarczy usunąć zakazane słowo z początku tytułu)
birgire
@ Birgire poszedłem z tym tylko dlatego, że moja znajomość języka SQL jest słaba jak mysz kościelna, hahahaha. Dzięki za literówkę
Pieter Goosen
1
Dowcipna mysz może być o wiele bardziej zwinna niż
twardy
0

Odpowiedzi birgire'a działają dobrze przy zamawianiu tylko według tego pola. Wprowadziłem pewne modyfikacje, aby działały przy zamawianiu według wielu pól (nie jestem pewien, czy działa poprawnie, gdy porządkowanie tytułów jest podstawowym):

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 );
Yedidel Elhayany
źródło