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

13

Actualmente estoy tratando de 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 así:

  • Sábado negro
  • Led Zeppelin
  • Pink Floyd
  • Los Beatles
  • Los torceduras
  • Los Rolling Stones
  • Delgado lizzy

En cambio, me gustaría que se muestre alfabéticamente mientras se ignora el artículo inicial 'The', así:

  • Los Beatles
  • Sábado negro
  • Los torceduras
  • Led Zeppelin
  • Pink Floyd
  • Los Rolling Stones
  • Delgado lizzy

Encontré una solución en una entrada de blog del año pasado , que sugiere el 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 ajustando la consulta con add_filterantes yremove_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 'título2' en 'cláusula de pedido']

SELECCIONE wp_posts. * DESDE wp_posts DONDE 1 = 1 Y wp_posts.post_type = 'release' AND (wp_posts.post_status = 'publicar' O wp_posts.post_status = 'privado') ORDEN POR SUPERIOR (título2) ASC

No voy a mentir, soy bastante nuevo en la parte 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 ocuparse de eso. Además, si hay una forma más inteligente de hacer esto, soy todo oídos. He estado buscando y buscando en este sitio, pero realmente no he encontrado muchas soluciones.

Mi código usando 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();
?>
rpbtz
fuente
1
Una solución alternativa podría ser almacenar el título por el que desea ordenar como metadatos de publicación y ordenar en ese campo en lugar de título.
Milo
No estoy seguro de cómo seguir con eso. ¿No almacenar eso en una nueva columna daría como resultado un error similar al que estoy recibiendo actualmente?
rpbtz
1
no usaría ninguno de ese código, puede consultar y ordenar en meta meta con parámetros de meta consulta .
Milo

Respuestas:

8

El problema

Creo que hay un error tipográfico allí:

El nombre del filtro posts_fieldsno es post_fields.

Eso podría explicar por qué el title2campo 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 _customparámetro 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()

Pongamos en práctica la idea recursiva de Pascal Birchler , comentada 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 );
Birgire
fuente
Creo que esto debería resolver el problema mejor que mi solución
Pieter Goosen
Tenías toda la razón: cambiar post_fields por posts_fields solucionó el problema y ahora está ordenando exactamente como lo quiero. ¡Gracias! Me siento un poco estúpido ahora, ya que ese era el problema. Eso es lo que obtengo por codificar a las 4AM, supongo. Examinaré también la solución de filtro único. Parece una muy buena idea. Gracias de nuevo.
rpbtz
Marcaré esto como la respuesta correcta, ya que es la que está más relacionada con mis preguntas iniciales, aunque, por lo que puedo decir, las otras respuestas también son soluciones válidas.
rpbtz
La alternativa de filtro único también funcionó de maravilla. Ahora puedo mantener el código del filtro functions.phpy llamarlo orderbycuando lo necesite. Gran solución - gracias :-)
rpbtz
1
Me alegra saber que funcionó para ti: agregué el método recursivo. @rpbtz
birgire
12

Una forma más fácil puede ser revisar y cambiar el enlace permanente de las publicaciones que lo necesitan (debajo del título en la pantalla de escritura de publicaciones) y luego usarlo para ordenar en lugar del título.

es decir. post_nameno usar post_titlepara clasificar ...

Esto también significaría que su enlace permanente puede ser diferente si usa% postname% en su estructura de enlace permanente, lo que podría ser una ventaja adicional.

p.ej. http://example.com/rolling-stones/ no dahttp://example.com/the-rolling-stones/

EDITAR : código para actualizar las babosas existentes, eliminando los prefijos no deseados de la post_namecolumna ...

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
fuente
Gran solución: muy simple y eficiente para la clasificación.
BillK
La solución de error tipográfico de @birgire funcionó de maravilla, pero esta parece una alternativa decente. Iré con el otro por ahora, ya que hay bastantes publicaciones consultadas con un artículo inicial y cambiar todas las babosas de enlace permanente puede llevar algo de tiempo. Sin embargo, me gusta la simplicidad de esta solución. Gracias :-)
rpbtz
1
ya que le gustó, agregó un código que debería cambiar todas las babosas si lo desea / necesita. :-)
majick
6

EDITAR

He mejorado un poco el código. Todos los bloques de código se actualizan en consecuencia. 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 a 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 theque queremos excluir. Antes de hacer eso, primero debemos crear una función auxiliar para eliminar las palabras prohibidas de los nombres de términos y títulos de 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 lo tenemos cubierto, veamos el código para establecer nuestro campo personalizado. Debe eliminar este código por completo tan pronto como haya cargado alguna página una vez. Si tiene un sitio enorme con una tonelada de publicaciones, puede configurar posts_per_pagealgo 100y 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 se elimina el código anterior, debemos asegurarnos de configurar este campo personalizado para todas las publicaciones nuevas o cada vez que actualicemos el título de la publicación. Para esto usaremos el transition_post_statusgancho. El siguiente código puede ir a un complemento ( que recomiendo ) o en sufunctions.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 );

CONSULTA TUS PUBLICACIONES

Puede ejecutar sus consultas de forma normal sin ningún filtro personalizado. Puede consultar y ordenar sus publicaciones de la siguiente manera

$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
fuente
Me gusta este enfoque (tal vez sea suficiente para eliminar la palabra prohibida desde el comienzo del título)
birgire
@birgire Solo fui con esto porque mi conocimiento de SQL es tan pobre como el mouse de una iglesia, jajajaja. Gracias por el error tipográfico
Pieter Goosen
1
El ingenioso mouse puede ser mucho más ágil que el elefante SQL codificado ;-)
birgire
0

Las respuestas de birgire funcionan bien cuando se ordena solo por este campo. Hice algunas modificaciones para que funcione al ordenar por múltiples 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 );
Yedidel Elhayany
fuente