Ignorando artigos iniciais (como 'a', 'an' ou 'the') ao ordenar consultas?

12

Atualmente, estou tentando exibir uma lista de títulos de músicas e gostaria que a ordenação ignorasse (mas ainda exibisse) o artigo inicial do título.

Por exemplo, se eu tivesse uma lista de bandas, ela seria exibida em ordem alfabética no WordPress assim:

  • Black Sabbath
  • Led Zeppelin
  • Pink Floyd
  • Os Beatles
  • As dobras
  • Os Rolling Stones
  • Thin Lizzy

Em vez disso, eu gostaria que ele fosse exibido em ordem alfabética, ignorando o artigo inicial 'The', assim:

  • Os Beatles
  • Black Sabbath
  • As dobras
  • Led Zeppelin
  • Pink Floyd
  • Os Rolling Stones
  • Thin Lizzy

Eu encontrei uma solução em uma entrada de blog do ano passado , que sugere a código a seguir em 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, em seguida, agrupando a consulta com add_filter e remove_filter depois.

Eu tentei isso, mas continuo recebendo o seguinte erro no meu site:

  

Erro no banco de dados do WordPress: [Coluna desconhecida 'título2' em 'cláusula de pedido']

     

SELECIONE wp_posts. * FROM wp_posts WHERE 1 = 1 E wp_posts.post_type =   'release' AND (wp_posts.post_status = 'publicar' OU   wp_posts.post_status = 'private') ORDER BY UPPER (title2) ASC

Eu não vou mentir, sou muito novo na parte PHP do WordPress, então não sei por que estou recebendo este erro. Eu posso ver que tem algo a ver com a coluna 'title2', mas eu entendo que a primeira função deve resolver isso. Além disso, se há uma maneira mais inteligente de fazer isso, sou todo ouvidos. Andei pesquisando e pesquisando este site, mas ainda não encontrei muitas soluções.

Meu código usando os filtros é assim, se houver alguma ajuda:

<?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();
?>
    
por rpbtz 07.02.2016 / 02:12

4 respostas

8

O problema

Acho que há um erro de digitação:

O nome do filtro é posts_fields não post_fields .

Isso poderia explicar por que o campo title2 é desconhecido, porque sua definição não é adicionada à string SQL gerada.

Alternativa - Filtro único

Podemos reescrevê-lo para usar apenas um único 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 );

onde você pode ativar a ordem personalizada com o parâmetro _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();

Alternativa - Recursiva TRIM()

Vamos implementar a ideia recursiva por Pascal Birchler , comentado aqui :

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

onde podemos, por exemplo, construir a função recursiva como:

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

Isso significa que

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

irá gerar:

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

Alternativa - MariaDB

Em geral, gosto de usar MariaDB em vez de MySQL . Então é muito mais fácil porque MariaDB 10.0.5 suporta 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 );
    
por birgire 07.02.2016 / 09:34
12

Uma maneira mais fácil pode ser percorrer e alterar o slam de link permanente nas postagens que precisam dele (sob o título na tela de escrita de postagens) e depois usá-lo apenas para fazer pedidos em vez do título.

ie. use post_name não post_title para classificação ...

Isso também significa que seu permalink pode ser diferente se você usar% postname% em sua estrutura de permalink, o que poderia ser um bônus adicional.

por exemplo. dá http://example.com/rolling-stones/ não http://example.com/the-rolling-stones/

EDIT : código para atualizar os slugs existentes, removendo os prefixos indesejados da coluna 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);
            }
        }
    }
}
    
por majick 07.02.2016 / 08:30
6

EDITAR

Eu melhorei o código um pouco. Todos os blocos de código são atualizados de acordo. Apenas uma nota antes de entrar nas atualizações na RESPOSTA ORIGINAL , eu configurei o código para trabalhar com o seguinte

  • Tipo de postagem personalizada - > release

  • Taxonomia personalizada - > game

Certifique-se de definir isso de acordo com suas necessidades

RESPOSTA ORIGINAL

Além das outras respostas e do erro apontado por @birgire, aqui está outra abordagem.

Primeiro, definiremos o título como um campo personalizado oculto, mas primeiro removeremos as palavras como the que gostaríamos de excluir. Antes de fazermos isso, precisamos primeiro criar uma função auxiliar para remover as palavras banidas dos nomes dos termos e postar títulos

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

Agora que temos isso coberto, vamos dar uma olhada no trecho de código para definir nosso campo personalizado. Você deve remover esse código completamente assim que você carregar uma página uma vez. Se você tem um site enorme com várias postagens, pode definir posts_per_page como algo para 100 e executar os scripts algumas vezes até que todas as postagens tenham o campo personalizado definido para todas as postagens

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

Agora que os campos personalizados estão definidos para todas as postagens e o código acima é removido, precisamos nos certificar de que definimos o campo "personalizado" como todas as novas postagens ou sempre que atualizarmos o título da postagem. Para isso, vamos usar o gancho transition_post_status . O código a seguir pode entrar em um plugin ( que eu recomendo ) ou no seu 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 );

QUERYING SEUS POSTS

Você pode executar suas consultas normalmente sem nenhum filtro personalizado. Você pode consultar e classificar suas postagens conforme 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 );
    
por Pieter Goosen 07.02.2016 / 10:01
0

As respostas do birgire funcionam bem ao pedir apenas por este campo. Fiz algumas modificações para que funcionasse ao fazer o pedido por vários campos (não tenho certeza se funciona corretamente quando a ordenação de títulos é a principal):

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 );
    
por Yedidel Elhayany 28.05.2017 / 18:17