2

So I have a Custom Post Type with some Custom Taxonomies.

On the archive page where I list all the Custom Post Types I have an refine search in the sidebar which allows you to tick a Taxonomy Term and then search for Post Types with that Term. (Image Below)

Redefine Search

When you click say 'Cellar' it reloads the page and shows you the result (in this instance it's only one Post Type)

My question is: How do I update the numbers on the side of the names so if 'Cellar' is checked then the number next to 'Carpet' should update to say (1) because there is only 1 property with the 'Cellar' and 'Carpet' term together

Here is the code controlling the output of the number but at the moment this is just static and is just count every Post Type with that term.

<?php
$all_features = get_terms('features');

/* Features in search query */
$required_features_slugs = array();
if( isset ( $_GET['features'] ) ) {
    $required_features_slugs = $_GET['features'];
}

$features_count = count ($all_features);
if($features_count > 0) {
    ?>
    <div class="more-options-wrapper clearfix">
        <?php
        foreach ($all_features as $feature ) {
            ?>
            <div class="option-bar">
                <input type="checkbox"
                       id="feature-<?php echo $feature->slug; ?>"
                       name="features[]"
                       value="<?php echo $feature->slug; ?>" onclick="document.getElementById('refine-properties').submit();"
                    <?php if ( in_array( $feature->slug, $required_features_slugs ) ) { echo 'checked'; } ?> />
                <label for="feature-<?php echo $feature->slug; ?>"><?php echo $feature->name; ?> <small>(<?php echo $feature->count; ?>)</small></label>
            </div>
            <?php
        }
        ?>
    </div>
<?php
}
?>

I have put an example below:

enter image description here enter image description here

The Images above represent what I am trying to do

1

One way which would seem inefficient ( but at the moment I cannot think of a way without re-querying the database ) is to Query each term as you loop. You'll be making many more queries so we'll limit what is returned to try and speed them up.

if ( $features_count > 0 ) {
  ?>

    <div class="more-options-wrapper clearfix">

      <?php foreach ($all_features as $feature ) {
            $count = $feature->count;

            if( ! empty( $required_features_slugs ) ) {
                $tmp_required = $required_features_slugs;

                if( ! in_array( $feature->slug, $required_features_slugs ) ) {
                    array_push( $tmp_required, $feature->slug );
                }

                $tmp_query = new WP_Query( array(
                    'posts_per_page'    => -1,
                    'fields'            => 'ids',           // Only return post IDs
                    'tax_query'         => array( array(
                        'taxonomy'  => 'features',
                        'field'     => 'slug',
                        'terms'     => $tmp_required,
                    ) ),
                ) );

                $count = ( $tmp_query->have_posts() ) ? count( $tmp_query->posts ) : 0;
            }
      ?>

            <div class="option-bar">
                <input type="checkbox"
                       id="feature-<?php echo $feature->slug; ?>"
                       name="features[]"
                       value="<?php echo $feature->slug; ?>" onclick="document.getElementById('refine-properties').submit();"
                    <?php if ( in_array( $feature->slug, $required_features_slugs ) ) { echo 'checked'; } ?> />
                <label for="feature-<?php echo $feature->slug; ?>"><?php echo $feature->name; ?> <small>(<?php echo $count; ?>)</small></label>
            </div>

      <?php } ?>

    </div>

  <?php
}

What we're doing is that if one or more categories are selected we're going to loop through the categories and append the current slug. Then we're querying the database for posts that have our required categories plus the current category. Finally, we only return post IDs for speed purposes - if we have posts we count them otherwise it's 0.

  • Thanks for your reply, I will test it out in a bit and let you know of any complications I get. – Stephen Aug 2 '16 at 18:46
  • Okay so just tested the code and no result - I click a checklist the form submits but the numbers don't seem to update – Stephen Aug 2 '16 at 19:58
  • @CoderSte Whoops, I'm saving the $count variable but not displaying it in the correct place - see updated code. – Howdy_McGee Aug 2 '16 at 20:02
  • Coming back to this after having a day away from it. Is there no way to decrease the numbers? Like in the bottom images I provided you see Alarm starts with 36 but then I selected 2 taxonomies and it decreases to 6 because there are only 6 properties with the 3 selected taxonomies together – Stephen Aug 4 '16 at 9:46
1

Theory

Term association counts are stored statically in the database along with their respective terms. This means that there is no way to determine how many posts share two or more terms simply by examining the terms themselves.

I haven't tested or debugged this solution at this point - nor am I sure that it would be any more efficient than Howdy_McGee's answer - but I found the problem interesting and wanted to see where it took me.

The idea was to mitigate the expense of multiple post queries every page load by tracking changes to each property's features when a property is created/updated, then storing the IDs of every property associated with a feature in that feature's term metadata. Counting the items in the intersection of relevant features' property lists then yields the number of properties which share those features.

In principal, I would think that this method would be more efficient than running a post query for every term every page-load. But in practice I think it will choke harder when dealing with substantially large collections of features and/or properties.


(Preliminary) Implementation

Changes in a property's associated features can be tracked by comparing previous terms store in post metadata against the property's current terms. When a change is detected, the relevant feature terms' metadata lists of associated properties can then be updated:

add_action( 'save_post', 'wpse233841_update_property_feature_lists', 10, 2 );

function wpse233841_update_property_feature_lists( $post_id, $post ) {
  if ( wp_is_post_revision( $post_id ) )
    return;

  if( 'property' !== $post->post_type )
    return;

  $previous_feature_ids = get_post_meta( $post_id, '_previous_features', true );

  if( $previous_feature_ids )
    $previous_feature_ids = maybe_unserialize( $previous_feature_ids );
  else 
    $previous_feature_ids = [];

  $feature_ids = wp_get_object_terms(
    $post_id,
    'features',
    [
      'fields' => 'ids'
    ]
  );
  sort( $feature_ids );

  if( $previous_feature_ids == $feature_ids )
    return;

  $added = array_diff( $feature_ids, $previous_feature_ids );
  $removed = array_diff( $previous_feature_ids, $feature_ids );

  foreach( $added as $added_feature_id )
    wpse233841_feature_add_property( $added_feature_id, $post_id );

  foreach( $removed as $removed_feature_id )
    wpse233841_feature_remove_property( $removed_feature_id, $post_id );

  update_post_meta( $post_id, '_previous_features', $feature_ids );
}

function wpse233841_feature_add_property( $feature_id, $property_id ) {
  $property_ids = get_term_meta( $feature_id, '_properties', true );

  if( $property_ids )
    $property_ids = maybe_unserialize( $property_ids );
  else
    $property_ids = [];

  $property_ids[] = $property_id;

  update_term_meta( $feature_id, '_properties', $property_ids );
}

function wpse233841_feature_remove_property( $feature_id, $property_id ) {
  $property_ids = get_term_meta( $feature_id, '_properties', true );

  if( ! $property_ids )
    return;

  $property_ids = maybe_unserialize( $property_ids );
  $property_ids = array_diff( $property_ids, [$property_id] );

  update_term_meta( $feature_id, '_properties', $property_ids );
}

(I can't remember if I'm handling my metadata correctly - It's been a while).

Now, every feature's associated properties list can be quickly obtained through term meta queries instead of querying posts. These lists could even be easily be packed into JSON and delivered to the browser, enabling instantaneous filtering (minus actually querying relevant properties). Property lists in-hand, getting a list of properties that have any given set of features becomes a matter of intersecting the relevant arrays:

function wpse233841_get_properties_by_features( $feature_slugs ) {
  static $feature_properties = [];

  if( empty( $feature_properties ) ) {
    $all_feature_ids = get_terms([
        'taxonomy' => 'features',
        'fields' => 'id=>slug',
        'hide_empty' => false
    ]);

    foreach ($ all_feature_ids as $feature_id => $feature_slug ) {
      $property_ids = get_term_meta( $feature_id, '_properties', true );
      $feature_properties[ $feature_slug ] = maybe_unserialize( $property_ids );
    }
  }

  return call_user_func_array(
    'array_intersect',
    array_intersect_key( $feature_properties, array_flip( $feature_slugs ) )
  );
}

function wpse233841_get_feature_property_count( $feature_slug, $filter_features = [] ) {
  if( !in_array( $feature_slug, $filter_features ) )
    $filter_features[] = $feature_slug;

  return count( wpse233841_get_properties_by_features( $feature_slugs ) );
}

In your template then, relevant property counts can be obtained by calling wpse233841_get_feature_property_count() with a "subject" feature slug as the first argument, and an array of features to filter the count by as the second argument:

<?php
$all_features = get_terms('features');

/* Features in search query */
$required_features_slugs = array();
if( isset ( $_GET['features'] ) ) {
    $required_features_slugs = $_GET['features'];
}

$features_count = count ($all_features);
if($features_count > 0):
?>
  <div class="more-options-wrapper clearfix">
    <?php
      foreach ( $all_features as $feature ) {
        $cb_atts = [
          'id'      => 'feature-' . $feature->slug,
          'name'    => 'features[]',
          'value'   => $feature->slug,
          'onclick' => 'document.getElementById(\'refine-properties\').submit();'
        ];

        $cb_atts_string = '';
        foreach( $cb_atts as $key => $value )
          $cb_atts_string .= ' ' . $key . '="' . $value . '"';

        if( in_array( $feature->slug, $required_features_slugs ) )
          $cb_atts_string .= ' checked ';
    ?>
    <div class="option-bar">
      <input type="checkbox" <?= $cb_atts_string ?> />
      <label for="<?= $cb_atts['id'] ?>"><?= $feature->name ?> <small>(<?= wpse233841_get_feature_property_count( $feature->slug, $required_features_slugs ) ?>)</small>
      </label>
    </div>
    <?php
      }
    ?>
  </div>
<?php
endif;
?>
  • I'll likely be pre-disposed for the next couple weeks, but eventually I'll come back to test, debug, and comment on the implementation. Maybe a little performance profile if I'm feeling cavalier =P – bosco Aug 3 '16 at 3:19
  • Thanks for the very in-depth answer - I will try and test this solution and reply with any results I have :) – Stephen Aug 3 '16 at 7:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.