在 WordPress 中將分類按照階層排列

目標

假設現在有一篇文章的分類包含以下多項,且彼此之間有階層關係。

  • 關東
    • 東京
      • 代官山
      • 澀谷
    • 神奈川
      • 橫濱

所以我們想要將這些分類,依照原本的階層順序顯示為:關東、東京、代官山、澀谷、神奈川、橫濱。在 WordPress 中預設以name排序:代官山、東京、橫濱、澀谷、神奈川、關東,而其他比較有意義的排序法大概也就是按文章數量countquery查詢工具),但也並非想要的排列。

有些Plugin可以自訂分類排序,例如:

  • WP Term Order
  • Category Order and Taxonomy Terms Order
  • Custom Taxonomy Order

但是能夠改的排序,限為同層分類的排序,例如你可以把自訂把「澀谷」擺到「代官山」前面,或者讓「神奈川」整個群組都比「東京」先出現,依然不是我們要的功能。

只好來實做看看。

取得所有分類

首先要取得某篇文章的所選分類,可以用get_the_category來取得一個包含所有分類項目 Object 的 Array,再利用array_map把這些分類的名稱單獨挑出來。

$object_id = get_the_ID();
$categories = get_the_category( $object_id );

$terms = implode( '、', array_map(function($item) { return $item->name; }, $categories ));
echo $terms; // 代官山、東京、橫濱、澀谷、神奈川、關東 

或者,也可以使用wp_get_object_terms來取得任意自訂分類。

$object_id = get_the_ID();
$taxonomies = 'category'; // 這邊我們就選擇預設分類
$args = array( 'fields' => 'all' ); // options arguments
$categories = wp_get_object_terms( $object_id, $taxonomies, $args );

$terms = implode( '、', array_map(function($item) { return $item->name; }, $categories ));
echo $terms; // 代官山、東京、橫濱、澀谷、神奈川、關東 

我們發現:欸?階層不見了,最上層的「關東」表示憤怒竟然排在最後!!!先不要管為什麼,但接下來我們就要利用分類項目中的parent屬性,來為每個項目進行認親的動作,以達到優先以階層排序的目的。

為分類找階層排序

懶得看文章的話程式碼在這裡,使用方法就是把wp_get_object_terms改成get_object_terms_hierarchical就可以惹。

<?php
/**
  * Make terms retrieved by wp_get_object_terms() organized hierarchically.
  *
  * Use this function instead of wp_get_object_terms() to get a hierarchically
  * structured array or term data.
  *
  * This function takes the terms returned by wp_get_object_terms() and organizes
  * them into a hierarchical array with the parent's children (and grandchildren)
  * stored in nested arrays (within the 'children' item).
  *
  * @see wp_get_object_terms() for parameters.
  *
  * @return array $terms Hierarchilized $terms array.
  */
function get_object_terms_hierarchical( $object_ids, $taxonomies, $args = array() ) {
  $return_flat = true;
  $tree  = array();
  $terms = wp_get_object_terms( $object_ids, $taxonomies, $args );
  $ids = array_map(function($item) { return $item->term_id; }, $terms);
  if ( ! empty( $terms ) ) {
    foreach ( $terms as $term ) {
      if ( $term->parent == 0 || !in_array($term->parent, $ids) ) {
        if ( $return_flat ) {
          array_push($tree, $term);
          get_child_terms( $term->term_id, $terms, $tree );
        } else {
          $tree[ $term->term_id ] = $term;
          $tree[ $term->term_id ]->children = get_child_terms( $term->term_id, $terms );
        }
      }
    }
  }
  return $tree;
}
  
/**
 * Organizes the child terms.
  *
  * This is a recursive function.
  *
  * @param interval $parent_id The parent ID to retrieve the children terms of.
  * @param array  $terms The term data.
  *
  * @return array Returns nested child terms.
  */
function get_child_terms( $parent_id, $terms, &$order_terms = null ) {
  $children = array();
  foreach ( $terms as $term ) {
    if ( $term->parent == $parent_id ) {
      if ( $order_terms ) {
        array_push($order_terms, $term );
        get_child_terms( $term->term_id, $terms, $order_terms );
      } else {
        $children[ $term->term_id ] = $term;
        $children[ $term->term_id ]->children = get_child_terms( $term->term_id, $terms );
      }
    }
  }
  return $children;
} 

第 17 行的$return_flat設定輸出為單層 Array 或者維持階層,設定為false的話子項目會存在children這個 key 裡。

測試看看。

$object_id = get_the_ID();
$taxonomies = 'category'; // here we use the default
$args = array( 'fields' => 'all' ); // options arguments

$categories = wp_get_object_terms( $object_id, $taxonomies, $args );
$terms = implode( '、', array_map(function($item) { return $item->name; }, $categories ));
echo $terms; // 關東、東京、代官山、澀谷、神奈川、橫濱 

關東終於拿下他該有的位置了,很好!我們搞定。

解釋一下

事實上在後台設定了分類間的階層關係,也只會在 WordPress 資料庫中新增一個parent欄位描述某個分類的上層id,所以資料本身是沒有分層的,我們必須再自己重新配對好它們的從屬關係才行。

事實上,Wordpress 自己則是用了一個叫 Walker 類的方法,也一樣可以取得正確的階層分布,其基本原理也是會去「走」遍所有分類去做認親。

$object_id = get_the_ID();
$categories = get_the_category( $object_id );
$cat_ids = array_map( function($item) { return $item->term_id; },  $categories );
$args = array(
    'include' => $cat_ids,// 我們只取文章有使用到的分類 id 們
    'hierarchical' => true,
    'title_li' => '',   // 移除第一個大項目寫「分類」
    'style' => '',      // 設定成 list 以外的字串以避免輸出 ul>li
    'separator' => '、' // 將預設的 <br /> 改為中文頓號 
);
$terms = wp_list_categories( $args );
echo $terms; // 代官山、東京、橫濱、澀谷、神奈川、關東 

成功!那你說我用wp_list_categories就好啦?不行。

假如今天沒有選用任何最上層分類的話,這裡就會出現 bug。我們考慮同樣的分類階層,例如有一篇文章沒有勾選「關東」,只選用了其中的

  • 神奈川
    • 橫濱

這時候用 WordPress 的內建函式wp_list_categories會得到

echo $terms; // 橫濱、神奈川 

那欸阿餒?改用get_object_terms_hierarchical看看。

echo $terms; // 神奈川、橫濱 

成功啦!

參考資料

  1. EricBusch/get_object_terms_hierarchical
  2. WordPress/get_the_category
  3. WordPress/wp_get_object_terms
  4. WordPress/wp_list_categories