WordPress taxonomy filter

For one of my projects I needed to create a taxonomy filter for a webshop to filter by terms like width, height and other features of a product. The filter should also be able to function for different kind of product categories. So in my case I had one taxonomy called “shop-category” which was meant to create multiple categories in my webshop and in these sections you are able to filter by features like width and height for example.

So an example filter url looks like: https://www.webshop.com/shop/shop-category/wooden-boxes/width/90-cm/height/180-cm/

  • The /shop/ part is the post type “shop”.
  • The /shop-category/wooden-boxes/ part is the taxonomy “shop-category” with the category (term) “Wooden boxes”.
  • The /width/90-cm/ part is the “width” taxonomy with a “90 cm” term.
  • The /height/180-cm/ part is the “height” taxonomy with a “180 cm” term.

So created the following post type and taxonomies like:

// Register shop post type
function theme_register_post_types() {
   
   $args = array(
      'label'                 => __( 'Shop', 'theme_name' ),
      'description'           => __( 'A shop for standard products', 'theme_name' ),
      'supports'              => array( 'title', 'editor', 'excerpt', 'author', 'thumbnail', 'custom-fields', ),
      'hierarchical'          => false,
      'taxonomies'	      => array( 'shop-category' ),
      'public'                => true,
      'show_ui'               => true,
      'show_in_menu'          => true,
      'menu_position'         => 5,
      'menu_icon'             => 'dashicons-cart',
      'show_in_admin_bar'     => true,
      'show_in_nav_menus'     => true,
      'can_export'            => true,
      'has_archive'           => true,
      'exclude_from_search'   => false,
      'publicly_queryable'    => true,
      'capability_type'       => 'page',
   );
   register_post_type( 'shop', $args );
}

add_action( 'init', 'theme_register_post_types', 0 );

// Shop related taxonomies
$shop_taxonomies = array(

   // Shop category
   'shop-category' => array(
      'labels' => array(
         // Labels
      ),
      'hierarchical' => true
   ),

   // Width
   'width' => array(
      'labels' => array(
         // Labels
      ),
      'rewrite' => array(
         'slug' => _x( 'width', 'Taxonomy rewrite slug', 'theme_name' )
      ),
   ),

   // Height
   'height' => array(
      'labels' => array(
         // Labels
      ),
      'rewrite' => array(
         'slug' => _x( 'height', 'Taxonomy rewrite slug', 'theme_name' )
      ),
   )

   // Other shop filter taxonomies
);

// Register the taxonomies
foreach ( $shop_taxonomies as $taxonomy => $values ) {

   $args = array(
      'labels'		      => $values['labels'],
      'public'                => true,
      'show_ui'               => true,
      'show_admin_column'     => true,
      'show_in_nav_menus'     => true,
   );

   if ( isset( $values['hierarchical'] ) ) {
      $args['hierarchical'] = $values['hierarchical'];
   }

   if ( isset( $values['rewrite'] ) ) {
      $args['rewrite'] = $values['rewrite'];
   }

   register_taxonomy( $taxonomy, array( 'shop' ), $args );

}

Custom rewrite rules

It was important for me to have my url structured and workable so that the “shop” post type was on the first place, followed by the “shop-category” taxonomy with the right term and as last the other filter taxonomies with corresponding term.

To achieve this I needed to create some rewrite rules so that WordPress understands which filters are in use. The post type and shop-category taxonomy are always on the same place, but the other filter taxonomies must be able to operate in different places in the url because you do not know in what order a user uses the filters.

/**
 * Adds rewrite rules for shop taxonomy filters
 */
function gtp_add_shop_rewrite_rules() {

   global $wp_rewrite;

   // Get all taxonomy names related to shop post type
   $taxonomies = get_object_taxonomies( 'shop' );
   $index = 0;

   // Adds taxonomy filter rewrite rules
   for ( $i = 1; $i <= count( $taxonomies ); $i ++ ) { $index ++; // addslashes rewrite regex with all taxonomy filters $regex[] = '(' . implode( '|', $taxonomies ) . ')/(.+?)/'; // Adds rewrite redirect $redirect[] = $wp_rewrite->preg_index( $index ) . '=' . $wp_rewrite->preg_index( $index + 1 );

      // Adds regex and redirect to new rewrite rules array
      $new_rules['shop/' . implode( '', $regex ) . '?$'] = 'index.php?post_type=shop&' . implode( '&', $redirect );

      // With page number
      $new_rules['shop/' . implode( '', $regex ) . 'page/([0-9]*)/?$'] = 'index.php?post_type=shop&' . implode( '&', $redirect ) .  '&paged=' . $wp_rewrite->preg_index( $index + 2 );

      $index ++;
   }

   // Product feed rewrte rules
   $new_rules['shop/shop-category/(.+?)/feed/(.+?)/?$'] = 'index.php?post_type=shop&shop-category=' . $wp_rewrite->preg_index( 1 ) . '&feed=' . $wp_rewrite->preg_index( 2 );


   // Add new rewrite rules to rewrite rules array
   $wp_rewrite->rules = array_reverse( $new_rules ) + $wp_rewrite->rules;

}
add_action( 'generate_rewrite_rules', 'gtp_add_shop_rewrite_rules' );

The biggest part of the job was to create the filter functionality itself. I made 2 classes. One for initialise the filter and one for each filter item in this filter.

Taxonomy filter class

class TaxonomyFilter {

   // Post type for getting the right posts
   public $postType;

   // The taxonomy on which term pages the filter will be used
   public $sectionTaxonomy;

   /**
    * Initializes the object
    *
    * @param string $postType to get posts from this post type
    * @param string $sectionTaxonomy to get posts from this taxonomy
    */
   public function __construct( $postType = 'shop', $sectionTaxonomy = 'shop-category' ) {
      $this->postType = $postType;
      $this->sectionTaxonomy = $sectionTaxonomy;
   }

   /**
    * Gets the posts
    *
    * To get the right taxonomies and terms to create the filters
    * related to these queried posts
    */
   public function getPosts( $fixedSectionTerm = null ) {

      global $wp_query;

      if ( ! empty( $fixedSectionTerm ) ) {
        $term = $fixedSectionTerm;
      } elseif ( get_query_var( $this->sectionTaxonomy ) ) {
        $term = get_query_var( $this->sectionTaxonomy );
      }

      // Get the right term
      if ( ! empty( $term ) ) {

         // Set arguments
         $args = array(
            'post_type'       => $this->postType,
            'posts_per_page'  => -1,
            'tax_query'       => array(
               array(
                  'taxonomy'  => $this->sectionTaxonomy,
                  'field'     => 'slug',
                  'terms'     => $term
               )
            )
         );

         // If filter are set
         if ( ! empty( $wp_query->tax_query->queries ) ) {

            foreach ( $wp_query->tax_query->queries as $query ) {

               // Get taxonomy by rewrite slug
               if ( $taxonomy = get_taxonomy( $query['taxonomy'] ) ) {

                  // Add tax query
                  $args['tax_query'][] = array(
                     'taxonomy'  => $taxonomy->query_var,
                     'field'     => 'slug',
                     'terms'     => get_query_var( $taxonomy->query_var ),
                  );
               }
            }
         }

         // Query posts
         $posts = get_posts( $args );

         return $posts;

      }
      return false;

   }

   /**
    * Gets filters
    *
    * Gets the right filters related to the queried posts
    * and creates a structure to be able to show the filters as a navigation
    */
   public function getFilters( $fixedSectionTerm = null ) {

      // Get posts to get related taxonomies and terms
      if ( $posts = $this->getPosts( $fixedSectionTerm ) ) {

         foreach ( $posts as $post ) {

            // Get post's related taxonomies
            if ( $taxonomies = get_post_taxonomies( $post->ID ) ) {

               // Unset main taxonomy which is not a filter
               $sectionTaxIndex = array_search( $this->sectionTaxonomy, $taxonomies );
               unset( $taxonomies[$sectionTaxIndex] );

               foreach ( $taxonomies as $taxonomy ) {

                  // Get terms of each post
                  if ( $terms = get_the_terms( $post->ID, $taxonomy ) ) {

                     foreach ( $terms as $term ) {

                        // Get taxonomy name
                        $taxName = get_taxonomy( $taxonomy )->labels->singular_name;

                        // If taxonomy or term not exists in array
                        if ( empty( $filters[$taxName] ) || ! in_array( $term->term_id, array_keys( $filters[$taxName] ) ) ) {

                           // Add item object
                           $filters[$taxName][$term->term_id] = new TaxonomyFilterItem( $term->term_id, $this->sectionTaxonomy, $taxonomy, $this->postType );

                        }
                     }
                  }
               }
            }
         }
      }

      // If there are filters
      if ( ! empty( $filters ) ) {

         foreach ( $filters as $taxonomy => $items ) {

            // Sort the filter items by name
            uasort( $filters[$taxonomy], function( $a, $b ) {

               if ( $a->getName() == $b->getName() ) {
                  return 0;
               }
               return ( $a->getName() < $b->getName() ) ? 1 : -1;

            });
         }

         return $filters;
      }

      return false;

   }

}

Taxonomy filter item class

class TaxonomyFilterItem {

   // Stores term object
   protected $_term;

   // The taxonomy on which term pages the filter will be used
   protected $_sectionTaxonomy;

   // Item filter taxonomy
   protected $_taxonomy;

   // Item Post type
   protected $_postType;

   // Item classes
   protected $_class;

   /**
    * Initializes the object
    *
    * @param int $id to get the term object
    * @param string $sectionTaxonomy to create the url
    * @param string $taxonomy to get the taxonomy object
    * @param string|array $class adding extra classes to item
    */
   public function __construct( $id = 0, $sectionTaxonomy = 'shop-category', $taxonomy = null, $postType = 'shop', $class = [] ) {
      $this->_term            = get_term( $id, $taxonomy );
      $this->_sectionTaxonomy = $sectionTaxonomy;
      $this->_taxonomy        = $taxonomy;
      $this->_postType        = $postType;
      $this->_classes         = $class;
   }

   /**
    * Returns item's (term) name
    */
   public function getName() {
      return $this->_term->name;
   }

   /**
    * Returns item's url
    */
   public function getUrl( $urlAddition = [] ) {

      // Get taxonomy object
      $taxonomy = get_taxonomy( $this->_taxonomy );

      // Get taxonomy rewrite slug for a nice (translated) url
      $slug = $taxonomy->name;

      // Creates item url
      $url = $this->createUrl( $slug, $this->_term->slug, $urlAddition );

      return $url;

   }

   protected function createUrl( $taxonomySlug, $term, $urlAddition = [] ) {

      global $wp_query;

      // If there is already a filter running for this taxonomy
      if ( isset( $wp_query->query_vars[$taxonomySlug] ) ) {

         // And the term for this URL is not already being used to filter the taxonomy
         if ( strpos( $wp_query->query_vars[$taxonomySlug], $term ) === false ) {

            // Append the term
            $filterQuery = $taxonomySlug . '/' . $wp_query->query_vars[$taxonomySlug] . '+' . $term;

        } else {

            // Otherwise, remove the term
            if ( $wp_query->query_vars[$taxonomySlug] == $term ) {
                $filterQuery = '';

            } else {

                $filter = str_replace( $term, '', $wp_query->query_vars[$taxonomySlug] );

                // Remove any residual + symbols left behind
                $filter = str_replace( '++', '+', $filter );
                $filter = preg_replace( '/(^\+|\+$)/', '', $filter );
                $filterQuery = $taxonomySlug . '/' . $filter;
            }
        }

      } else {
         $filterQuery = $taxonomySlug . '/' . $term;
      }

      // Maintain the filters for other taxonomies
      if ( isset( $wp_query->tax_query ) ) {

         $existingQuery = '';

         foreach ( $wp_query->tax_query->queries as $query ) {

            $tax = get_taxonomy( $query['taxonomy'] );

            // Have we already handled this taxonomy?
            if ( $tax->query_var == $taxonomySlug )
                continue;

            // Make sure taxonomy hasn't already been added to query string
            if ( strpos( $existingQuery, $tax->query_var ) === false )
                $existingQuery .= $tax->query_var . '/' . $wp_query->query_vars[$tax->query_var] . '/';
         }

      }

      if ( isset( $existingQuery ) ) {
         $filterQuery = $existingQuery . $filterQuery;
      }

      if ( ! empty( $urlAddition ) ) {
        $addition = rtrim( implode( '/', $urlAddition ), '/' ) . '/';
      } else {
        $addition = '';
      }

      return trailingslashit( get_post_type_archive_link( $this->_postType ) . $addition . $filterQuery );
   }

   /**
    * Returns item's class
    */
   public function getClass() {

      global $wp_query;

      // Get taxonomy object
      $taxonomy = get_taxonomy( $this->_taxonomy );

      // Get taxonomy rewrite slug
      $slug = $taxonomy->name;

      // No classes
      $class = [];

      // If this filter is active, current class will be added
      if ( ! empty( $wp_query->query_vars[$slug] ) && $this->_term->slug == $wp_query->query_vars[$slug] ) {
         $class[] = 'current';
      }

      if ( ! empty( $this->_class ) ) {
         $class = array_merge( $class, $this->_class );
      }

      return ! empty( $class ) ? implode( ' ', $class ) : false;

   }

   /**
   * Adds item's classes
   */
   public function addClass( $class = [] ) {

      if ( is_array( $class ) ) {
         $this->_class = array_merge( $this->_class, $class );
      } else {
         $this->_class[] = $class;
      }

   }

}

In action

In your shop-archive.php you can do something like below to show the filter:

<?php
// Initializes taxonomy filter
$filter = new TaxonomyFilter();

if ( $filters = $filter->getFilters() ) { ?>

   <nav class="filter">

      <?php
      foreach ( $filters as $heading => $items ) {

         $count = 0; ?>

         <div class="nav-part">

            <h2 class="heading"><?php echo $heading; ?></h2>
            <ul class="list">
               <?php
               foreach ( $items as $item ) {
                  echo '<li class="' . $item->getClass() . '"><a href="' . $item->getUrl() . '">' . $item->getName() . '</a></li>';
               }
               ?>
            </ul><!--End .list-->

         </div><!--End .nav-part-->

         <?php
      }
      ?>
   </nav><!--End .side-navigation-->
<?php
}
?>