Игнорирование начальных статей (например, «a», «an» или «the») при сортировке запросов?

13

В настоящее время я пытаюсь вывести список названий музыки и хотел бы, чтобы сортировка игнорировала (но все еще отображала) исходную статью заголовка.

Например, если бы у меня был список полос, он будет отображаться в алфавитном порядке в WordPress следующим образом:

  • Black Sabbath
  • Лед Зеппелин
  • Пинк Флойд
  • Битлз
  • Изломы
  • Катящиеся камни
  • Тонкая Лиззи

Вместо этого я хотел бы, чтобы он отображался в алфавитном порядке, игнорируя исходную статью «The», например:

  • Битлз
  • Black Sabbath
  • Изломы
  • Лед Зеппелин
  • Пинк Флойд
  • Катящиеся камни
  • Тонкая Лиззи

Я нашел решение в записи блога с прошлого года , в котором предлагается следующий код 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;
}

а затем завернуть запрос add_filterдо и remove_filterпосле.

Я пробовал это, но я продолжаю получать следующую ошибку на моем сайте:

Ошибка базы данных WordPress: [Неизвестный столбец 'title2' в 'предложении заказа']

ВЫБЕРИТЕ wp_posts. * ОТ wp_posts ГДЕ 1 = 1 И wp_posts.post_type = 'release' И (wp_posts.post_status = 'publish' ИЛИ ​​wp_posts.post_status = 'private') ЗАКАЗАТЬ ВЕРХНИЙ (title2) ASC

Я не буду врать, я довольно новичок в php-части WordPress, поэтому я не уверен, почему я получаю эту ошибку. Я вижу, что это как-то связано со столбцом title2, но, насколько я понимаю, первая функция должна позаботиться об этом. Кроме того, если есть более умный способ сделать это, я весь слух. Я гуглил и искал этот сайт, но я действительно не нашел много решений.

Мой код с использованием фильтров выглядит следующим образом:

<?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
источник
1
альтернативным решением может быть сохранение заголовка, по которому вы хотите отсортировать, в виде метаданных и порядка размещения в этом поле вместо заголовка.
Майло
Я немного не уверен в том, как продолжать это. Не приведет ли сохранение этого в новом столбце к ошибке, подобной той, которую я получаю в настоящее время?
rpbtz
1
Вы не будете использовать этот код, вы можете запрашивать и сортировать метаданные поста с параметрами мета-запроса .
Майло

Ответы:

8

Эта проблема

Я думаю, что там есть опечатка:

Название фильтра posts_fieldsнет post_fields.

Это может объяснить, почему title2поле неизвестно, поскольку его определение не добавляется в сгенерированную строку SQL.

Альтернатива - одиночный фильтр

Мы можем переписать его, чтобы использовать только один фильтр:

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

где теперь вы можете активировать пользовательский заказ с _customпараметром 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();

Альтернатива - рекурсивная TRIM()

Давайте реализуем рекурсивную идею Паскаля Бирчлера , прокомментированную здесь :

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

где мы можем, например, построить рекурсивную функцию как:

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

Это значит, что

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

сгенерирует:

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

Альтернатива - MariaDB

В общем, я люблю использовать MariaDB вместо MySQL . Тогда это намного проще, потому что MariaDB 10.0.5 поддерживает 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
источник
Я думаю, что это должно решить проблему лучше, чем мое решение
Питер Гусен
Вы были абсолютно правы - изменение post_fields на posts_fields устранило проблему, и теперь она сортируется именно так, как я хочу. Спасибо! Я чувствую себя немного глупо, видя, как это было проблемой. Это то, что я получаю за кодирование в 4 утра, наверное. Я также рассмотрю решение с одним фильтром. Похоже, действительно хорошая идея. Еще раз спасибо.
rpbtz
Я отмечу это как правильный ответ, так как он наиболее тесно связан с моими первоначальными вопросами, хотя, насколько я могу судить, другие ответы также являются допустимыми решениями.
rpbtz
Единственная альтернатива фильтра работала как очарование также. Теперь я могу сохранить код фильтра functions.phpи вызывать его, orderbyкогда мне это нужно. Отличное решение - спасибо :-)
rpbtz
1
Рад слышать, что это работает для вас - я добавил рекурсивный метод. @rpbtz
birgire
12

Более простым способом может быть просмотр и изменение слагаемого с постоянными ссылками на те посты, которые в нем нуждаются (под заголовком на экране записи поста), а затем просто использовать его для упорядочивания вместо заголовка.

то есть. использовать post_nameне post_titleдля сортировки ...

Это также означает, что ваша постоянная ссылка может отличаться, если вы используете% postname% в структуре постоянных ссылок, что может быть дополнительным бонусом.

например. http://example.com/rolling-stones/ не даетhttp://example.com/the-rolling-stones/

РЕДАКТИРОВАТЬ : код для обновления существующих слагов, удалив нежелательные префиксы из post_nameстолбца ...

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
источник
Отличное решение - очень просто и эффективно для сортировки.
BillK
Опечатка от @birgire работала как шарм, но это похоже на достойную альтернативу. Я пойду с другим на данный момент, так как есть довольно много запрошенных постов с первоначальной статьей, и изменение всех постоянных ссылок может занять некоторое время. Мне нравится простота этого решения, хотя. Спасибо :-)
rpbtz
1
так как вам понравилось, добавили некоторый код, который должен изменить все слагы, если нужно / нужно. :-)
majick
6

РЕДАКТИРОВАТЬ

Я немного улучшил код. Все блоки кода обновляются соответственно. Просто обратите внимание, что прежде чем перейти к обновлениям в ОРИГИНАЛЬНОМ ОТВЕТЕ , я настроил код для работы со следующими

  • Пользовательский тип сообщения -> release

  • Таможенная таксономия -> game

Убедитесь в том, чтобы установить это в соответствии с вашими потребностями

ОРИГИНАЛЬНЫЙ ОТВЕТ

В дополнение к другим ответам и опечатке, указанной @birgire, есть еще один подход.

Сначала мы установим заголовок как скрытое настраиваемое поле, но сначала удалим такие слова, theкоторые мы хотели бы исключить. Прежде чем мы это сделаем, нам нужно сначала создать вспомогательную функцию, чтобы удалить запрещенные слова из названий терминов и названий постов.

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

Теперь, когда мы это рассмотрели, давайте посмотрим на фрагмент кода для установки нашего настраиваемого поля. Вы должны полностью удалить этот код, как только однажды загрузили любую страницу. Если у вас огромный сайт с кучей постов, вы можете настроить posts_per_pageчто-либо на 100и запускать сценарии пару раз, пока для всех постов не будет установлено настраиваемое поле для всех постов.

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

Теперь, когда настраиваемые поля установлены для всех сообщений, а приведенный выше код удален, нам нужно убедиться, что мы установили это настраиваемое поле для всех новых сообщений или всякий раз, когда мы обновляем заголовок сообщения. Для этого мы будем использовать transition_post_statusкрючок. Следующий код может пойти в плагин ( который я рекомендую ) или в вашемfunctions.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 );

ЗАПРОС ВАШИХ ПОЧТ

Вы можете выполнять свои запросы как обычно без каких-либо пользовательских фильтров. Вы можете запросить и отсортировать свои сообщения следующим образом

$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 );
Питер Гусен
источник
Мне нравится этот подход (может быть, этого достаточно, чтобы убрать запрещенное слово из начала заголовка)
birgire
@birgire Я только пошел с этим, потому что мои знания SQL так же бедны, как у церковной мыши, хахахаха. Спасибо за опечатку
Питер Гусен
1
Остроумная мышь может быть намного более ловкой, чем жестко закодированный SQL-слон ;-)
birgire
0

Ответы birgire хорошо работают при заказе только по этому полю. Я внес некоторые изменения, чтобы он работал при упорядочении по нескольким полям (я не уверен, что он работает правильно, когда упорядочение заголовков является основным):

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 );
Едидель Эльхайяны
источник