¿Ignora los artículos iniciales (como 'a', 'an' o 'the') al ordenar las consultas?

12

Actualmente estoy intentando generar una lista de títulos de música y me gustaría que la clasificación ignore (pero aún muestre) el artículo inicial del título.

Por ejemplo, si tuviera una lista de bandas, se mostrará alfabéticamente en WordPress como este:

  • Black Sabbath
  • Led Zeppelin
  • Pink Floyd
  • Los Beatles
  • The Kinks
  • Los Rolling Stones
  • Thin Lizzy

En su lugar, me gustaría que se muestre en orden alfabético e ignore el artículo inicial 'El', como este:

  • Los Beatles
  • Black Sabbath
  • The Kinks
  • Led Zeppelin
  • Pink Floyd
  • Los Rolling Stones
  • Thin Lizzy

Encontré una solución en una entrada de blog del año pasado , que sugiere la siguiente código en 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;
}

y luego envuelve la consulta con add_filter antes y remove_filter después.

He intentado esto, pero sigo recibiendo el siguiente error en mi sitio:

  

Error de la base de datos de WordPress: [Columna desconocida 'title2' en 'cláusula de orden']

     

SELECCIONE wp_posts. * FROM wp_posts WHERE 1 = 1 AND wp_posts.post_type =   'release' AND (wp_posts.post_status = 'publish' OR   wp_posts.post_status = 'private') ORDEN POR SUPERIOR (title2) ASC

No voy a mentir, soy bastante nuevo en la parte de PHP de WordPress, así que no estoy seguro de por qué recibo este error. Puedo ver que tiene algo que ver con la columna 'título2', pero entendí que la primera función debería encargarse de eso. Además, si hay una forma más inteligente de hacerlo, soy todo oídos. He estado buscando en Google y buscando en este sitio, pero realmente no he encontrado muchas soluciones.

Mi código que usa los filtros se ve así si es de alguna ayuda:

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

4 respuestas

8

El problema

Creo que hay un error tipográfico allí:

El nombre del filtro es posts_fields no post_fields .

Eso podría explicar por qué el campo title2 es desconocido, porque su definición no se agrega a la cadena SQL generada.

Alternativa - Filtro único

Podemos reescribirlo para usar un solo 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 );

donde ahora puede activar el pedido personalizado con el 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 - TRIM() recursivo

Implementemos la idea recursiva de Pascal Birchler , comentó aquí :

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

donde podemos, por ejemplo, construir la función 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 );
}

Esto significa que

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

generará:

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

Alternativa - MariaDB

En general, me gusta usar MariaDB en lugar de MySQL . Entonces es mucho más fácil porque MariaDB 10.0.5 admite 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 );
    
respondido por el birgire 07.02.2016 - 09:34
12

Una forma más fácil puede ser pasar y cambiar la barra de enlace permanente en las publicaciones que lo necesitan (debajo del título en la pantalla de publicación de publicaciones) y luego usarla para ordenar en lugar del título.

es decir. use post_name no post_title para ordenar ...

Esto también significaría que tu enlace permanente puede ser diferente si usas% postname% en tu estructura de enlace permanente, lo que podría ser un bono adicional.

por ejemplo. da http://example.com/rolling-stones/ no http://example.com/the-rolling-stones/

EDIT : código para actualizar las babosas existentes, eliminando los prefijos no deseados de la columna 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);
            }
        }
    }
}
    
respondido por el majick 07.02.2016 - 08:30
6

EDITAR

He mejorado un poco el código. Todos los bloques de código se actualizan en consecuencia. Sin embargo, solo una nota antes de saltar a las actualizaciones en la RESPUESTA ORIGINAL , he configurado el código para que funcione con lo siguiente

  • Tipo de publicación personalizada - > release

  • Taxonomía personalizada - > game

Asegúrese de configurar esto de acuerdo con sus necesidades

RESPUESTA ORIGINAL

Además de las otras respuestas y el error tipográfico señalado por @birgire, aquí hay otro enfoque.

Primero, estableceremos el título como un campo personalizado oculto, pero primero eliminaremos las palabras como the que querríamos excluir. Antes de hacerlo, primero debemos crear una función de ayuda para eliminar las palabras prohibidas de los nombres de los términos y los títulos de las publicaciones

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

Ahora que hemos cubierto eso, veamos la parte del código para establecer nuestro campo personalizado. Debe eliminar este código completamente tan pronto como haya cargado una página una vez. Si tienes un sitio enorme con una tonelada de publicaciones, puedes configurar posts_per_page en algo como 100 y ejecutar los scripts un par de veces hasta que todas las publicaciones tengan el campo personalizado configurado en todas las publicaciones

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

Ahora que los campos personalizados están configurados para todas las publicaciones y que se elimina el código anterior, debemos asegurarnos de que configuramos este campo personalizado para todas las publicaciones nuevas o cada vez que actualizamos el título de la publicación. Para ello utilizaremos el gancho transition_post_status . El siguiente código puede ir a un complemento ( que recomiendo ) o en su 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 );

CONSIDERANDO TUS POSTES

Puede ejecutar sus consultas normalmente sin ningún filtro personalizado. Puedes consultar y ordenar tus publicaciones como sigue

$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 );
    
respondido por el Pieter Goosen 07.02.2016 - 10:01
0

las respuestas de birgire funcionan bien cuando se ordenan solo por este campo. Hice algunas modificaciones para que funcione cuando se ordenan por varios campos (no estoy seguro de que funcione correctamente cuando el orden de títulos es el 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 );
    
respondido por el Yedidel Elhayany 28.05.2017 - 18:17

Lea otras preguntas en las etiquetas