UPDATE
Because when the 4 first products are out of stock it does not display anything (In case the default 4 products are shown) you could use the following snippet instead of overwriting the template file.
function filter_woocommerce_related_products( $related_posts, $product_id, $args ) {
foreach( $related_posts as $key => $related_post ) {
// Get product
$related_product = wc_get_product( $related_post );
// Is a WC product
if ( is_a( $related_product, 'WC_Product' ) ) {
// Stock status
$stock_status = $related_product->get_stock_status();
// NOT instock
if ( $stock_status != 'instock' ) {
unset( $related_posts[$key] );
}
}
}
return $related_posts;
}
add_filter( 'woocommerce_related_products', 'filter_woocommerce_related_products', 10, 3 );
Overwriting the template file
There are always multiple solutions but 1 of them could be by overwriting the template file.
https://github.com/woocommerce/woocommerce/blob/02cf0dfaed5923513de0c88add597d1560c2cfd2/templates/single-product/related.php
- This template can be overridden by copying it to
yourtheme/woocommerce/single-product/related.php
Replace
<?php foreach ( $related_products as $related_product ) : ?>
<?php
$post_object = get_post( $related_product->get_id() );
setup_postdata( $GLOBALS['post'] =& $post_object ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited, Squiz.PHP.DisallowMultipleAssignments.Found
wc_get_template_part( 'content', 'product' );
?>
<?php endforeach; ?>
With
<?php foreach ( $related_products as $related_product ) : ?>
<?php
$stock_status = $related_product->get_stock_status();
if ( $stock_status == 'instock' ) {
$post_object = get_post( $related_product->get_id() );
setup_postdata( $GLOBALS['post'] =& $post_object ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited, Squiz.PHP.DisallowMultipleAssignments.Found
wc_get_template_part( 'content', 'product' );
}
?>
<?php endforeach; ?>