Building an Intuitive Resource Filter for WordPress - Part 2

In Part 1 of this series, we discussed the main template, resources.php. In this blogpost we'll have a closer look into the filtering system that allows users to narrow down their search.

Complete files may be viewed at GitHub

Understanding the Filtering Logic

When users browse content, they expect filters to work intuitively. Selecting one filter should automatically update which options make sense in other filters. For example, if users select Blog Posts as a type, they should only see categories and authors with blog posts. Our check_available_results() function makes this possible by determining which filter options should be available based on the current selection.

The core of our filtering system lives in the check_available_results() function. It accepts two parameters: the allowed post types and the current filters that have been applied. Here's how it begins:

function check_available_results($selected_types, $current_filters)
{
  $cache_key = 'available_results_' . md5(serialize($selected_types) . serialize($current_filters));
  $available = wp_cache_get($cache_key);

  if (false === $available) {
    $available = array(
      'types' => array(),
      'authors' => array(),
      'categories' => array()
  );

    $base_args = array(
      'post_status' => 'publish',
      'posts_per_page' => -1,
      'fields' => 'ids'
    );

The function starts by checking if we've already calculated the available options for this particular combination of filters. This caching mechanism is essential because calculating available options requires several database queries. If we've seen this combination before, we can return the cached result instead of recalculating everything.

When someone applies a filter, we need to know what other filter options remain valid. To do this, we apply all current filters except the one we're checking.

Understanding how we determine available options is key to this filtering system. When users make selections, we know their current choice must be valid—otherwise, they could not have selected it. Using this valid selection as our starting point, we only need to find what options are valid for their next choice.

Here's how that works:

if (!empty($current_filters['category']) && 
  isset($current_filters['checking']) && 
  $current_filters['checking'] !== 'category') {
  $base_args['tax_query'] = array(
    array(
      'taxonomy' => 'category',
      'field' => 'slug',
      'terms' => $current_filters['category']
    )
  );
}

if (!empty($current_filters['auth']) && 
  isset($current_filters['checking']) && 
  $current_filters['checking'] !== 'author') {
  $base_args['meta_query'] = array(
    array(
      'key' => 'authored_by',
      'value' => serialize(strval($current_filters['auth'])),
      'compare' => 'LIKE'
      )
  );
}

Let's say a user selected Blog Posts as their content type. Since they could select it, we know blog posts exist in our system. When they want to see available categories, we only need to look within blog posts to find which categories are used. If they then select the Technology category, we know both Blog Posts and Technology are a valid combination. To show them, available authors, we simply look for authors who have written blog posts in the Technology category.

This approach follows the user's natural selection path. Each valid choice helps narrow down the next set of valid options.

if (!isset($current_filters['checking']) || 
  $current_filters['checking'] === 'type') {
  foreach ($selected_types as $post_type) {
    $type_posts = array_filter($filtered_posts, function($post) use ($post_type) {
      return get_post_type($post) === $post_type;
    });
    
    if (!empty($type_posts)) {
      $available['types'][] = $post_type;
    }
  }
}

For categories, we examine each post that matches our current filters and collect all unique categories they belong to:

if (!isset($current_filters['checking']) || 
  $current_filters['checking'] === 'category') {
  foreach ($filtered_posts as $post_id) {
    $post_categories = wp_get_post_categories($post_id, array('fields' => 'all'));
    foreach ($post_categories as $category) {
      if ($category->slug !== 'uncategorized' && 
        !in_array($category->slug, $available['categories'])) {
        $available['categories'][] = $category->slug;
      }
    }
  }
}

Finally, we do the same for authors, checking who has authored the content that matches our current filters:

if (!isset($current_filters['checking']) || $current_filters['checking'] === 'author') {
  foreach ($filtered_posts as $post_id) {
    $author_objects = get_field('authored_by', $post_id);
    if (is_array($author_objects)) {
      foreach ($author_objects as $author) {
        if (is_object($author) && isset($author->ID)) {
          if (!in_array($author->ID, $available['authors'])) {
              $available['authors'][] = $author->ID;
          }
        }
      }
    }
  }
}

After collecting all available options, we cache the results for future use. This means the next time someone applies the same combination of filters, we can return the results instantly without querying the database again.

This filtering system creates a smooth user experience by automatically disabling impossible combinations. If no blog posts are in the Technology category, the option will be grayed out when Blog Posts is selected as the type. Similarly, if an author has never written a case study, their name will be disabled when Case Studies is chosen as the type.

In our next article, we'll explore how these options are displayed in the user interface and how we handle the interaction between different types of filters.

Any comments? Find me on Bluesky.

Scroll to top