web-dev-qa-db-fra.com

Menu Plusieurs taxonomies

J'ai un post_type "produits" qui a les taxonomies "Catégorie" et "Marque". Chaque produit a exactement 1 catégorie et 1 marque.

J'aimerais afficher un menu qui répertorie d'abord les catégories en tant que menu de niveau supérieur, puis les marques contenues dans chacune d'elles en tant que sous-menu.

Chaque sous-menu ne doit afficher que les marques associées aux produits de cette catégorie de premier niveau spécifique.

Les liens dans le sous-menu devraient ensuite aller à une page qui affiche uniquement les produits de la marque et de la catégorie sélectionnées ... mais je pense pouvoir comprendre cette partie avec wp_query. La plupart du temps je suis confus sur la question du menu.

Toute aide est grandement appréciée!

1
Charles

Tout d'abord, vous devez trouver un moyen d'obtenir une terminologie sous forme d'une taxonomie (marque) liée à des messages associés à une autre taxonomie (catégorie).

Le moyen le plus rapide de le faire est le suivant:

  1. obtenir une liste de toutes les catégories
  2. pour chaque catégorie obtenir des produits connexes
  3. produits en boucle et récupération de marques connexes

Ce flux de travail nécessite une boucle imbriquée, ainsi que des requêtes a + b + 1, où a est le nombre de catégories et b est le nombre de produits.

Donc si vous avez, e. g. 200 publications et 20 catégories, cette fonction exécute 221 requêtes de base de données et une boucle imbriquée sur toutes les catégories et toutes les publications.

Notez que nous ne pouvons pas utiliser la pagination, car nous devons parcourir tous les produits . Par conséquent, si vous avez plusieurs milliers de produits, cette fonction peut détruire les performances de votre site.

Que pouvons-nous faire pour améliorer cette situation? 2 choses:

  • Utilisation d'une méthode de requête SQL plus performante pour réduire les requêtes de base de données
  • Utiliser le cache

Pour la première partie, nous pouvons écrire une requête SQL personnalisée, qui utilise les clauses JOIN et WHERE appropriées pour obtenir les termes associés des 2 taxonomies sans impliquer de publications.

Regardez cette fonction:

function term_related_terms( $term, $term_tax, $target_tax, $onlyparent = FALSE ) {
  if ( ! taxonomy_exists( $term_tax ) || ! taxonomy_exists( $target_tax ) ) {
    return FALSE;
  }
  $term_tax_obj = $term_tax_id = FALSE;
  if ( is_numeric( $term ) ) {
    $term_tax_obj = get_term( $term, $term_tax );
  } elseif ( is_string( $term ) ) {
    $term_tax_obj = get_term_by( 'slug', $term, $term_tax );
  } elseif ( is_object( $term ) ) {
    $term_tax_obj = $term;
  }
  if ( is_object( $term_tax_obj ) && isset(  $term_tax_obj->term_taxonomy_id ) ) {
    $term_tax_id = $term_tax_obj->term_taxonomy_id;
  }
  if ( ! $term_tax_id ) return FALSE;
  global $wpdb;
  $query = $wpdb->prepare(
    "SELECT t.term_id, t.name, t.slug, tt.* FROM {$wpdb->terms} t
    INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_id = t.term_id
    INNER JOIN {$wpdb->term_relationships} tr ON tr.term_taxonomy_id = tt.term_taxonomy_id
    INNER JOIN {$wpdb->term_relationships} tr2 ON tr2.object_id = tr.object_id
    INNER JOIN {$wpdb->term_taxonomy} tt2 ON tr2.term_taxonomy_id = tt2.term_taxonomy_id
    WHERE tt.taxonomy = %s AND tt2.taxonomy = %s AND tr2.term_taxonomy_id = %s",
    $target_tax, $term_tax, $term_tax_id
  );
  if ( $onlyparent ) $query .= " AND tt.parent = 0";
  return $wpdb->get_results( $query .= " GROUP BY tt.term_taxonomy_id");
}

Cette fonction obtient 3 arguments obligatoires:

  • un terme (peut être un identifiant de terme, un slug ou un objet)
  • la taxonomie appartient au terme
  • un autre nom de taxonomie

Et renvoyer tous les termes de la deuxième taxonomie associée aux messages ayant le terme donné de la première taxonomie.

L'argument $onlyparent facultatif, comme on peut le deviner, s'il est défini sur true, rend la fonction ne renvoie que les termes de niveau supérieur de la deuxième taxonomie.

Cela signifie que cette fonction est appelée ainsi:

term_related_terms( $cadID, 'category', 'brand' );

nous pouvons obtenir toutes les marques associées aux produits ayant le $cadID donné, en exécutant une seule requête de base de données.

Et en exécutant cette fonction pour toutes les catégories, nous pouvons obtenir toutes les données dont nous avons besoin, en exécutant n + 1 requêtes, où n est le nombre de catégories et la requête supplémentaire consiste à obtenir toutes les catégories.

En utilisant l'exemple précédent (200 produits et 20 catégories), nous devons exécuter 21 requêtes au lieu de 221 et également un cycle simple au lieu d'un cycle imbriqué.

L'amélioration est remarquable, mais les requêtes de 21 db ne sont pas une tâche facile, nous devons mettre en cache les résultats.

Écrivons une fonction qui, à la première exécution, appelle la fonction précédente, met le résultat en cache dans un transitoire et, lors des appels suivants, renvoie les résultats mis en cache:

function category_brand_menu_data() {
  // be sure following slugs match your setup
  $category_slug = 'category';
  $brand_slug = 'brand';
  $cache = get_transient( 'category_brand_menu_data' );  // try to get from cache
  if ( ! empty( $cache ) ) {
    return $cache;
  }
  // firts get the categories
  $cats = get_terms( $category_slug );
  $data = array();
  if ( ! empty( $cats ) ) {
    // get brands related to each category using term_related_terms()
    foreach ( $cats as $cat ) {
      $brands = term_related_terms( $cat, $category_slug, $brand_slug );
      if ( ! empty( $brands ) ) {
        $data[$cat->term_id] = array(
          'name' => $cat->name,
          'slug' => $cat->slug,
          'brands' => $brands
        );
      }
    }
    if ( ! empty( $brands ) ) {
      set_transient( 'category_brand_menu_data', $data ); // cache data for next call
    }
  }
  return $data;
}

Maintenant que nous avons implémenté le cache, les performances ont été considérablement améliorées. Cependant, nous avons besoin d'un mécanisme pour invalider le cache lorsque quelque chose de pertinent se produit.

Pour être plus précis, quelque chose de pertinent est:

  • un produit est associé à une catégorie
  • un produit est associé à une marque
  • l'association entre produit et catégorie est supprimée
  • l'association entre le produit et la marque est supprimée
  • un terme de catégorie est supprimé
  • un terme de marque est supprimé

Les 4 premiers événements peuvent être ciblés à l'aide de l'action 'set_object_terms', qui transmet la taxonomie aux fonctions de raccordement en tant que 4ème argument.

Les 2 événements restants peuvent être ciblés à l'aide de l'action 'delete_term', qui transmet la taxonomie aux fonctions de raccordement en tant que troisième argument.

Écrivons donc une fonction qui invalide le cache (c’est-à-dire supprime le transitoire) et l’ajoutons aux deux points:

function category_brand_menu_clean( $a, $b, $tax_del, $tax_upd = NULL ) {
  // be sure following slugs match your setup
  $category_slug = 'category';
  $brand_slug = 'brand';
  // 'delete_term' pass taxonomy as 3rd argument, 'set_object_terms' as 4th
  $tax = ( current_filter() === 'delete_term' ) ? $tax_del : $tax_upd;
  if ( in_array( $tax, array( $category_slug, $brand_slug ) ) ) {
    delete_transient( 'category_brand_menu_data' );
    category_brand_menu_data();
  }
}

add_action( 'set_object_terms', 'category_brand_menu_clean', 20, 4 );
add_action( 'delete_term', 'category_brand_menu_clean', 20, 3 );

La fonction, après avoir supprimé le cache, appelle category_brand_menu_data() afin que le cache soit reconstitué. Lorsque la fonction sera appelée depuis le frontend, la sortie du cache sera bien exécutée.

Nous avons maintenant une fonction performante qui récupère les associations de termes, et nous n’avons besoin que d’une fonction qui utilise les données récupérées pour afficher le menu:

function category_brand_menu() {
  // be sure following slugs match your setup
  $category_slug = 'category';
  $brand_slug = 'brand';
  // get data (can be cached or not)
  $data = category_brand_menu_data();
  if ( empty( $data ) ) return;
  // set html format according to your needs
  $format = '<nav><ul class="menu">%s</ul></nav>';
  $parentformat = '<li><a href="%s">%s</a><ul class="submenu">%s</ul></li>';
  $itemformat = '<li><a href="%s">%s</a></li>';
  $menu = '';
  $tax_obj = get_taxonomy( $category_slug );
  $query_var = $tax_obj->query_var;
  // loop through data retrieved to build menu html string
  foreach ( $data as $catid => $cat_data ) {
    $cat_link = get_term_link( $catid, $category_slug );
    $items = '';
    foreach( $cat_data['brands'] as $brand ) {
      $t_link = get_term_link( $brand, $brand_slug );
      // add category query arg to brand link, so when link is clicked
      // will show archives having both terms: the category and the brand
      $link = add_query_arg( array( $query_var => $cat_data['slug'] ), $t_link );
      $items .= sprintf( $itemformat, $link, $brand->name );
    }
    $menu .= sprintf( $parentformat, $cat_link, $cat_data['name'], $items );
  }
  // print the menu
  printf( $format, $menu );
}

Dans vos modèles, où vous souhaitez afficher le menu, utilisez simplement la nouvelle balise de modèle comme suit:

<?php category_brand_menu(); ?>

et tu as fini.


Notez que dans toutes les fonctions ci-dessus, j'ai défini 2 variables:

 $category_slug = 'category';
 $brand_slug = 'brand';

parce que je ne sais pas quelles sont les bonnes limaces pour vos taxonomies, avant de tester mon code, assurez-vous de définir les bonnes limaces dans toutes les fonctions.

3
gmazzap

J'ai compris comment extraire toutes les combinaisons de catégories/marques avec une seule requête MySQL.

Ensuite, je passe en revue les combinaisons pour créer un tableau de toutes les catégories et de leurs marques associées. Voici la requête:

$brands_categories = $wpdb->get_results("
    SELECT te2.term_id AS category_id, te2.slug AS category_slug, te2.name AS category_name, t2.parent AS category_parent_id, te1.slug AS brand_slug, te1.name AS brand_name
    FROM wp_posts p

    LEFT JOIN wp_term_relationships r1 ON p.ID = r1.object_id
    CROSS JOIN wp_term_taxonomy t1 ON r1.term_taxonomy_id = t1.term_taxonomy_id AND t1.taxonomy = 'brands'
    LEFT JOIN wp_terms te1 ON t1.term_id = te1.term_id

    LEFT JOIN wp_term_relationships r2 ON p.ID = r2.object_id
    CROSS JOIN wp_term_taxonomy t2 ON r2.term_taxonomy_id = t2.term_taxonomy_id AND t2.taxonomy = 'product_cat'
    LEFT JOIN wp_terms te2 ON t2.term_id = te2.term_id

    WHERE p.post_status = 'publish' and p.post_type = 'product'

    GROUP BY te2.slug, te1.slug

    ORDER BY te2.slug ASC
", ARRAY_A);

Donc ça tire essentiellement

category_id
category_slug
category_name
brand_slug
brand_name

pour toutes les combinaisons.

Ensuite, je passe en revue et crée un tableau clair de catégories, avec les marques répertoriées comme sous-tableaux.

foreach ($brands_categories as $brand_category) {
    if (!is_array($categories[$brand_category['category_id']])) {
        $categories[$brand_category['category_id']] = array(
            "slug" => $brand_category['category_slug'],
            "name" => $brand_category['category_name'],
            "parent" => $brand_category['category_parent_id'],
            "brands" => array()
        );
    }
    $categories[$brand_category['category_id']]['brands'][] = array(
        "slug" => $brand_category['brand_slug'],
        "name" => $brand_category['brand_name']
    );
}

Merci pour l'aide! C'était vraiment beaucoup plus compliqué que cela ne devrait être. Ce serait un problème très simple dans une configuration personnalisée php/mysql.

2
Charles