wc-product-functions.php
Functions
wc_get_products()
Standard way of retrieving products based on certain parameters.
wc_get_products(array<string|int, mixed> $args) : array<string|int, mixed>|stdClass
This function should be used for product retrieval so that we have a data agnostic way to get a list of products.
Args and usage: https://developer.woocommerce.com/docs/extensions/core-concepts/wc-get-products/
Parameters
- $args : array<string|int, mixed>
-
Array of args (above).
Tags
wc_get_product()
Main function for returning products, uses the WC_Product_Factory class.
wc_get_product([mixed $the_product = false ][, array<string|int, mixed> $deprecated = array() ]) : WC_Product|null|false
This function should only be called after 'init' action is finished, as there might be taxonomies that are getting registered during the init action.
Parameters
- $the_product : mixed = false
-
Post object or post ID of the product.
- $deprecated : array<string|int, mixed> = array()
-
Previously used to pass arguments to the factory, e.g. to force a type.
Tags
wc_get_product_object()
Get a product object.
wc_get_product_object(string $product_type, int $product_id) : WC_Product
Parameters
- $product_type : string
-
Product type. If used an invalid type a WC_Product_Simple instance will be returned.
- $product_id : int
-
Product ID.
Tags
wc_product_sku_enabled()
Returns whether or not SKUS are enabled.
wc_product_sku_enabled() : bool
wc_product_weight_enabled()
Returns whether or not product weights are enabled.
wc_product_weight_enabled() : bool
wc_product_dimensions_enabled()
Returns whether or not product dimensions (HxWxD) are enabled.
wc_product_dimensions_enabled() : bool
wc_delete_product_transients()
Clear transient cache for product data.
wc_delete_product_transients(int $post_id) : mixed
Parameters
- $post_id : int
-
(default: 0) The product ID.
wc_delete_related_product_transients()
Delete all related products transients when a product is updated/created.
wc_delete_related_product_transients(int $post_id) : mixed
This is necessary because changing one product affects all related products too.
Parameters
- $post_id : int
-
The product ID updated/created.
Tags
wc_get_product_ids_on_sale()
Function that returns an array containing the IDs of the products that are on sale.
wc_get_product_ids_on_sale() : array<string|int, mixed>
Tags
wc_get_featured_product_ids()
Function that returns an array containing the IDs of the featured products.
wc_get_featured_product_ids() : array<string|int, mixed>
Tags
wc_product_post_type_link()
Filter to allow product_cat in the permalinks for products.
wc_product_post_type_link(string $permalink, WP_Post $post) : string
Parameters
- $permalink : string
-
The existing permalink URL.
- $post : WP_Post
-
WP_Post object.
wc_product_canonical_redirect()
Ensure that the product_cat value determined in `wc_product_post_type_link` is the canonical value.
wc_product_canonical_redirect() : void
If other values are used in this part of the permalink, it will be redirected.
wc_placeholder_img_src()
Get the placeholder image URL either from media, or use the fallback image.
wc_placeholder_img_src([string $size = 'woocommerce_thumbnail' ]) : string
Parameters
- $size : string = 'woocommerce_thumbnail'
-
Thumbnail size to use.
wc_placeholder_img()
Get the placeholder image.
wc_placeholder_img([string $size = 'woocommerce_thumbnail' ][, string|array<string|int, mixed> $attr = '' ]) : string
Uses wp_get_attachment_image if using an attachment ID @since 3.6.0 to handle responsiveness.
Parameters
- $size : string = 'woocommerce_thumbnail'
-
Image size.
- $attr : string|array<string|int, mixed> = ''
-
Optional. Attributes for the image markup. Default empty.
wc_get_formatted_variation()
Variation Formatting.
wc_get_formatted_variation(array<string|int, mixed>|WC_Product_Variation $variation[, bool $flat = false ][, bool $include_names = true ][, bool $skip_attributes_in_name = false ]) : string
Gets a formatted version of variation data or item meta.
Parameters
- $variation : array<string|int, mixed>|WC_Product_Variation
-
Variation object.
- $flat : bool = false
-
Should this be a flat list or HTML list? (default: false).
- $include_names : bool = true
-
include attribute names/labels in the list.
- $skip_attributes_in_name : bool = false
-
Do not list attributes already part of the variation name.
wc_schedule_product_sale_events()
Schedule start/end sale actions for a product based on its sale dates.
wc_schedule_product_sale_events(WC_Product $product) : void
Uses Action Scheduler to fire events at the exact sale start/end times, rather than relying on the daily cron.
An action is not scheduled if an identical one (same hook, args, group and timestamp) is already pending, so concurrent processes saving the same product don't pile up duplicate actions.
Parameters
- $product : WC_Product
-
Product object.
Tags
wc_apply_sale_state_for_product()
Apply the expected sale state for a product.
wc_apply_sale_state_for_product(WC_Product $product, string $mode) : void
This is a shared helper used by both the per-product Action Scheduler callbacks and the daily cron safety net.
Parameters
- $product : WC_Product
-
Product object.
- $mode : string
-
'start' or 'end'.
Tags
wc_handle_product_start_scheduled_sale()
Handle scheduled sale start for a product.
wc_handle_product_start_scheduled_sale(int $product_id) : void
This is the Action Scheduler callback that fires at the exact sale start time.
Parameters
- $product_id : int
-
Product ID.
Tags
wc_handle_product_end_scheduled_sale()
Handle scheduled sale end for a product.
wc_handle_product_end_scheduled_sale(int $product_id) : void
This is the Action Scheduler callback that fires at the exact sale end time.
Parameters
- $product_id : int
-
Product ID.
Tags
wc_maybe_schedule_product_sale_events()
Schedule sale events when a product is saved with sale dates.
wc_maybe_schedule_product_sale_events(int $product_id[, WC_Product|null $product = null ]) : void
Parameters
- $product_id : int
-
Product ID.
- $product : WC_Product|null = null
-
Product object (optional).
Tags
wc_maybe_schedule_sale_events_on_meta_change()
Schedule sale events when sale date meta is added, updated, or deleted.
wc_maybe_schedule_sale_events_on_meta_change(int|array<string|int, int> $meta_id, int $object_id, string $meta_key) : void
Hooks into post meta operations so per-product sale events are kept in sync regardless of how the meta is written: WooCommerce CRUD, direct update_post_meta() calls from importers, ERP sync tools, or custom code.
Parameters
- $meta_id : int|array<string|int, int>
-
Meta ID (or array of IDs for delete).
- $object_id : int
-
Post ID.
- $meta_key : string
-
Meta key.
Tags
wc_scheduled_sales()
Function which handles the start and end of scheduled sales via cron.
wc_scheduled_sales() : mixed
Previously, this daily cron was the only mechanism for starting/ending scheduled sales, which caused timing issues - sales could be "a day off" depending on when WP-Cron ran. Now, per-product Action Scheduler events fire at exact sale times.
This function now acts as a safety net to:
- Catch any products missed by the per-product Action Scheduler events
- Handle products created before the AS events were introduced
This function is kept for backwards compatibility. Extenders may hook into the
woocommerce_scheduled_sales cron event or the before/after hooks fired within.
Note: The before/after hooks (wc_before_products_starting_sales, etc.) only fire when this cron finds products to process. If per-product AS events handled sales on time, these hooks may not fire.
Tags
wc_get_attachment_image_attributes()
Get attachment image attributes.
wc_get_attachment_image_attributes(array<string|int, mixed> $attr) : array<string|int, mixed>
Parameters
- $attr : array<string|int, mixed>
-
Image attributes.
wc_prepare_attachment_for_js()
Prepare attachment for JavaScript.
wc_prepare_attachment_for_js(array<string|int, mixed> $response) : array<string|int, mixed>
Parameters
- $response : array<string|int, mixed>
-
JS version of a attachment post object.
wc_track_product_view()
Track product views.
wc_track_product_view() : mixed
wc_get_product_types()
Get product types.
wc_get_product_types() : array<string|int, mixed>
Tags
wc_product_has_unique_sku()
Check if product sku is unique.
wc_product_has_unique_sku(int $product_id, string $sku) : bool
Parameters
- $product_id : int
-
Product ID.
- $sku : string
-
Product SKU.
Tags
wc_product_has_global_unique_id()
Check if product unique ID is unique.
wc_product_has_global_unique_id(int $product_id, string $global_unique_id) : bool
Parameters
- $product_id : int
-
Product ID.
- $global_unique_id : string
-
Product Unique ID.
Tags
wc_product_force_unique_sku()
Force a unique SKU.
wc_product_force_unique_sku(int $product_id) : mixed
Parameters
- $product_id : int
-
Product ID.
Tags
wc_product_generate_unique_sku()
Recursively appends a suffix until a unique SKU is found.
wc_product_generate_unique_sku(int $product_id, string $sku, int $index) : string
Parameters
- $product_id : int
-
Product ID.
- $sku : string
-
Product SKU.
- $index : int
-
An optional index that can be added to the product SKU.
Tags
wc_get_product_id_by_sku()
Get product ID by SKU.
wc_get_product_id_by_sku(string $sku) : int
Parameters
- $sku : string
-
Product SKU.
Tags
wc_get_product_id_by_global_unique_id()
Get product ID by Unique ID.
wc_get_product_id_by_global_unique_id(string $global_unique_id) : int|null
Parameters
- $global_unique_id : string
-
Product Unique ID.
Tags
wc_get_product_variation_attributes()
Get attributes/data for an individual variation from the database and maintain its integrity.
wc_get_product_variation_attributes(int $variation_id) : array<string|int, mixed>
Parameters
- $variation_id : int
-
Variation ID.
Tags
wc_get_product_cat_ids()
Get all product cats for a product by ID, including hierarchy
wc_get_product_cat_ids(int $product_id) : array<string|int, mixed>
Parameters
- $product_id : int
-
Product ID.
Tags
wc_get_product_attachment_props()
Gets data about an attachment, such as alt text and captions.
wc_get_product_attachment_props([int|null $attachment_id = null ][, WC_Product|bool $product = false ]) : array<string|int, mixed>
Parameters
- $attachment_id : int|null = null
-
Attachment ID.
- $product : WC_Product|bool = false
-
WC_Product object.
Tags
wc_get_product_visibility_options()
Get product visibility options.
wc_get_product_visibility_options() : array<string|int, mixed>
Tags
wc_get_product_tax_class_options()
Get product tax class options.
wc_get_product_tax_class_options() : array<string|int, mixed>
Tags
wc_get_product_stock_status_options()
Get stock status options.
wc_get_product_stock_status_options() : array<string|int, mixed>
Tags
wc_get_product_backorder_options()
Get backorder options.
wc_get_product_backorder_options() : array<string|int, mixed>
Tags
wc_get_related_products()
Get related products based on product category and tags.
wc_get_related_products(int $product_id[, int $limit = 5 ][, array<string|int, mixed> $exclude_ids = array() ][, array<string|int, mixed> $related_by = array() ]) : array<string|int, mixed>
Parameters
- $product_id : int
-
Product ID.
- $limit : int = 5
-
Limit of results.
- $exclude_ids : array<string|int, mixed> = array()
-
Exclude IDs from the results.
- $related_by : array<string|int, mixed> = array()
-
Related by category and tags boolean flags.
Tags
wc_get_product_term_ids()
Retrieves product term ids for a taxonomy.
wc_get_product_term_ids(int $product_id, string $taxonomy) : array<string|int, mixed>
Parameters
- $product_id : int
-
Product ID.
- $taxonomy : string
-
Taxonomy slug.
Tags
wc_get_price_including_tax()
For a given product, and optionally price/qty, work out the price with tax included, based on store settings.
wc_get_price_including_tax(WC_Product $product[, array<string|int, mixed> $args = array() ]) : float|string
Parameters
- $product : WC_Product
-
WC_Product object.
- $args : array<string|int, mixed> = array()
-
Optional arguments to pass product quantity and price.
Tags
wc_get_price_excluding_tax()
For a given product, and optionally price/qty, work out the price with tax excluded, based on store settings.
wc_get_price_excluding_tax(WC_Product $product[, array<string|int, mixed> $args = array() ]) : float|string
Parameters
- $product : WC_Product
-
WC_Product object.
- $args : array<string|int, mixed> = array()
-
Optional arguments to pass product quantity and price.
Tags
wc_get_price_to_display()
Returns the price including or excluding tax.
wc_get_price_to_display(WC_Product $product[, array<string|int, mixed> $args = array() ]) : float
By default it's based on the 'woocommerce_tax_display_shop' setting.
Set $arg['display_context'] to 'cart' to base on the 'woocommerce_tax_display_cart' setting instead.
Parameters
- $product : WC_Product
-
WC_Product object.
- $args : array<string|int, mixed> = array()
-
Optional arguments to pass product quantity and price.
Tags
wc_get_product_category_list()
Returns the product categories in a list.
wc_get_product_category_list(int $product_id[, string $sep = ', ' ][, string $before = '' ][, string $after = '' ]) : string
Parameters
- $product_id : int
-
Product ID.
- $sep : string = ', '
-
(default: ', ').
- $before : string = ''
-
(default: '').
- $after : string = ''
-
(default: '').
wc_get_product_tag_list()
Returns the product tags in a list.
wc_get_product_tag_list(int $product_id[, string $sep = ', ' ][, string $before = '' ][, string $after = '' ]) : string
Parameters
- $product_id : int
-
Product ID.
- $sep : string = ', '
-
(default: ', ').
- $before : string = ''
-
(default: '').
- $after : string = ''
-
(default: '').
wc_products_array_filter_visible()
Callback for array filter to get visible only.
wc_products_array_filter_visible(WC_Product $product) : bool
Parameters
- $product : WC_Product
-
WC_Product object.
Tags
wc_products_array_filter_visible_grouped()
Callback for array filter to get visible grouped products only.
wc_products_array_filter_visible_grouped(WC_Product $product) : bool
Parameters
- $product : WC_Product
-
WC_Product object.
Tags
wc_products_array_filter_editable()
Callback for array filter to get products the user can edit only.
wc_products_array_filter_editable(WC_Product $product) : bool
Parameters
- $product : WC_Product
-
WC_Product object.
Tags
wc_products_array_filter_readable()
Callback for array filter to get products the user can view only.
wc_products_array_filter_readable(WC_Product $product) : bool
Parameters
- $product : WC_Product
-
WC_Product object.
Tags
wc_products_array_orderby()
Sort an array of products by a value.
wc_products_array_orderby(array<string|int, mixed> $products[, string $orderby = 'date' ][, string $order = 'desc' ]) : array<string|int, mixed>
Parameters
- $products : array<string|int, mixed>
-
List of products to be ordered.
- $orderby : string = 'date'
-
Optional order criteria.
- $order : string = 'desc'
-
Ascending or descending order.
Tags
wc_products_array_orderby_title()
Sort by title.
wc_products_array_orderby_title(WC_Product $a, WC_Product $b) : int
Parameters
- $a : WC_Product
-
First WC_Product object.
- $b : WC_Product
-
Second WC_Product object.
Tags
wc_products_array_orderby_id()
Sort by id.
wc_products_array_orderby_id(WC_Product $a, WC_Product $b) : int
Parameters
- $a : WC_Product
-
First WC_Product object.
- $b : WC_Product
-
Second WC_Product object.
Tags
wc_products_array_orderby_date()
Sort by date.
wc_products_array_orderby_date(WC_Product $a, WC_Product $b) : int
Parameters
- $a : WC_Product
-
First WC_Product object.
- $b : WC_Product
-
Second WC_Product object.
Tags
wc_products_array_orderby_modified()
Sort by modified.
wc_products_array_orderby_modified(WC_Product $a, WC_Product $b) : int
Parameters
- $a : WC_Product
-
First WC_Product object.
- $b : WC_Product
-
Second WC_Product object.
Tags
wc_products_array_orderby_menu_order()
Sort by menu order.
wc_products_array_orderby_menu_order(WC_Product $a, WC_Product $b) : int
Parameters
- $a : WC_Product
-
First WC_Product object.
- $b : WC_Product
-
Second WC_Product object.
Tags
wc_products_array_orderby_price()
Sort by price low to high.
wc_products_array_orderby_price(WC_Product $a, WC_Product $b) : int
Parameters
- $a : WC_Product
-
First WC_Product object.
- $b : WC_Product
-
Second WC_Product object.
Tags
wc_deferred_product_sync()
Queue a product for syncing at the end of the request.
wc_deferred_product_sync(int $product_id) : mixed
Parameters
- $product_id : int
-
Product ID.
wc_update_product_lookup_tables_is_running()
See if the lookup table is being generated already.
wc_update_product_lookup_tables_is_running() : bool
Tags
wc_update_product_lookup_tables()
Populate lookup table data for products.
wc_update_product_lookup_tables() : mixed
Tags
wc_update_product_lookup_tables_column()
Populate lookup table column data.
wc_update_product_lookup_tables_column(string $column) : mixed
Parameters
- $column : string
-
Column name to set.
Tags
wc_update_product_lookup_tables_rating_count()
Populate rating count lookup table data for products.
wc_update_product_lookup_tables_rating_count(array<string|int, mixed> $rows) : mixed
Parameters
- $rows : array<string|int, mixed>
-
Rows of rating counts to update in lookup table.
Tags
wc_update_product_lookup_tables_rating_count_batch()
Populate a batch of rating count lookup table data for products.
wc_update_product_lookup_tables_rating_count_batch(array<string|int, mixed> $offset, array<string|int, mixed> $limit) : mixed
Parameters
- $offset : array<string|int, mixed>
-
Offset to query.
- $limit : array<string|int, mixed>
-
Limit to query.
Tags
wc_product_attach_featured_image()
Attach product featured image. Use image filename to match a product sku when product is not provided.
wc_product_attach_featured_image(int $attachment_id[, WC_Product $product = null ][, bool $save_product = true ]) : void
Parameters
- $attachment_id : int
-
Media attachment ID.
- $product : WC_Product = null
-
Optional product object.
- $save_product : bool = true
-
If true, the changes in the product will be saved before the method returns.
Tags
Source code
<?php
/**
* WooCommerce Product Functions
*
* Functions for product specific things.
*
* @package WooCommerce\Functions
* @version 3.0.0
*/
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Enums\ProductStatus;
use Automattic\WooCommerce\Enums\ProductStockStatus;
use Automattic\WooCommerce\Enums\ProductType;
use Automattic\WooCommerce\Enums\CatalogVisibility;
use Automattic\WooCommerce\Enums\TaxDisplayMode;
use Automattic\WooCommerce\Internal\Caches\ProductTransientsDeferrer;
use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\ArrayUtil;
use Automattic\WooCommerce\Utilities\NumberUtil;
use Automattic\WooCommerce\Internal\ProductImage\MatchImageBySKU;
defined( 'ABSPATH' ) || exit;
/**
* Standard way of retrieving products based on certain parameters.
*
* This function should be used for product retrieval so that we have a data agnostic
* way to get a list of products.
*
* Args and usage: https://developer.woocommerce.com/docs/extensions/core-concepts/wc-get-products/
*
* @since 3.0.0
* @param array $args Array of args (above).
* @return array|stdClass Number of pages and an array of product objects if
* paginate is true, or just an array of values.
*/
function wc_get_products( $args ) {
// Handle some BW compatibility arg names where wp_query args differ in naming.
$map_legacy = array(
'numberposts' => 'limit',
'post_status' => 'status',
'post_parent' => 'parent',
'posts_per_page' => 'limit',
'paged' => 'page',
);
foreach ( $map_legacy as $from => $to ) {
if ( isset( $args[ $from ] ) ) {
$args[ $to ] = $args[ $from ];
}
}
$query = new WC_Product_Query( $args );
return $query->get_products();
}
/**
* Main function for returning products, uses the WC_Product_Factory class.
*
* This function should only be called after 'init' action is finished, as there might be taxonomies that are getting
* registered during the init action.
*
* @since 2.2.0
*
* @param mixed $the_product Post object or post ID of the product.
* @param array $deprecated Previously used to pass arguments to the factory, e.g. to force a type.
* @return WC_Product|null|false
*/
function wc_get_product( $the_product = false, $deprecated = array() ) {
if ( ! did_action( 'woocommerce_init' ) || ! did_action( 'woocommerce_after_register_taxonomy' ) || ! did_action( 'woocommerce_after_register_post_type' ) ) {
/* translators: 1: wc_get_product 2: woocommerce_init 3: woocommerce_after_register_taxonomy 4: woocommerce_after_register_post_type */
wc_doing_it_wrong( __FUNCTION__, sprintf( __( '%1$s should not be called before the %2$s, %3$s and %4$s actions have finished.', 'woocommerce' ), 'wc_get_product', 'woocommerce_init', 'woocommerce_after_register_taxonomy', 'woocommerce_after_register_post_type' ), '3.9' );
return false;
}
if ( ! empty( $deprecated ) ) {
wc_deprecated_argument( 'args', '3.0', 'Passing args to wc_get_product is deprecated. If you need to force a type, construct the product class directly.' );
}
return WC()->product_factory->get_product( $the_product, $deprecated );
}
/**
* Get a product object.
*
* @see WC_Product_Factory::get_product_classname
* @since 3.9.0
* @param string $product_type Product type. If used an invalid type a WC_Product_Simple instance will be returned.
* @param int $product_id Product ID.
* @return WC_Product
*/
function wc_get_product_object( $product_type, $product_id = 0 ) {
$classname = WC_Product_Factory::get_product_classname( $product_id, $product_type );
return new $classname( $product_id );
}
/**
* Returns whether or not SKUS are enabled.
*
* @return bool
*/
function wc_product_sku_enabled() {
return apply_filters( 'wc_product_sku_enabled', true );
}
/**
* Returns whether or not product weights are enabled.
*
* @return bool
*/
function wc_product_weight_enabled() {
return apply_filters( 'wc_product_weight_enabled', true );
}
/**
* Returns whether or not product dimensions (HxWxD) are enabled.
*
* @return bool
*/
function wc_product_dimensions_enabled() {
return apply_filters( 'wc_product_dimensions_enabled', true );
}
/**
* Clear transient cache for product data.
*
* @param int $post_id (default: 0) The product ID.
*/
function wc_delete_product_transients( $post_id = 0 ) {
$container = wc_get_container();
if ( $container->get( ProductTransientsDeferrer::class )->maybe_defer_deletion( absint( $post_id ) ) ) {
return;
}
$container->get( ProductUtil::class )->delete_product_transients_for_products( array( $post_id ) );
}
/**
* Delete all related products transients when a product is updated/created.
* This is necessary because changing one product affects all related products too.
*
* @since 9.8.0
* @deprecated 10.1.0 This function is deprecated and will be removed in a future version.
* @param int $post_id The product ID updated/created.
*/
function wc_delete_related_product_transients( $post_id ) {
wc_deprecated_function( 'wc_delete_related_product_transients', '10.1.0', 'This function is deprecated and will be removed in a future version.' );
if ( ! is_numeric( $post_id ) ) {
return;
}
$transient_name = 'wc_related_' . $post_id;
$old_transient = get_transient( $transient_name );
$old_related_product_ids = array();
if ( is_array( $old_transient ) && ! empty( $old_transient ) ) {
$old_related_product_ids = $old_transient[ array_key_first( $old_transient ) ];
}
// Delete current product transient so that it can be refreshed below.
delete_transient( $transient_name );
// Gets new related products and sets current product transient.
$new_related_product_ids = wc_get_related_products( $post_id, 1000 );
// Combine all product IDs that need their transients cleared.
$related_product_ids = array_unique(
array_merge(
$old_related_product_ids,
$new_related_product_ids
)
);
if ( empty( $related_product_ids ) ) {
return;
}
// Create the list of transient names to delete.
$related_product_transients = array_map(
function ( $id ) {
return 'wc_related_' . $id;
},
$related_product_ids
);
_wc_delete_transients( $related_product_transients );
}
/**
* Function that returns an array containing the IDs of the products that are on sale.
*
* @since 2.0
* @return array
*/
function wc_get_product_ids_on_sale() {
// Load from cache.
$product_ids_on_sale = get_transient( 'wc_products_onsale' );
// Valid cache found.
if ( false !== $product_ids_on_sale ) {
return $product_ids_on_sale;
}
$data_store = WC_Data_Store::load( 'product' );
$on_sale_products = $data_store->get_on_sale_products();
$product_ids_on_sale = wp_parse_id_list( array_merge( wp_list_pluck( $on_sale_products, 'id' ), array_diff( wp_list_pluck( $on_sale_products, 'parent_id' ), array( 0 ) ) ) );
set_transient( 'wc_products_onsale', $product_ids_on_sale, DAY_IN_SECONDS * 30 );
return $product_ids_on_sale;
}
/**
* Function that returns an array containing the IDs of the featured products.
*
* @since 2.1
* @return array
*/
function wc_get_featured_product_ids() {
// Load from cache.
$featured_product_ids = get_transient( 'wc_featured_products' );
// Valid cache found.
if ( false !== $featured_product_ids ) {
return $featured_product_ids;
}
$data_store = WC_Data_Store::load( 'product' );
$featured = $data_store->get_featured_product_ids();
$product_ids = array_keys( $featured );
$parent_ids = array_values( array_filter( $featured ) );
$featured_product_ids = array_unique( array_merge( $product_ids, $parent_ids ) );
set_transient( 'wc_featured_products', $featured_product_ids, DAY_IN_SECONDS * 30 );
return $featured_product_ids;
}
/**
* Filter to allow product_cat in the permalinks for products.
*
* @param string $permalink The existing permalink URL.
* @param WP_Post $post WP_Post object.
* @return string
*/
function wc_product_post_type_link( $permalink, $post ) {
// Abort if post is not a product.
if ( 'product' !== $post->post_type ) {
return $permalink;
}
// Abort early if the placeholder rewrite tag isn't in the generated URL.
if ( false === strpos( $permalink, '%' ) ) {
return $permalink;
}
// Only process category if the permalink structure uses category placeholders.
$needs_category = strpos( $permalink, '%category%' ) !== false || strpos( $permalink, '%product_cat%' ) !== false;
$product_cat = '';
if ( $needs_category ) {
// Get the custom taxonomy terms in use by this post.
$terms = get_the_terms( $post->ID, 'product_cat' );
if ( ! empty( $terms ) && ! is_wp_error( $terms ) && is_array( $terms ) ) {
// Re-index array to ensure sequential keys starting from 0 since filters may remove some keys.
$terms = array_values( $terms );
// Find the deepest category (most ancestors) for the permalink.
$deepest_term = $terms[0];
$deepest_ancestors = $deepest_term->parent ? get_ancestors( $deepest_term->term_id, 'product_cat' ) : array();
foreach ( $terms as $term ) {
if ( $term->term_id === $deepest_term->term_id ) {
continue;
}
// Skip root categories - they can't be deeper than current.
if ( ! $term->parent ) {
continue;
}
$ancestors = get_ancestors( $term->term_id, 'product_cat' );
if ( count( $ancestors ) > count( $deepest_ancestors ) ) {
$deepest_ancestors = $ancestors;
$deepest_term = $term;
}
}
/**
* Filter the product category used for the product permalink.
*
* By default, the deepest category (most ancestors) is selected. Prior to 9.9.0,
* categories were sorted by parent term ID descending, then term ID ascending.
* This filter allows customization of which category is used in the product permalink.
*
* @since 2.4.0
* @since 9.9.0 Selection algorithm changed to use deepest category instead of sort order.
*
* @param WP_Term $deepest_term The selected category term object (deepest category since 9.9.0).
* @param WP_Term[] $terms All category terms assigned to the product.
* @param WP_Post $post The product post object.
*/
$category_object = apply_filters( 'wc_product_post_type_link_product_cat', $deepest_term, $terms, $post );
$category_object = ! $category_object instanceof WP_Term ? $deepest_term : $category_object;
$product_cat = $category_object->slug;
if ( $category_object->parent ) {
// Reuse cached ancestors if the filter didn't change the category, otherwise fetch them.
$ancestors = ( $category_object->term_id === $deepest_term->term_id )
? $deepest_ancestors
: get_ancestors( $category_object->term_id, 'product_cat' );
foreach ( $ancestors as $ancestor ) {
$ancestor_object = get_term( $ancestor, 'product_cat' );
/**
* Filter whether to use only the top-level parent category in the product permalink.
*
* When true, only the top-level ancestor category slug is used instead of
* the full category hierarchy path (e.g., 'parent' instead of 'parent/child/grandchild').
*
* @since 2.6.5
*
* @param bool $use_parent_only Whether to use only the top-level parent category. Default false.
*/
if ( apply_filters( 'woocommerce_product_post_type_link_parent_category_only', false ) ) {
$product_cat = $ancestor_object->slug;
} else {
$product_cat = $ancestor_object->slug . '/' . $product_cat;
}
}
}
} else {
// If no terms are assigned to this post, use a string instead (can't leave the placeholder there).
$product_cat = _x( 'uncategorized', 'slug', 'woocommerce' );
}
}
$find = array(
'%year%',
'%monthnum%',
'%day%',
'%hour%',
'%minute%',
'%second%',
'%post_id%',
'%category%',
'%product_cat%',
);
$replace = array(
date_i18n( 'Y', strtotime( $post->post_date ) ),
date_i18n( 'm', strtotime( $post->post_date ) ),
date_i18n( 'd', strtotime( $post->post_date ) ),
date_i18n( 'H', strtotime( $post->post_date ) ),
date_i18n( 'i', strtotime( $post->post_date ) ),
date_i18n( 's', strtotime( $post->post_date ) ),
(string) $post->ID,
$product_cat,
$product_cat,
);
$permalink = str_replace( $find, $replace, $permalink );
return $permalink;
}
add_filter( 'post_type_link', 'wc_product_post_type_link', 10, 2 );
/**
* Ensure that the product_cat value determined in `wc_product_post_type_link` is the canonical value.
*
* If other values are used in this part of the permalink, it will be redirected.
*
* @return void
*/
function wc_product_canonical_redirect(): void {
global $wp_rewrite;
if (
! did_action( 'woocommerce_init' )
|| ! is_product()
|| ! is_a( $wp_rewrite, WP_Rewrite::class )
) {
return;
}
// In the event we are dealing with ugly permalinks, this will be empty.
$specified_category_slug = get_query_var( 'product_cat' );
$specified_category_slug = is_array( $specified_category_slug ) ? '' : urldecode( (string) $specified_category_slug );
if ( '' === $specified_category_slug ) {
return;
}
// What category slug did we expect? Normally this maps back to the first assigned product_cat
// term. However, this is filterable so we use the relevant helper function to figure this out.
$expected_category_slug = wc_product_post_type_link( '%product_cat%', get_post( get_the_ID() ) );
$expected_category_slug = urldecode( $expected_category_slug );
if ( $specified_category_slug === $expected_category_slug ) {
return;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$query_vars = isset( $_GET ) && is_array( $_GET ) ? $_GET : array();
wp_safe_redirect( add_query_arg( $query_vars, wc_get_product( get_the_ID() )->get_permalink() ), 301 );
exit();
}
add_action( 'template_redirect', 'wc_product_canonical_redirect', 5 );
/**
* Get the placeholder image URL either from media, or use the fallback image.
*
* @param string $size Thumbnail size to use.
* @return string
*/
function wc_placeholder_img_src( $size = 'woocommerce_thumbnail' ) {
$src = WC()->plugin_url() . '/assets/images/placeholder.webp';
$placeholder_image = get_option( 'woocommerce_placeholder_image', 0 );
if ( ! empty( $placeholder_image ) ) {
if ( is_numeric( $placeholder_image ) ) {
$image = wp_get_attachment_image_src( $placeholder_image, $size );
if ( ! empty( $image[0] ) ) {
$src = $image[0];
}
} else {
$src = $placeholder_image;
}
}
return apply_filters( 'woocommerce_placeholder_img_src', $src );
}
/**
* Get the placeholder image.
*
* Uses wp_get_attachment_image if using an attachment ID @since 3.6.0 to handle responsiveness.
*
* @param string $size Image size.
* @param string|array $attr Optional. Attributes for the image markup. Default empty.
* @return string
*/
function wc_placeholder_img( $size = 'woocommerce_thumbnail', $attr = '' ) {
$dimensions = wc_get_image_size( $size );
$placeholder_image = get_option( 'woocommerce_placeholder_image', 0 );
$default_attr = array(
'class' => 'woocommerce-placeholder wp-post-image',
'alt' => __( 'Placeholder', 'woocommerce' ),
);
$attr = wp_parse_args( $attr, $default_attr );
if ( wp_attachment_is_image( $placeholder_image ) ) {
$image_html = wp_get_attachment_image(
$placeholder_image,
$size,
false,
$attr
);
} else {
$image = wc_placeholder_img_src( $size );
$hwstring = image_hwstring( $dimensions['width'], $dimensions['height'] );
$attributes = array();
foreach ( $attr as $name => $value ) {
$attributes[] = esc_attr( $name ) . '="' . esc_attr( $value ) . '"';
}
$image_html = '<img src="' . esc_url( $image ) . '" ' . $hwstring . implode( ' ', $attributes ) . '/>';
}
return apply_filters( 'woocommerce_placeholder_img', $image_html, $size, $dimensions );
}
/**
* Variation Formatting.
*
* Gets a formatted version of variation data or item meta.
*
* @param array|WC_Product_Variation $variation Variation object.
* @param bool $flat Should this be a flat list or HTML list? (default: false).
* @param bool $include_names include attribute names/labels in the list.
* @param bool $skip_attributes_in_name Do not list attributes already part of the variation name.
* @return string
*/
function wc_get_formatted_variation( $variation, $flat = false, $include_names = true, $skip_attributes_in_name = false ) {
$return = '';
if ( is_a( $variation, 'WC_Product_Variation' ) ) {
$variation_attributes = $variation->get_attributes();
$product = $variation;
$variation_name = $variation->get_name();
} else {
$product = false;
$variation_name = '';
// Remove attribute_ prefix from names.
$variation_attributes = array();
if ( is_array( $variation ) ) {
foreach ( $variation as $key => $value ) {
$variation_attributes[ str_replace( 'attribute_', '', $key ) ] = $value;
}
}
}
$list_type = $include_names ? 'dl' : 'ul';
if ( is_array( $variation_attributes ) && ! empty( $variation_attributes ) ) {
if ( ! $flat ) {
$return = '<' . $list_type . ' class="variation">';
}
$variation_list = array();
foreach ( $variation_attributes as $name => $value ) {
// If this is a term slug, get the term's nice name.
if ( taxonomy_exists( $name ) ) {
$term = get_term_by( 'slug', $value, $name );
if ( ! is_wp_error( $term ) && $term && null !== $term->name && '' !== $term->name ) {
$value = $term->name;
}
}
// Do not list attributes already part of the variation name.
if ( '' === $value || ( $skip_attributes_in_name && wc_is_attribute_in_product_name( $value, $variation_name ) ) ) {
continue;
}
if ( $include_names ) {
if ( $flat ) {
$variation_list[] = wc_attribute_label( $name, $product ) . ': ' . rawurldecode( $value );
} else {
$variation_list[] = '<dt>' . wc_attribute_label( $name, $product ) . ':</dt><dd>' . rawurldecode( $value ) . '</dd>';
}
} elseif ( $flat ) {
$variation_list[] = rawurldecode( $value );
} else {
$variation_list[] = '<li>' . rawurldecode( $value ) . '</li>';
}
}
if ( $flat ) {
$return .= implode( ', ', $variation_list );
} else {
$return .= implode( '', $variation_list );
}
if ( ! $flat ) {
$return .= '</' . $list_type . '>';
}
}
return $return;
}
/**
* Schedule start/end sale actions for a product based on its sale dates.
*
* Uses Action Scheduler to fire events at the exact sale start/end times,
* rather than relying on the daily cron.
*
* An action is not scheduled if an identical one (same hook, args, group and
* timestamp) is already pending, so concurrent processes saving the same
* product don't pile up duplicate actions.
*
* @since 10.5.0
* @param WC_Product $product Product object.
* @return void
*/
function wc_schedule_product_sale_events( WC_Product $product ): void {
$product_id = $product->get_id();
$schedule = function ( ?WC_DateTime $date, string $hook ) use ( $product_id ): void {
if ( is_null( $date ) ) {
return;
}
$timestamp = $date->getTimestamp();
if ( $timestamp <= time() ) {
return;
}
$args = array( 'product_id' => $product_id );
// An identical pending action means a concurrent process (parallel save, importer,
// daily cron) already scheduled it after the unschedule-all step ran. The query
// filters by the exact timestamp: a pending action for a different time (e.g. left
// behind by a process that saw older sale dates) must not block scheduling.
$identical_pending = as_get_scheduled_actions(
array(
'hook' => $hook,
'args' => $args,
'group' => 'woocommerce-sales',
'status' => ActionScheduler_Store::STATUS_PENDING,
'date' => gmdate( 'Y-m-d H:i:s', $timestamp ),
'date_compare' => '=',
'per_page' => 1,
),
'ids'
);
if ( empty( $identical_pending ) ) {
as_schedule_single_action( $timestamp, $hook, $args, 'woocommerce-sales' );
}
};
$schedule( $product->get_date_on_sale_from( 'edit' ), 'wc_product_start_scheduled_sale' );
$schedule( $product->get_date_on_sale_to( 'edit' ), 'wc_product_end_scheduled_sale' );
}
/**
* Apply the expected sale state for a product.
*
* This is a shared helper used by both the per-product Action Scheduler
* callbacks and the daily cron safety net.
*
* @since 10.5.0
* @param WC_Product $product Product object.
* @param string $mode 'start' or 'end'.
* @return void
*/
function wc_apply_sale_state_for_product( WC_Product $product, string $mode ): void {
$product_id = $product->get_id();
if ( 'start' === $mode ) {
$sale_price = $product->get_sale_price( 'edit' );
if ( $sale_price ) {
$product->set_price( $sale_price );
$product->save();
// Workaround: `_price` is not in `meta_key_to_props` mapping and only syncs
// when date/price props change in `handle_updated_props()`. Since we only
// changed `price` prop, we must update `_price` meta directly.
// See comment in `WC_Product_Data_Store_CPT::handle_updated_props()`.
update_post_meta( $product_id, '_price', $sale_price );
}
} elseif ( 'end' === $mode ) {
$regular_price = $product->get_regular_price( 'edit' );
$product->set_price( $regular_price );
$product->save();
// Workaround: see above.
update_post_meta( $product_id, '_price', $regular_price );
}
// Refresh the lookup table since only the `price` prop changed, which is
// not in the tracked props list in handle_updated_props().
$data_store = WC_Data_Store::load( 'product' );
if ( $data_store->has_callable( 'refresh_product_lookup_table' ) ) {
$data_store->refresh_product_lookup_table( $product_id ); // @phpstan-ignore method.notFound (Guarded by has_callable() and called via __call() on the underlying product data store instance.)
}
wc_delete_product_transients( $product_id );
// Sync parent variable product price range if this is a variation.
if ( $product->is_type( 'variation' ) ) {
$parent_id = $product->get_parent_id();
if ( $parent_id ) {
WC_Product_Variable::sync( $parent_id );
}
}
}
/**
* Handle scheduled sale start for a product.
*
* This is the Action Scheduler callback that fires at the exact sale start time.
*
* @since 10.5.0
* @param int $product_id Product ID.
* @return void
*/
function wc_handle_product_start_scheduled_sale( $product_id ): void {
$product = wc_get_product( $product_id );
if ( ! $product ) {
return;
}
// Skip product types with derived prices.
if ( $product->is_type( array( 'variable', 'grouped' ) ) ) {
return;
}
// Verify sale should still start (dates/price might have changed since scheduling).
if ( ! $product->get_sale_price( 'edit' ) ) {
return;
}
$now = time();
$date_from = $product->get_date_on_sale_from( 'edit' );
$date_to = $product->get_date_on_sale_to( 'edit' );
if ( $date_from && $date_from->getTimestamp() > $now ) {
return;
}
if ( $date_to && $date_to->getTimestamp() < $now ) {
return;
}
if ( (float) $product->get_price( 'edit' ) === (float) $product->get_sale_price( 'edit' ) ) {
return;
}
wc_apply_sale_state_for_product( $product, 'start' );
}
add_action( 'wc_product_start_scheduled_sale', 'wc_handle_product_start_scheduled_sale' );
/**
* Handle scheduled sale end for a product.
*
* This is the Action Scheduler callback that fires at the exact sale end time.
*
* @since 10.5.0
* @param int $product_id Product ID.
* @return void
*/
function wc_handle_product_end_scheduled_sale( $product_id ): void {
$product = wc_get_product( $product_id );
if ( ! $product ) {
return;
}
// Skip product types with derived prices.
if ( $product->is_type( array( 'variable', 'grouped' ) ) ) {
return;
}
$now = time();
$date_to = $product->get_date_on_sale_to( 'edit' );
if ( $date_to && $date_to->getTimestamp() > $now ) {
return;
}
if ( (float) $product->get_price( 'edit' ) === (float) $product->get_regular_price( 'edit' ) ) {
return;
}
wc_apply_sale_state_for_product( $product, 'end' );
}
add_action( 'wc_product_end_scheduled_sale', 'wc_handle_product_end_scheduled_sale' );
/**
* Schedule sale events when a product is saved with sale dates.
*
* @since 10.5.0
* @param int $product_id Product ID.
* @param WC_Product|null $product Product object (optional).
* @return void
*/
function wc_maybe_schedule_product_sale_events( $product_id, $product = null ): void {
if ( ! $product ) {
$product = wc_get_product( $product_id );
if ( ! $product ) {
return;
}
}
$product_id = $product->get_id();
// Always clear existing events first.
as_unschedule_all_actions( 'wc_product_start_scheduled_sale', array( 'product_id' => $product_id ), 'woocommerce-sales' );
as_unschedule_all_actions( 'wc_product_end_scheduled_sale', array( 'product_id' => $product_id ), 'woocommerce-sales' );
$date_from = $product->get_date_on_sale_from( 'edit' );
$date_to = $product->get_date_on_sale_to( 'edit' );
if ( $date_from || $date_to ) {
wc_schedule_product_sale_events( $product );
}
}
/**
* Schedule sale events when sale date meta is added, updated, or deleted.
*
* Hooks into post meta operations so per-product sale events are kept in sync regardless
* of how the meta is written: WooCommerce CRUD, direct update_post_meta() calls from
* importers, ERP sync tools, or custom code.
*
* @since 10.8.0
* @param int|int[] $meta_id Meta ID (or array of IDs for delete).
* @param int $object_id Post ID.
* @param string $meta_key Meta key.
* @return void
*/
function wc_maybe_schedule_sale_events_on_meta_change( $meta_id, $object_id, $meta_key ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
if ( '_sale_price_dates_from' !== $meta_key && '_sale_price_dates_to' !== $meta_key ) {
return;
}
// Prevent duplicate scheduling when a sale handler's save() rewrites dates already in flight.
if ( doing_action( 'wc_product_start_scheduled_sale' ) || doing_action( 'wc_product_end_scheduled_sale' ) ) {
return;
}
$post_type = get_post_type( $object_id );
if ( 'product' !== $post_type && 'product_variation' !== $post_type ) {
return;
}
wc_maybe_schedule_product_sale_events( $object_id );
}
add_action( 'added_post_meta', 'wc_maybe_schedule_sale_events_on_meta_change', 10, 3 );
add_action( 'updated_post_meta', 'wc_maybe_schedule_sale_events_on_meta_change', 10, 3 );
add_action( 'deleted_post_meta', 'wc_maybe_schedule_sale_events_on_meta_change', 10, 3 );
/**
* Function which handles the start and end of scheduled sales via cron.
*
* Previously, this daily cron was the only mechanism for starting/ending scheduled
* sales, which caused timing issues - sales could be "a day off" depending on when
* WP-Cron ran. Now, per-product Action Scheduler events fire at exact sale times.
*
* This function now acts as a safety net to:
* 1. Catch any products missed by the per-product Action Scheduler events
* 2. Handle products created before the AS events were introduced
*
* This function is kept for backwards compatibility. Extenders may hook into the
* `woocommerce_scheduled_sales` cron event or the before/after hooks fired within.
*
* Note: The before/after hooks (wc_before_products_starting_sales, etc.) only fire
* when this cron finds products to process. If per-product AS events handled sales
* on time, these hooks may not fire.
*
* @since 3.0.0
*/
function wc_scheduled_sales() {
$data_store = WC_Data_Store::load( 'product' );
$product_util = wc_get_container()->get( ProductUtil::class );
$must_refresh_transient = false;
// Sales which are due to start.
$product_ids = $data_store->get_starting_sales();
if ( $product_ids ) {
_prime_post_caches( $product_ids );
$must_refresh_transient = true;
do_action( 'wc_before_products_starting_sales', $product_ids );
foreach ( $product_ids as $product_id ) {
$product = wc_get_product( $product_id );
if ( $product ) {
wc_apply_sale_state_for_product( $product, 'start' );
// Note: wc_apply_sale_state_for_product() calls save(), which writes sale
// date meta and triggers wc_maybe_schedule_sale_events_on_meta_change(),
// which schedules the end AS event.
}
$product_util->delete_product_specific_transients( $product ? $product : $product_id );
}
do_action( 'wc_after_products_starting_sales', $product_ids );
delete_transient( 'wc_products_onsale' );
}
// Sales which are due to end.
$product_ids = $data_store->get_ending_sales();
if ( $product_ids ) {
_prime_post_caches( $product_ids );
$must_refresh_transient = true;
do_action( 'wc_before_products_ending_sales', $product_ids );
foreach ( $product_ids as $product_id ) {
$product = wc_get_product( $product_id );
if ( $product ) {
wc_apply_sale_state_for_product( $product, 'end' );
}
$product_util->delete_product_specific_transients( $product ? $product : $product_id );
}
do_action( 'wc_after_products_ending_sales', $product_ids );
delete_transient( 'wc_products_onsale' );
}
if ( $must_refresh_transient ) {
// Kept for compatibility, WooCommerce core doesn't use product transient versions anymore.
WC_Cache_Helper::get_transient_version( 'product', true );
}
}
add_action( 'woocommerce_scheduled_sales', 'wc_scheduled_sales' );
/**
* Get attachment image attributes.
*
* @param array $attr Image attributes.
* @return array
*/
function wc_get_attachment_image_attributes( $attr ) {
/*
* If the user can manage woocommerce, allow them to
* see the image content.
*/
if ( current_user_can( 'manage_woocommerce' ) ) {
return $attr;
}
/*
* If the user does not have the right capabilities,
* filter out the image source and replace with placeholder
* image.
*/
if ( isset( $attr['src'] ) && strstr( $attr['src'], 'woocommerce_uploads/' ) ) {
$attr['src'] = wc_placeholder_img_src();
if ( isset( $attr['srcset'] ) ) {
$attr['srcset'] = '';
}
}
return $attr;
}
add_filter( 'wp_get_attachment_image_attributes', 'wc_get_attachment_image_attributes' );
/**
* Prepare attachment for JavaScript.
*
* @param array $response JS version of a attachment post object.
* @return array
*/
function wc_prepare_attachment_for_js( $response ) {
/*
* If the user can manage woocommerce, allow them to
* see the image content.
*/
if ( current_user_can( 'manage_woocommerce' ) ) {
return $response;
}
/*
* If the user does not have the right capabilities,
* filter out the image source and replace with placeholder
* image.
*/
if ( isset( $response['url'] ) && strstr( $response['url'], 'woocommerce_uploads/' ) ) {
$response['full']['url'] = wc_placeholder_img_src();
if ( isset( $response['sizes'] ) ) {
foreach ( $response['sizes'] as $size => $value ) {
$response['sizes'][ $size ]['url'] = wc_placeholder_img_src();
}
}
}
return $response;
}
add_filter( 'wp_prepare_attachment_for_js', 'wc_prepare_attachment_for_js' );
/**
* Track product views.
*/
function wc_track_product_view() {
if ( ! is_singular( 'product' ) || ! is_active_widget( false, false, 'woocommerce_recently_viewed_products', true ) ) {
return;
}
global $post;
if ( empty( $_COOKIE['woocommerce_recently_viewed'] ) ) { // @codingStandardsIgnoreLine.
$viewed_products = array();
} else {
$viewed_products = wp_parse_id_list( (array) explode( '|', wp_unslash( $_COOKIE['woocommerce_recently_viewed'] ) ) ); // @codingStandardsIgnoreLine.
}
// Unset if already in viewed products list.
$keys = array_flip( $viewed_products );
if ( isset( $keys[ $post->ID ] ) ) {
unset( $viewed_products[ $keys[ $post->ID ] ] );
}
$viewed_products[] = $post->ID;
if ( count( $viewed_products ) > 15 ) {
array_shift( $viewed_products );
}
// Store for session only.
wc_setcookie( 'woocommerce_recently_viewed', implode( '|', $viewed_products ) );
}
add_action( 'template_redirect', 'wc_track_product_view', 20 );
/**
* Get product types.
*
* @since 2.2
* @return array
*/
function wc_get_product_types() {
return (array) apply_filters(
'product_type_selector',
array(
ProductType::SIMPLE => __( 'Simple product', 'woocommerce' ),
ProductType::GROUPED => __( 'Grouped product', 'woocommerce' ),
ProductType::EXTERNAL => __( 'External/Affiliate product', 'woocommerce' ),
ProductType::VARIABLE => __( 'Variable product', 'woocommerce' ),
)
);
}
/**
* Check if product sku is unique.
*
* @since 2.2
* @param int $product_id Product ID.
* @param string $sku Product SKU.
* @return bool
*/
function wc_product_has_unique_sku( $product_id, $sku ) {
/**
* Gives plugins an opportunity to verify SKU uniqueness themselves.
*
* @since 9.0.0
*
* @param bool|null $has_unique_sku Set to a boolean value to short-circuit the default SKU check.
* @param int $product_id The ID of the current product.
* @param string $sku The SKU to check for uniqueness.
*/
$has_unique_sku = apply_filters( 'wc_product_pre_has_unique_sku', null, $product_id, $sku );
if ( ! is_null( $has_unique_sku ) ) {
return boolval( $has_unique_sku );
}
$data_store = WC_Data_Store::load( 'product' );
$sku_found = $data_store->is_existing_sku( $product_id, $sku );
if ( apply_filters( 'wc_product_has_unique_sku', $sku_found, $product_id, $sku ) ) {
return false;
}
return true;
}
/**
* Check if product unique ID is unique.
*
* @since 9.1.0
* @param int $product_id Product ID.
* @param string $global_unique_id Product Unique ID.
* @return bool
*/
function wc_product_has_global_unique_id( $product_id, $global_unique_id ) {
/**
* Gives plugins an opportunity to verify Unique ID uniqueness themselves.
*
* @since 9.1.0
*
* @param bool|null $has_global_unique_id Set to a boolean value to short-circuit the default Unique ID check.
* @param int $product_id The ID of the current product.
* @param string $sku The Unique ID to check for uniqueness.
*/
$has_global_unique_id = apply_filters( 'wc_product_pre_has_global_unique_id', null, $product_id, $global_unique_id );
if ( ! is_null( $has_global_unique_id ) ) {
return boolval( $has_global_unique_id );
}
$data_store = WC_Data_Store::load( 'product' );
if ( $data_store->has_callable( 'is_existing_global_unique_id' ) ) {
$global_unique_id_found = $data_store->is_existing_global_unique_id( $product_id, $global_unique_id );
} else {
$logger = wc_get_logger();
$logger->error( 'The method is_existing_global_unique_id is not implemented in the data store.', array( 'source' => 'wc_product_has_global_unique_id' ) );
}
/**
* Gives plugins an opportunity to verify Unique ID uniqueness themselves.
*
* @since 9.1.0
*
* @param boolean $global_unique_id_found Whether the Unique ID is found.
* @param int $product_id The ID of the current product.
* @param string $sku The Unique ID to check for uniqueness.
*/
if ( apply_filters( 'wc_product_has_global_unique_id', $global_unique_id_found, $product_id, $global_unique_id ) ) {
return false;
}
return true;
}
/**
* Force a unique SKU.
*
* @since 3.0.0
* @param integer $product_id Product ID.
*/
function wc_product_force_unique_sku( $product_id ) {
$product = wc_get_product( $product_id );
$current_sku = $product ? $product->get_sku( 'edit' ) : '';
if ( $current_sku ) {
try {
$new_sku = wc_product_generate_unique_sku( $product_id, $current_sku );
if ( $current_sku !== $new_sku ) {
$product->set_sku( $new_sku );
$product->save();
}
} catch ( Exception $e ) {} // @codingStandardsIgnoreLine.
}
}
/**
* Recursively appends a suffix until a unique SKU is found.
*
* @since 3.0.0
* @param integer $product_id Product ID.
* @param string $sku Product SKU.
* @param integer $index An optional index that can be added to the product SKU.
* @return string
*/
function wc_product_generate_unique_sku( $product_id, $sku, $index = 0 ) {
$generated_sku = 0 < $index ? $sku . '-' . $index : $sku;
if ( ! wc_product_has_unique_sku( $product_id, $generated_sku ) ) {
$generated_sku = wc_product_generate_unique_sku( $product_id, $sku, ( $index + 1 ) );
}
return $generated_sku;
}
/**
* Get product ID by SKU.
*
* @since 2.3.0
* @param string $sku Product SKU.
* @return int
*/
function wc_get_product_id_by_sku( $sku ) {
$data_store = WC_Data_Store::load( 'product' );
return $data_store->get_product_id_by_sku( $sku );
}
/**
* Get product ID by Unique ID.
*
* @since 9.1.0
* @param string $global_unique_id Product Unique ID.
* @return int|null
*/
function wc_get_product_id_by_global_unique_id( $global_unique_id ) {
$data_store = WC_Data_Store::load( 'product' );
if ( $data_store->has_callable( 'get_product_id_by_global_unique_id' ) ) {
return $data_store->get_product_id_by_global_unique_id( $global_unique_id );
} else {
$logger = wc_get_logger();
$logger->error( 'The method get_product_id_by_global_unique_id is not implemented in the data store.', array( 'source' => 'wc_get_product_id_by_global_unique_id' ) );
}
return null;
}
/**
* Get attributes/data for an individual variation from the database and maintain its integrity.
*
* @since 2.4.0
* @param int $variation_id Variation ID.
* @return array
*/
function wc_get_product_variation_attributes( $variation_id ) {
// Build variation data from meta.
$all_meta = is_array( get_post_meta( $variation_id ) ) ? get_post_meta( $variation_id ) : array();
$parent_id = wp_get_post_parent_id( $variation_id );
$parent_attributes = array_filter( (array) get_post_meta( $parent_id, '_product_attributes', true ) );
$found_parent_attributes = array();
$variation_attributes = array();
// Compare to parent variable product attributes and ensure they match.
foreach ( $parent_attributes as $attribute_name => $options ) {
if ( ! empty( $options['is_variation'] ) ) {
$attribute = 'attribute_' . sanitize_title( $attribute_name );
$found_parent_attributes[] = $attribute;
if ( ! array_key_exists( $attribute, $variation_attributes ) ) {
$variation_attributes[ $attribute ] = ''; // Add it - 'any' will be assumed.
}
}
}
// Get the variation attributes from meta.
foreach ( $all_meta as $name => $value ) {
// Only look at valid attribute meta, and also compare variation level attributes and remove any which do not exist at parent level.
if ( 0 !== strpos( $name, 'attribute_' ) || ! in_array( $name, $found_parent_attributes, true ) ) {
unset( $variation_attributes[ $name ] );
continue;
}
/**
* Pre 2.4 handling where 'slugs' were saved instead of the full text attribute.
* Attempt to get full version of the text attribute from the parent.
*/
if ( sanitize_title( $value[0] ) === $value[0] && version_compare( get_post_meta( $parent_id, '_product_version', true ), '2.4.0', '<' ) ) {
foreach ( $parent_attributes as $attribute ) {
if ( 'attribute_' . sanitize_title( $attribute['name'] ) !== $name ) {
continue;
}
$text_attributes = wc_get_text_attributes( $attribute['value'] );
foreach ( $text_attributes as $text_attribute ) {
if ( sanitize_title( $text_attribute ) === $value[0] ) {
$value[0] = $text_attribute;
break;
}
}
}
}
$variation_attributes[ $name ] = $value[0];
}
return $variation_attributes;
}
/**
* Get all product cats for a product by ID, including hierarchy
*
* @since 2.5.0
* @param int $product_id Product ID.
* @return array
*/
function wc_get_product_cat_ids( $product_id ) {
$product_cats = wc_get_product_term_ids( $product_id, 'product_cat' );
foreach ( $product_cats as $product_cat ) {
$product_cats = array_merge( $product_cats, get_ancestors( $product_cat, 'product_cat' ) );
}
return $product_cats;
}
/**
* Gets data about an attachment, such as alt text and captions.
*
* @since 2.6.0
*
* @param int|null $attachment_id Attachment ID.
* @param WC_Product|bool $product WC_Product object.
*
* @return array
*/
function wc_get_product_attachment_props( $attachment_id = null, $product = false ) {
$props = array(
'title' => '',
'caption' => '',
'url' => '',
'alt' => '',
'src' => '',
'srcset' => false,
'sizes' => false,
);
$attachment = get_post( $attachment_id );
if ( $attachment && 'attachment' === $attachment->post_type ) {
$props['title'] = wp_strip_all_tags( $attachment->post_title );
$props['caption'] = wp_strip_all_tags( $attachment->post_excerpt );
$props['url'] = wp_get_attachment_url( $attachment_id );
// Alt text.
$alt_text = array( wp_strip_all_tags( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ), $props['caption'], wp_strip_all_tags( $attachment->post_title ) );
if ( $product && $product instanceof WC_Product ) {
$alt_text[] = wp_strip_all_tags( get_the_title( $product->get_id() ) );
}
$alt_text = array_filter( $alt_text );
$props['alt'] = $alt_text ? reset( $alt_text ) : '';
/**
* Filters the size for the full gallery image.
*
* @param string $size Image size name.
*
* @since 2.6.0
*/
$full_size = apply_filters( 'woocommerce_gallery_full_size', apply_filters( 'woocommerce_product_thumbnails_large_size', 'full' ) );
$src = wp_get_attachment_image_src( $attachment_id, $full_size );
$props['full_src'] = $src[0] ?? null;
$props['full_src_w'] = $src[1] ?? null;
$props['full_src_h'] = $src[2] ?? null;
$gallery_thumbnail = wc_get_image_size( 'gallery_thumbnail' );
/**
* Filters the size for the gallery thumbnail.
*
* @param array $size Array containing width and height dimensions.
*
* @since 2.6.0
*/
$gallery_thumbnail_size = apply_filters( 'woocommerce_gallery_thumbnail_size', array( $gallery_thumbnail['width'], $gallery_thumbnail['height'] ) );
$src = wp_get_attachment_image_src( $attachment_id, $gallery_thumbnail_size );
$props['gallery_thumbnail_src'] = $src[0] ?? null;
$props['gallery_thumbnail_src_w'] = $src[1] ?? null;
$props['gallery_thumbnail_src_h'] = $src[2] ?? null;
/**
* Filters the thumbnail size.
*
* @param string $size Image size name.
*
* @since 2.6.0
*/
$thumbnail_size = apply_filters( 'woocommerce_thumbnail_size', 'woocommerce_thumbnail' );
$src = wp_get_attachment_image_src( $attachment_id, $thumbnail_size );
$props['thumb_src'] = $src[0] ?? null;
$props['thumb_src_w'] = $src[1] ?? null;
$props['thumb_src_h'] = $src[2] ?? null;
/**
* Filters the size for the gallery image.
*
* @param string $size Image size name.
*
* @since 2.6.0
*/
$image_size = apply_filters( 'woocommerce_gallery_image_size', 'woocommerce_single' );
$src = wp_get_attachment_image_src( $attachment_id, $image_size );
$props['src'] = $src[0] ?? null;
$props['src_w'] = $src[1] ?? null;
$props['src_h'] = $src[2] ?? null;
$props['srcset'] = function_exists( 'wp_get_attachment_image_srcset' ) ? wp_get_attachment_image_srcset( $attachment_id, $image_size ) : false;
$props['sizes'] = function_exists( 'wp_get_attachment_image_sizes' ) ? wp_get_attachment_image_sizes( $attachment_id, $image_size ) : false;
}
return $props;
}
/**
* Get product visibility options.
*
* @since 3.0.0
* @return array
*/
function wc_get_product_visibility_options() {
return apply_filters(
'woocommerce_product_visibility_options',
array(
CatalogVisibility::VISIBLE => __( 'Shop and search results', 'woocommerce' ),
CatalogVisibility::CATALOG => __( 'Shop only', 'woocommerce' ),
CatalogVisibility::SEARCH => __( 'Search results only', 'woocommerce' ),
CatalogVisibility::HIDDEN => __( 'Hidden', 'woocommerce' ),
)
);
}
/**
* Get product tax class options.
*
* @since 3.0.0
* @return array
*/
function wc_get_product_tax_class_options() {
$tax_classes = WC_Tax::get_tax_classes();
$tax_class_options = array();
$tax_class_options[''] = __( 'Standard', 'woocommerce' );
if ( ! empty( $tax_classes ) ) {
foreach ( $tax_classes as $class ) {
$tax_class_options[ sanitize_title( $class ) ] = $class;
}
}
return $tax_class_options;
}
/**
* Get stock status options.
*
* @since 3.0.0
* @return array
*/
function wc_get_product_stock_status_options() {
return apply_filters(
'woocommerce_product_stock_status_options',
array(
ProductStockStatus::IN_STOCK => __( 'In stock', 'woocommerce' ),
ProductStockStatus::OUT_OF_STOCK => __( 'Out of stock', 'woocommerce' ),
ProductStockStatus::ON_BACKORDER => __( 'On backorder', 'woocommerce' ),
)
);
}
/**
* Get backorder options.
*
* @since 3.0.0
* @return array
*/
function wc_get_product_backorder_options() {
return array(
'no' => __( 'Do not allow', 'woocommerce' ),
'notify' => __( 'Allow, but notify customer', 'woocommerce' ),
'yes' => __( 'Allow', 'woocommerce' ),
);
}
/**
* Get related products based on product category and tags.
*
* @since 3.0.0
* @param int $product_id Product ID.
* @param int $limit Limit of results.
* @param array $exclude_ids Exclude IDs from the results.
* @param array $related_by Related by category and tags boolean flags.
* @return array
*/
function wc_get_related_products( $product_id, $limit = 5, $exclude_ids = array(), $related_by = array() ) {
// Log an error if the limit is not an integer since this is what we expect.
// However this is not a problem and we can continue.
if ( ! is_int( $limit ) ) {
wc_get_logger()->error(
sprintf(
'Invalid limit type passed to wc_get_related_products. Expected integer, got %s with value: %s',
gettype( $limit ),
wp_json_encode( $limit )
),
array( 'source' => 'wc_get_related_products' )
);
}
// If the limit is not numeric, set it to null.
$limit = is_numeric( $limit ) ? (int) $limit : null;
if ( null === $limit ) {
return array();
}
$product_id = absint( $product_id );
$limit = $limit >= -1 ? $limit : 5;
$exclude_ids = array_merge( array( 0, $product_id ), $exclude_ids );
$transient_name = 'wc_related_' . $product_id;
$query_args = http_build_query(
array(
'limit' => $limit,
'exclude_ids' => $exclude_ids,
'related_by' => $related_by,
)
);
$transient = get_transient( $transient_name );
$related_posts = $transient && is_array( $transient ) && isset( $transient[ $query_args ] ) ? $transient[ $query_args ] : false;
// We want to query related posts if they are not cached, or we don't have enough.
if ( false === $related_posts || count( $related_posts ) < $limit ) {
$cats_array = apply_filters( 'woocommerce_product_related_posts_relate_by_category', true, $product_id ) ? apply_filters( 'woocommerce_get_related_product_cat_terms', wc_get_product_term_ids( $product_id, 'product_cat' ), $product_id ) : array();
$tags_array = apply_filters( 'woocommerce_product_related_posts_relate_by_tag', true, $product_id ) ? apply_filters( 'woocommerce_get_related_product_tag_terms', wc_get_product_term_ids( $product_id, 'product_tag' ), $product_id ) : array();
// Don't bother if none are set, unless woocommerce_product_related_posts_force_display is set to true in which case all products are related.
if ( empty( $cats_array ) && empty( $tags_array ) && ! apply_filters( 'woocommerce_product_related_posts_force_display', false, $product_id ) ) {
$related_posts = array();
} else {
$data_store = WC_Data_Store::load( 'product' );
$related_posts = $data_store->get_related_products( $cats_array, $tags_array, $exclude_ids, $limit + 10, $product_id );
}
if ( $transient && is_array( $transient ) ) {
$transient[ $query_args ] = $related_posts;
} else {
$transient = array( $query_args => $related_posts );
}
set_transient( $transient_name, $transient, DAY_IN_SECONDS );
}
$related_posts = apply_filters(
'woocommerce_related_products',
$related_posts,
$product_id,
array(
'limit' => $limit,
'excluded_ids' => $exclude_ids,
)
);
$related_posts = is_array( $related_posts ) ? $related_posts : array();
if ( apply_filters( 'woocommerce_product_related_posts_shuffle', true ) ) {
shuffle( $related_posts );
}
return array_slice( $related_posts, 0, $limit );
}
/**
* Retrieves product term ids for a taxonomy.
*
* @since 3.0.0
* @param int $product_id Product ID.
* @param string $taxonomy Taxonomy slug.
* @return array
*/
function wc_get_product_term_ids( $product_id, $taxonomy ) {
$terms = get_the_terms( $product_id, $taxonomy );
return ( empty( $terms ) || is_wp_error( $terms ) ) ? array() : wp_list_pluck( $terms, 'term_id' );
}
/**
* For a given product, and optionally price/qty, work out the price with tax included, based on store settings.
*
* @since 3.0.0
* @param WC_Product $product WC_Product object.
* @param array $args Optional arguments to pass product quantity and price.
* @return float|string Price with tax included, or an empty string if price calculation failed.
*/
function wc_get_price_including_tax( $product, $args = array() ) {
$args = wp_parse_args(
$args,
array(
'qty' => '',
'price' => '',
)
);
$price = '' !== $args['price'] ? max( 0.0, (float) $args['price'] ) : (float) $product->get_price();
$qty = '' !== $args['qty'] ? max( 0.0, (float) $args['qty'] ) : 1;
if ( empty( $qty ) ) {
return 0.0;
}
$line_price = $price * $qty;
$return_price = $line_price;
if ( $product->is_taxable() ) {
if ( ! wc_prices_include_tax() ) {
// If the customer is exempt from VAT, set tax total to 0.
if ( ! empty( WC()->customer ) && WC()->customer->get_is_vat_exempt() ) {
$taxes_total = 0.00;
} else {
$tax_rates = WC_Tax::get_rates( $product->get_tax_class() );
$taxes = WC_Tax::calc_tax( $line_price, $tax_rates, false );
if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) {
$taxes_total = array_sum( $taxes );
} else {
$taxes_total = array_sum( array_map( 'wc_round_tax_total', $taxes ) );
}
}
$return_price = NumberUtil::round( $line_price + $taxes_total, wc_get_price_decimals() );
} else {
$tax_rates = WC_Tax::get_rates( $product->get_tax_class() );
$base_tax_rates = WC_Tax::get_base_tax_rates( $product->get_tax_class( 'unfiltered' ) );
/**
* If the customer is exempt from VAT, remove the taxes here.
* Either remove the base or the user taxes depending on woocommerce_adjust_non_base_location_prices setting.
*/
if ( ! empty( WC()->customer ) && WC()->customer->get_is_vat_exempt() ) { // @codingStandardsIgnoreLine.
$remove_taxes = apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ? WC_Tax::calc_tax( $line_price, $base_tax_rates, true ) : WC_Tax::calc_tax( $line_price, $tax_rates, true );
if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) {
$remove_taxes_total = array_sum( $remove_taxes );
} else {
$remove_taxes_total = array_sum( array_map( 'wc_round_tax_total', $remove_taxes ) );
}
$return_price = NumberUtil::round( $line_price - $remove_taxes_total, wc_get_price_decimals() );
/**
* The woocommerce_adjust_non_base_location_prices filter can stop base taxes being taken off when dealing with out of base locations.
* e.g. If a product costs 10 including tax, all users will pay 10 regardless of location and taxes.
* This feature is experimental @since 2.4.7 and may change in the future. Use at your risk.
*/
} elseif ( $tax_rates !== $base_tax_rates && apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) {
$base_taxes = WC_Tax::calc_tax( $line_price, $base_tax_rates, true );
$modded_taxes = WC_Tax::calc_tax( $line_price - array_sum( $base_taxes ), $tax_rates, false );
if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) {
$base_taxes_total = array_sum( $base_taxes );
$modded_taxes_total = array_sum( $modded_taxes );
} else {
$base_taxes_total = array_sum( array_map( 'wc_round_tax_total', $base_taxes ) );
$modded_taxes_total = array_sum( array_map( 'wc_round_tax_total', $modded_taxes ) );
}
$return_price = NumberUtil::round( $line_price - $base_taxes_total + $modded_taxes_total, wc_get_price_decimals() );
}
}
}
return apply_filters( 'woocommerce_get_price_including_tax', $return_price, $qty, $product );
}
/**
* For a given product, and optionally price/qty, work out the price with tax excluded, based on store settings.
*
* @since 3.0.0
* @param WC_Product $product WC_Product object.
* @param array $args Optional arguments to pass product quantity and price.
* @return float|string Price with tax excluded, or an empty string if price calculation failed.
*/
function wc_get_price_excluding_tax( $product, $args = array() ) {
if ( ! ( $product instanceof WC_Product ) ) {
return '';
}
$args = wp_parse_args(
$args,
array(
'qty' => '',
'price' => '',
)
);
$price = '' !== $args['price'] ? max( 0.0, (float) $args['price'] ) : (float) $product->get_price();
$qty = '' !== $args['qty'] ? max( 0.0, (float) $args['qty'] ) : 1;
if ( empty( $qty ) ) {
return 0.0;
}
$line_price = $price * $qty;
if ( $product->is_taxable() && wc_prices_include_tax() ) {
$order = ArrayUtil::get_value_or_default( $args, 'order' );
$customer_id = $order ? $order->get_customer_id() : 0;
$tax_rates = false;
if ( apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) {
$tax_rates = WC_Tax::get_base_tax_rates( $product->get_tax_class( 'unfiltered' ) );
} elseif ( $customer_id ) {
$customer = wc_get_container()->get( LegacyProxy::class )->get_instance_of( WC_Customer::class, $customer_id );
$tax_rates = WC_Tax::get_rates( $product->get_tax_class(), $customer );
} elseif ( is_object( $order ) && method_exists( $order, 'get_taxable_location' ) ) {
$tax_location = $order->get_taxable_location();
if ( is_array( $tax_location ) && isset( $tax_location['country'] ) ) {
$tax_rates = WC_Tax::find_rates(
array(
'country' => $tax_location['country'],
'state' => $tax_location['state'] ?? '',
'postcode' => $tax_location['postcode'] ?? '',
'city' => $tax_location['city'] ?? '',
'tax_class' => $product->get_tax_class(),
)
);
}
}
// Fallback if no tax rates were determined.
if ( false === $tax_rates ) {
$tax_rates = WC_Tax::get_rates( $product->get_tax_class(), null );
}
$remove_taxes = WC_Tax::calc_tax( $line_price, $tax_rates, true );
$return_price = $line_price - array_sum( $remove_taxes ); // Unrounded since we're dealing with tax inclusive prices. Matches logic in cart-totals class. @see adjust_non_base_location_price.
} else {
$return_price = $line_price;
}
return apply_filters( 'woocommerce_get_price_excluding_tax', $return_price, $qty, $product );
}
/**
* Returns the price including or excluding tax.
*
* By default it's based on the 'woocommerce_tax_display_shop' setting.
* Set `$arg['display_context']` to 'cart' to base on the 'woocommerce_tax_display_cart' setting instead.
*
* @since 3.0.0
* @since 7.6.0 Added `display_context` argument.
*
* @param WC_Product $product WC_Product object.
* @param array $args Optional arguments to pass product quantity and price.
* @return float
*/
function wc_get_price_to_display( $product, $args = array() ) {
$args = wp_parse_args(
$args,
array(
'qty' => 1,
'price' => $product->get_price(),
'display_context' => 'shop',
)
);
$price = $args['price'];
$qty = $args['qty'];
$tax_display = get_option(
'cart' === $args['display_context'] ? 'woocommerce_tax_display_cart' : 'woocommerce_tax_display_shop'
);
return TaxDisplayMode::INCLUSIVE === $tax_display ?
wc_get_price_including_tax(
$product,
array(
'qty' => $qty,
'price' => $price,
)
) :
wc_get_price_excluding_tax(
$product,
array(
'qty' => $qty,
'price' => $price,
)
);
}
/**
* Returns the product categories in a list.
*
* @param int $product_id Product ID.
* @param string $sep (default: ', ').
* @param string $before (default: '').
* @param string $after (default: '').
* @return string
*/
function wc_get_product_category_list( $product_id, $sep = ', ', $before = '', $after = '' ) {
return get_the_term_list( $product_id, 'product_cat', $before, $sep, $after );
}
/**
* Returns the product tags in a list.
*
* @param int $product_id Product ID.
* @param string $sep (default: ', ').
* @param string $before (default: '').
* @param string $after (default: '').
* @return string
*/
function wc_get_product_tag_list( $product_id, $sep = ', ', $before = '', $after = '' ) {
return get_the_term_list( $product_id, 'product_tag', $before, $sep, $after );
}
/**
* Callback for array filter to get visible only.
*
* @since 3.0.0
* @param WC_Product $product WC_Product object.
* @return bool
*/
function wc_products_array_filter_visible( $product ) {
return $product && is_a( $product, 'WC_Product' ) && $product->is_visible();
}
/**
* Callback for array filter to get visible grouped products only.
*
* @since 3.1.0
* @param WC_Product $product WC_Product object.
* @return bool
*/
function wc_products_array_filter_visible_grouped( $product ) {
return $product && is_a( $product, 'WC_Product' ) && ( ProductStatus::PUBLISH === $product->get_status() || current_user_can( 'edit_product', $product->get_id() ) );
}
/**
* Callback for array filter to get products the user can edit only.
*
* @since 3.0.0
* @param WC_Product $product WC_Product object.
* @return bool
*/
function wc_products_array_filter_editable( $product ) {
return $product && is_a( $product, 'WC_Product' ) && current_user_can( 'edit_product', $product->get_id() );
}
/**
* Callback for array filter to get products the user can view only.
*
* @since 3.4.0
* @param WC_Product $product WC_Product object.
* @return bool
*/
function wc_products_array_filter_readable( $product ) {
return $product && is_a( $product, 'WC_Product' ) && current_user_can( 'read_product', $product->get_id() );
}
/**
* Sort an array of products by a value.
*
* @since 3.0.0
*
* @param array $products List of products to be ordered.
* @param string $orderby Optional order criteria.
* @param string $order Ascending or descending order.
*
* @return array
*/
function wc_products_array_orderby( $products, $orderby = 'date', $order = 'desc' ) {
$orderby = strtolower( $orderby );
$order = strtolower( $order );
switch ( $orderby ) {
case 'title':
case 'id':
case 'date':
case 'modified':
case 'menu_order':
case 'price':
usort( $products, 'wc_products_array_orderby_' . $orderby );
break;
case 'none':
break;
default:
shuffle( $products );
break;
}
if ( 'desc' === $order ) {
$products = array_reverse( $products );
}
return $products;
}
/**
* Sort by title.
*
* @since 3.0.0
* @param WC_Product $a First WC_Product object.
* @param WC_Product $b Second WC_Product object.
* @return int
*/
function wc_products_array_orderby_title( $a, $b ) {
return strcasecmp( $a->get_name(), $b->get_name() );
}
/**
* Sort by id.
*
* @since 3.0.0
* @param WC_Product $a First WC_Product object.
* @param WC_Product $b Second WC_Product object.
* @return int
*/
function wc_products_array_orderby_id( $a, $b ) {
if ( $a->get_id() === $b->get_id() ) {
return 0;
}
return ( $a->get_id() < $b->get_id() ) ? -1 : 1;
}
/**
* Sort by date.
*
* @since 3.0.0
* @param WC_Product $a First WC_Product object.
* @param WC_Product $b Second WC_Product object.
* @return int
*/
function wc_products_array_orderby_date( $a, $b ) {
if ( $a->get_date_created() === $b->get_date_created() ) {
return 0;
}
return ( $a->get_date_created() < $b->get_date_created() ) ? -1 : 1;
}
/**
* Sort by modified.
*
* @since 3.0.0
* @param WC_Product $a First WC_Product object.
* @param WC_Product $b Second WC_Product object.
* @return int
*/
function wc_products_array_orderby_modified( $a, $b ) {
if ( $a->get_date_modified() === $b->get_date_modified() ) {
return 0;
}
return ( $a->get_date_modified() < $b->get_date_modified() ) ? -1 : 1;
}
/**
* Sort by menu order.
*
* @since 3.0.0
* @param WC_Product $a First WC_Product object.
* @param WC_Product $b Second WC_Product object.
* @return int
*/
function wc_products_array_orderby_menu_order( $a, $b ) {
if ( $a->get_menu_order() === $b->get_menu_order() ) {
return 0;
}
return ( $a->get_menu_order() < $b->get_menu_order() ) ? -1 : 1;
}
/**
* Sort by price low to high.
*
* @since 3.0.0
* @param WC_Product $a First WC_Product object.
* @param WC_Product $b Second WC_Product object.
* @return int
*/
function wc_products_array_orderby_price( $a, $b ) {
if ( $a->get_price() === $b->get_price() ) {
return 0;
}
return ( $a->get_price() < $b->get_price() ) ? -1 : 1;
}
/**
* Queue a product for syncing at the end of the request.
*
* @param int $product_id Product ID.
*/
function wc_deferred_product_sync( $product_id ) {
global $wc_deferred_product_sync;
if ( empty( $wc_deferred_product_sync ) ) {
$wc_deferred_product_sync = array();
}
$wc_deferred_product_sync[] = $product_id;
}
/**
* See if the lookup table is being generated already.
*
* @since 3.6.0
* @return bool
*/
function wc_update_product_lookup_tables_is_running() {
$table_updates_pending = WC()->queue()->search(
array(
'status' => 'pending',
'group' => 'wc_update_product_lookup_tables',
'per_page' => 1,
)
);
return (bool) count( $table_updates_pending );
}
/**
* Populate lookup table data for products.
*
* @since 3.6.0
*/
function wc_update_product_lookup_tables() {
global $wpdb;
$is_cli = Constants::is_true( 'WP_CLI' );
// Note that the table is not yet generated.
update_option( 'woocommerce_product_lookup_table_is_generating', true );
// Make a row per product in lookup table.
$wpdb->query(
"
INSERT IGNORE INTO {$wpdb->wc_product_meta_lookup} (`product_id`)
SELECT
posts.ID
FROM {$wpdb->posts} posts
WHERE
posts.post_type IN ('product', 'product_variation')
"
);
// List of column names in the lookup table we need to populate.
$columns = array(
'min_max_price',
'stock_quantity',
'sku',
'global_unique_id',
'stock_status',
'average_rating',
'total_sales',
'downloadable',
'virtual',
'onsale',
'tax_class',
'tax_status', // When last column is updated, woocommerce_product_lookup_table_is_generating is updated.
);
foreach ( $columns as $index => $column ) {
if ( $is_cli ) {
wc_update_product_lookup_tables_column( $column );
} else {
WC()->queue()->schedule_single(
time() + $index,
'wc_update_product_lookup_tables_column',
array(
'column' => $column,
),
'wc_update_product_lookup_tables'
);
}
}
// Rating counts are serialised so they have to be unserialised before populating the lookup table.
if ( $is_cli ) {
$rating_count_rows = $wpdb->get_results(
"
SELECT post_id, meta_value FROM {$wpdb->postmeta}
WHERE meta_key = '_wc_rating_count'
AND meta_value != ''
AND meta_value != 'a:0:{}'
",
ARRAY_A
);
wc_update_product_lookup_tables_rating_count( $rating_count_rows );
} else {
WC()->queue()->schedule_single(
time() + 10,
'wc_update_product_lookup_tables_rating_count_batch',
array(
'offset' => 0,
'limit' => 50,
),
'wc_update_product_lookup_tables'
);
}
}
/**
* Populate lookup table column data.
*
* @since 3.6.0
* @param string $column Column name to set.
*/
function wc_update_product_lookup_tables_column( $column ) {
if ( empty( $column ) ) {
return;
}
global $wpdb;
switch ( $column ) {
case 'min_max_price':
$wpdb->query(
"
UPDATE
{$wpdb->wc_product_meta_lookup} lookup_table
INNER JOIN (
SELECT lookup_table.product_id, MIN( meta_value+0 ) as min_price, MAX( meta_value+0 ) as max_price
FROM {$wpdb->wc_product_meta_lookup} lookup_table
LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = '_price'
WHERE
meta1.meta_value <> ''
GROUP BY lookup_table.product_id
) as source on source.product_id = lookup_table.product_id
SET
lookup_table.min_price = source.min_price,
lookup_table.max_price = source.max_price
"
);
break;
case 'stock_quantity':
$wpdb->query(
"
UPDATE
{$wpdb->wc_product_meta_lookup} lookup_table
LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = '_manage_stock'
LEFT JOIN {$wpdb->postmeta} meta2 ON lookup_table.product_id = meta2.post_id AND meta2.meta_key = '_stock'
SET
lookup_table.stock_quantity = meta2.meta_value
WHERE
meta1.meta_value = 'yes'
"
);
break;
case 'sku':
case 'global_unique_id':
case 'stock_status':
case 'average_rating':
case 'total_sales':
case 'tax_class':
case 'tax_status':
if ( 'total_sales' === $column ) {
$meta_key = 'total_sales';
} elseif ( 'average_rating' === $column ) {
$meta_key = '_wc_average_rating';
} else {
$meta_key = '_' . $column;
}
$column = esc_sql( $column );
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->query(
$wpdb->prepare(
"
UPDATE
{$wpdb->wc_product_meta_lookup} lookup_table
LEFT JOIN {$wpdb->postmeta} meta ON lookup_table.product_id = meta.post_id AND meta.meta_key = %s
SET
lookup_table.`{$column}` = meta.meta_value
",
$meta_key
)
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
break;
case 'downloadable':
case 'virtual':
$column = esc_sql( $column );
$meta_key = '_' . $column;
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->query(
$wpdb->prepare(
"
UPDATE
{$wpdb->wc_product_meta_lookup} lookup_table
LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = %s
SET
lookup_table.`{$column}` = IF ( meta1.meta_value = 'yes', 1, 0 )
",
$meta_key
)
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
break;
case 'onsale':
$column = esc_sql( $column );
$decimals = absint( wc_get_price_decimals() );
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->query(
$wpdb->prepare(
"
UPDATE
{$wpdb->wc_product_meta_lookup} lookup_table
LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = '_price'
LEFT JOIN {$wpdb->postmeta} meta2 ON lookup_table.product_id = meta2.post_id AND meta2.meta_key = '_sale_price'
LEFT JOIN {$wpdb->postmeta} meta3 ON lookup_table.product_id = meta3.post_id AND meta3.meta_key = '_regular_price'
SET
lookup_table.`{$column}` = IF (
CAST( meta1.meta_value AS DECIMAL ) >= 0
AND CAST( meta2.meta_value AS CHAR ) != ''
AND CAST( meta1.meta_value AS DECIMAL( 10, %d ) ) = CAST( meta2.meta_value AS DECIMAL( 10, %d ) )
AND CAST( meta3.meta_value AS DECIMAL( 10, %d ) ) > CAST( meta2.meta_value AS DECIMAL( 10, %d ) )
, 1, 0 )
",
$decimals,
$decimals,
$decimals,
$decimals
)
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
break;
}
// Final column - mark complete.
if ( 'tax_status' === $column ) {
delete_option( 'woocommerce_product_lookup_table_is_generating' );
}
}
add_action( 'wc_update_product_lookup_tables_column', 'wc_update_product_lookup_tables_column' );
/**
* Populate rating count lookup table data for products.
*
* @since 3.6.0
* @param array $rows Rows of rating counts to update in lookup table.
*/
function wc_update_product_lookup_tables_rating_count( $rows ) {
if ( ! $rows || ! is_array( $rows ) ) {
return;
}
global $wpdb;
foreach ( $rows as $row ) {
$count = array_sum( (array) maybe_unserialize( $row['meta_value'] ) );
$wpdb->update(
$wpdb->wc_product_meta_lookup,
array(
'rating_count' => absint( $count ),
),
array(
'product_id' => absint( $row['post_id'] ),
)
);
}
}
/**
* Populate a batch of rating count lookup table data for products.
*
* @since 3.6.2
* @param array $offset Offset to query.
* @param array $limit Limit to query.
*/
function wc_update_product_lookup_tables_rating_count_batch( $offset = 0, $limit = 0 ) {
global $wpdb;
if ( ! $limit ) {
return;
}
$rating_count_rows = $wpdb->get_results(
$wpdb->prepare(
"
SELECT post_id, meta_value FROM {$wpdb->postmeta}
WHERE meta_key = '_wc_rating_count'
AND meta_value != ''
AND meta_value != 'a:0:{}'
ORDER BY post_id ASC
LIMIT %d, %d
",
$offset,
$limit
),
ARRAY_A
);
if ( $rating_count_rows ) {
wc_update_product_lookup_tables_rating_count( $rating_count_rows );
WC()->queue()->schedule_single(
time() + 1,
'wc_update_product_lookup_tables_rating_count_batch',
array(
'offset' => $offset + $limit,
'limit' => $limit,
),
'wc_update_product_lookup_tables'
);
}
}
add_action( 'wc_update_product_lookup_tables_rating_count_batch', 'wc_update_product_lookup_tables_rating_count_batch', 10, 2 );
/**
* Attach product featured image. Use image filename to match a product sku when product is not provided.
*
* @since 8.5.0
* @param int $attachment_id Media attachment ID.
* @param WC_Product $product Optional product object.
* @param bool $save_product If true, the changes in the product will be saved before the method returns.
* @return void
*/
function wc_product_attach_featured_image( $attachment_id, $product = null, $save_product = true ) {
$attachment_post = get_post( $attachment_id );
if ( ! $attachment_post ) {
return;
}
if ( null === $product && wc_get_container()->get( MatchImageBySKU::class )->is_enabled() ) {
// On upload the attachment post title is the uploaded file's filename.
$file_name = pathinfo( $attachment_post->post_title, PATHINFO_FILENAME );
if ( ! $file_name ) {
return;
}
$product_id = wc_get_product_id_by_sku( $file_name );
$product = wc_get_product( $product_id );
}
if ( ! $product ) {
return;
}
$product->set_image_id( $attachment_id );
if ( $save_product ) {
$product->save();
}
if ( 0 === $attachment_post->post_parent ) {
wp_update_post(
array(
'ID' => $attachment_id,
'post_parent' => $product->get_id(),
)
);
}
}
add_action( 'add_attachment', 'wc_product_attach_featured_image' );
