Restrict WooCommerce products and shop access by user role with PHP

Access control in WooCommerce is a common requirement for B2B stores, membership sites, and wholesale platforms. The typical scenarios are: hiding all shop content from guests until they register, showing certain products only to users with a “wholesale” role, restricting the checkout to logged-in users, and preventing direct URL access to product pages that should be private. WooCommerce does not include granular access control out of the box, but WordPress’s user role and capability system combined with WooCommerce’s action and filter hooks makes all of these implementable in pure PHP without a plugin. The most reliable approach is to store access rules in post meta on each product (e.g. _ha_required_role) and check that meta in a template_redirect hook that fires before any content is rendered. For hiding products from the shop loop and search results, the woocommerce_product_query filter lets you modify the underlying WP_Query to exclude products the current user should not see. Always combine the query filter with a direct URL access check — filtering the query prevents products from appearing in listings, but a user who knows the product URL can still access it without the direct access check. WooCommerce provides the woocommerce_is_purchasable filter to prevent add-to-cart for products the user can see but not buy. The checkout fields guide and the custom email guide show how to extend the checkout and order flow for different user segments once access control is in place.

Problem: You need to hide specific WooCommerce products from guest users and from users without a “wholesale” role, and redirect unauthorised users who try to access restricted product pages directly.

Solution: Combine a woocommerce_product_query filter to hide products in listings with a template_redirect check for direct URL access:

// Hide restricted products from shop loop and search results
add_action( 'woocommerce_product_query', 'ha_filter_restricted_products' );

function ha_filter_restricted_products( WP_Query $query ) {
    if ( is_admin() ) return;

    // Get all product IDs that require the 'wholesale_customer' role
    $restricted_ids = ha_get_restricted_product_ids( 'wholesale_customer' );
    if ( empty( $restricted_ids ) ) return;

    // If the current user is wholesale, show everything
    if ( current_user_can( 'wholesale_customer' ) ) return;

    $existing_exclude = (array) $query->get( 'post__not_in' );
    $query->set( 'post__not_in', array_merge( $existing_exclude, $restricted_ids ) );
}

// Redirect on direct product page access
add_action( 'template_redirect', 'ha_block_restricted_product_access' );

function ha_block_restricted_product_access() {
    if ( ! is_singular( 'product' ) ) return;

    $product_id    = get_queried_object_id();
    $required_role = get_post_meta( $product_id, '_ha_required_role', true );

    if ( ! $required_role ) return; // No restriction set

    // Not logged in — redirect to login
    if ( ! is_user_logged_in() ) {
        wp_safe_redirect( wc_get_page_permalink( 'myaccount' ) );
        exit;
    }

    // Logged in but missing role — redirect to shop with notice
    if ( ! current_user_can( $required_role ) ) {
        wc_add_notice( __( 'You do not have access to this product.', 'woocommerce' ), 'error' );
        wp_safe_redirect( wc_get_page_permalink( 'shop' ) );
        exit;
    }
}

// Prevent add-to-cart for restricted products visible to non-qualifying users
add_filter( 'woocommerce_is_purchasable', 'ha_restrict_purchasable', 10, 2 );

function ha_restrict_purchasable( $purchasable, WC_Product $product ) {
    $required_role = get_post_meta( $product->get_id(), '_ha_required_role', true );
    if ( $required_role && ! current_user_can( $required_role ) ) {
        return false;
    }
    return $purchasable;
}

// Helper: return product IDs that have a specific required role set
function ha_get_restricted_product_ids( $role ) {
    $cached = get_transient( 'ha_restricted_ids_' . $role );
    if ( false !== $cached ) return $cached;

    global $wpdb;
    $ids = $wpdb->get_col( $wpdb->prepare(
        "SELECT post_id FROM {$wpdb->postmeta}
         WHERE meta_key = '_ha_required_role' AND meta_value = %s",
        $role
    ) );
    $ids = array_map( 'absint', $ids );
    set_transient( 'ha_restricted_ids_' . $role, $ids, HOUR_IN_SECONDS );
    return $ids;
}

NOTE: To set the _ha_required_role meta on a product, add a custom metabox to the product edit screen using add_meta_box() on the add_meta_boxes hook and save it with a save_post_product action. Clear the ha_restricted_ids_* transients whenever a product is saved by hooking into save_post_product and calling delete_transient(). This access control implementation assumes the wholesale_customer capability is granted via a custom role — create it once with add_role( ‘wholesale_customer’, ‘Wholesale Customer’, array( ‘read’ => true, ‘wholesale_customer’ => true ) ) in a plugin activation hook.