1

Any help with this would be greatly appreciated - I've been grappling with it for days :)

I have 2 post types - product + offer

I have an ACF relationship field related_products.

This field returns an array of post IDs Array ( [0] => 395 [1] => 120 [2] => 388 [3] => 391 )

I am using related_products on the offer post type - it's a one-way relationship (not bi-directional).

Each of my post types has a "card" template part that I use in all my loops - that's what I want to do here ... use the related_products IDs to get the product card parts and show them on the offer.

I had no luck with the ACF documentation - but that's probably because I'm a noob.

Instead, I set up a custom WP_Query that get's me close, but not there.

The if ( $custom_query->have_posts() ) IS working - when an offer has related products, the correct product cards ARE displayed.

But, when an offer has no related_products, ALL products are being shown.

My questions are:

  • Is a custom WP_QUERY the right/best way to go about this?
  • If so, what do I need to fix in the query?

Thank you for your time and help :)

// Array ( [0] => 395 [1] => 120 [2] => 388 [3] => 391 )
$related_products = get_field('related_products');

$args = array( 
    'post_type' => 'product',
    'post__in' => $related_products,    
    'fields' => 'ids',
    'cache_results'  => false,
    'update_post_meta_cache' => false, 
    'update_post_term_cache' => false, 
    'posts_per_page' => -1, 
    'paged' => false,
);

$custom_query = new WP_Query( $args );

if ( $custom_query->have_posts() ) :  
    while ( $custom_query->have_posts() ) : 
        $custom_query->the_post(); 
        get_template_part( 'parts/card', get_post_type() );
    endwhile;
else : 
    // do something else
endif; 
wp_reset_query();

3 Answers 3

3

But, when an offer has no related_products, ALL products are being shown.

This is because you're not doing anything to check if the field has a value before running your query. If this is empty:

$related_products = get_field('related_products');

Then that makes the query argument equivalent to:

'post__in' => [],

And if that argument value is empty, it doesn't query no posts, it just gets ignored, which means you're query is the equivalent of:

$args = array( 
    'post_type'              => 'product',
    'fields'                 => 'ids',
    'cache_results'          => false,
    'update_post_meta_cache' => false, 
    'update_post_term_cache' => false, 
    'posts_per_page'         => -1, 
    'paged'                  => false,
);

Which is querying all products.

So you need to check if your field has any values before doing anything, then only querying if there are any.

Your other problem is that you have fields set to ids. If you do this then the result is just an array of posts IDs, which you already have because that's what you're querying. If you want to use the loop with your results, you need to remove that line. So all you should have is:

$related_products = get_field( 'related_products' );

if ( $related_products ) {
    $args = array( 
        'post_type' => 'product',
        'post__in'  => $related_products,    
    );

    $custom_query = new WP_Query( $args );

    if ( $custom_query->have_posts() ) :  
        while ( $custom_query->have_posts() ) : 
            $custom_query->the_post(); 
            get_template_part( 'parts/card', get_post_type() );
        endwhile;
    else : 
    // do something else
    endif;

    wp_reset_postdata();
}

But doing this query at all is unnecessary. If you set the field to return post objects, rather than IDs, you can just loop through those:

$related_products = get_field( 'related_products' );

if ( $related_products ) {
    global $post; // Necessary.

    foreach ( $related_products as $post ) : // Must be called $post. 
        setup_postdata( $post ); 
        get_template_part( 'parts/card', get_post_type() );
    endforeach;

    wp_reset_postdata();
}

This is the same as Faye's answer, I'm just including it for completeness. If you're receiving any errors from that, then the issue is somewhere else. Probably inside your card template.

12
  • Thank you Jacob! Very good answer. I agree, I would expect the issue to be coming from elsewhere.
    – Faye
    Commented Jul 12, 2019 at 16:44
  • First I want to say thanks to you both for the help - time is valuable and I so totally appreciate it <3 Second - @JacobPeattie answer is NOT the same as @Faye - Adding global $post solved my issue - Yay! Again - Thank you both for your time and help - this is such a great community!
    – user145691
    Commented Jul 12, 2019 at 19:20
  • Not because I need it here, but because I'm still learning ... I'm not sure this part of Jacobs answer is accurate ... "Your other problem is that you have fields set to ids ... If you want to use the loop with your results, you need to remove that line" ... It seems passing an array of IDs to $args is separate from aksing WP_QUERY to return just IDs ... without 'fields' => 'ids' the query is going to return all fields by default. That's a perfomance issue - why waste resources getting fields I don't need? ... kinsta.com/blog/wp-query/#returned-fields
    – user145691
    Commented Jul 12, 2019 at 19:21
  • Firstly, returning IDs from the query is totally redundant, because you already have the IDs! So even if you did only want the IDs there’s no point querying for IDs you already have. Secondly, you do need those fields. Where do you think the content you’re going to output comes from? If you use the_title() then you need to get the titles of the post from the database. Commented Jul 13, 2019 at 0:42
  • Sorry, but I think you are totally wrong about this ... the official documentation states that without this parameter, ALL fields are returned ... THAT is redundant when all you need are IDs to get template parts ... you DO need all fields if you want to get a single field like the_title .... have a look at the link I provided above and here ... developer.wordpress.org/reference/classes/wp_query/…
    – user145691
    Commented Jul 14, 2019 at 20:22
2

I could be wrong, but it looks like you're trying to do something much simpler than what you've got going there. I use the method shown here on a regular basis and it works just fine.

<?php 

$posts = get_field('related_products');

if( $posts ): ?>

    <?php foreach( $posts as $post): // variable must be called $post (IMPORTANT) ?>
        <?php setup_postdata($post); ?>

        <?php get_template_part( 'parts/card', get_post_type() );?> 
        <!-- this is the only thing I'm not sure if you'd want to just directly call 'parts/card', 'product' -->

    <?php endforeach; ?>
    <?php wp_reset_postdata(); // IMPORTANT - reset the $post object so the rest of the page works correctly ?>
<?php endif; ?>
2
  • Thanks for your reply. As I stated in my OP, I tried the methods in the ACF documentation as you describe ... I tested setting the field to return both the post object and the post ID ... This method works great for getting specific fields, like post title, but doesn't work for getting template parts ... with either the object or the ID I get this warning: "Notice: Uninitialized string offset: 0 in /www/wp-includes/class-wp-query.php on line 3323"
    – user145691
    Commented Jul 11, 2019 at 22:34
  • ... and that error is in rewind_posts() ... but I don't know enough to tell you what that means :P
    – user145691
    Commented Jul 11, 2019 at 23:36
1

My OP is about using the ACF relationship field in WP_QUERY. The ACF documentation leaves out one helpful bit that @JacobPeattie supplied.

My final working code looks like this:

// ACF field is set to return IDs.
// Array ( [0] => 395 [1] => 120 [2] => 388 [3] => 391 )

$related_product = get_field( 'related_product' );

if ( $related_product ) :

    global $post; // missing from ACF documentation

    foreach ( $related_product as $post ) : // Must be called $post. 
        setup_postdata( $post ); 
        get_template_part( 'parts/card', get_post_type() );
    endforeach;

    wp_reset_postdata();

endif;

However, part of Jacob's explanation and subsequent comments are wrong. Because I rely on this community for accurate information, I can't accept his answer as correct.

Jacob incorrectly asserts that passing an array of IDs to WP_QUERY and then using the fields parameter to return only IDs is redundant and won't work.

You can use any number of things to inform WP_QUERY which posts to GET - terms, post type, IDs. This is completely separate from what you tell the query to RETURN - there is nothing redundant about passing then returning IDs.

The fields parameter defaults to returning all fields. Alternatively, you can set fields to return ids (note plural).

Deciding to return all fields or just IDs is a matter of optimization - if you don't need all fields, then don't return them.

Here are two examples from my live site demonstrating that YOU DO NOT NEED ALL FIELDS in many common scenarios. You can easily try these for yourself and I've linked to more info below.

This example displays a field - the_title ... super common, I use it all the time and you don't need to return all fields to do it.

$args = array( 
    'post_type' => 'product',
    'fields' => 'ids',
);

$custom_query = new WP_Query( $args );

if ( $custom_query->have_posts() ) :  

    while ( $custom_query->have_posts() ) : 
        $custom_query->the_post(); 
        the_title();
    endwhile;

else : 
    // do something else
endif; 

wp_reset_query();

Here's an even more complex / optimized example. This one is for displaying a template part. This part has standard stuff like the_title + ACF fields + an Add Favorite feature + pulls in other parts. The results are filterable by FacetWP.

Again, you only need WP_QUERY to return IDs.

$args = array( 
    'post_type' => 'product',
    'fields' => 'ids',
    'cache_results'  => false,
    'update_post_meta_cache' => false, 
    'update_post_term_cache' => false, 
    'facetwp' => true, 
);

$custom_query = new WP_Query( $args );

if ( $custom_query->have_posts() ) : 

    while ( $custom_query->have_posts() ) : 
        $custom_query->the_post(); 
        get_template_part( 'parts/card', get_post_type() );                           
    endwhile;

else :
    // do something else
endif;

wp_reset_query();

You can read more about optimizing WP_QUERY here (and lots of other places):

Thinking about it, these examples make perfect sense - things like global Spost and setup_postdata($post) and $custom_query->the_post() are specifically for setting context and ensuring the correct ID is referenced so you can then get the right field, template etc ...

And why would a database need anything more than an ID to get a record's associated fields? ... The only answer I can think of is if the database is poorly designed ... In fact, I have never had a scenario where returning just IDs hasn't worked.

I'm not claiming to be an expert - I only know that you can return just IDs because I read about it, tried it in my own projects, and it works.

2
  • 1
    Not to bring this up again as it seems to have been hit in previous comments but Jacob is correct. The method $query->the_post() calls $query->setup_postdata() and the first line is a conditional which says if what is given is not a WP_Post, call the database and get that post. By only requesting Post IDs you are not giving setup_postdata() a WP_Post but a Post ID which it then needs to get the rest of the post. While your code does work, in this case it is calling the database more times than it needs to.
    – Howdy_McGee
    Commented Aug 7, 2019 at 2:53
  • I sincerely appreciate your explanation and understand - I wish Jacob had said it isn't optimal instead of insisting it doesn't work ... It does work and is right for some scenarios, just not mine.
    – user145691
    Commented May 21, 2020 at 20:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.