get( CostOfGoodsSoldController::class ); $this->cogs_is_enabled = $cogs_controller->feature_is_enabled(); $this->use_cogs_lookup_column = $this->cogs_is_enabled && $cogs_controller->product_meta_lookup_table_cogs_value_columns_exist(); } /** * Pre-warm the wp_count_posts cache before the list table renders. * * @internal * @since 11.0.0 * * @return void */ public function prime_status_counts_cache(): void { $screen = get_current_screen(); if ( ! $screen || 'edit-product' !== $screen->id ) { return; } // Performance note: the current listings architecture prevents us from isolating wp_count_posts calls. // In the context of the products page, we can still isolate the underlying SQL by warming up the wp_count_posts cache. $cache = (object) array_map( static fn ( $count ) => $count ? (string) $count : (int) $count, wc_get_container()->get( ProductUtil::class )->get_counts_for_type( 'product' ) ); // Trade-off: private-status tally may read slightly high for restricted roles (other users' privates included) — non-critical. wp_cache_set_multiple( array( 'posts-product' => $cache, 'posts-product_readable_' . get_current_user_id() => $cache, ), 'counts' ); } /** * Prime featured image caches for product queries to avoid individual queries during rendering. * * @since 10.9.0 * * @param \WP_Post[] $posts Posts from WP Query. * @param \WP_Query $query Current query. * @return array */ public function prime_thumbnail_caches( $posts, $query ) { if ( $query instanceof \WP_Query && 'product' === $query->get( 'post_type' ) ) { update_post_thumbnail_cache( $query ); } return $posts; } /** * Render blank state. */ protected function render_blank_state() { echo '
'; echo '

' . esc_html__( 'Ready to start selling something awesome?', 'woocommerce' ) . '

'; echo '
'; echo '' . esc_html__( 'Create Product', 'woocommerce' ) . ''; echo '' . esc_html__( 'Start Import', 'woocommerce' ) . ''; echo '
'; echo '
'; } /** * Define primary column. * * @return string */ protected function get_primary_column() { return 'name'; } /** * Get row actions to show in the list table. * * @param array $actions Array of actions. * @param WP_Post $post Current post object. * @return array */ protected function get_row_actions( $actions, $post ) { /* translators: %d: product ID. */ return array_merge( array( 'id' => sprintf( __( 'ID: %d', 'woocommerce' ), $post->ID ) ), $actions ); } /** * Define which columns are sortable. * * @param array $columns Existing columns. * @return array */ public function define_sortable_columns( $columns ) { $custom = array( 'price' => 'price', 'sku' => 'sku', 'name' => 'title', 'global_unique_id' => 'global_unique_id', ); if ( $this->use_cogs_lookup_column ) { $custom['cogs_value'] = 'cogs_value'; } return wp_parse_args( $custom, $columns ); } /** * Define which columns to show on this screen. * * @param array $columns Existing columns. * @return array */ public function define_columns( $columns ) { if ( empty( $columns ) && ! is_array( $columns ) ) { $columns = array(); } unset( $columns['title'], $columns['comments'], $columns['date'] ); $show_columns = array(); $show_columns['cb'] = ''; $show_columns['thumb'] = '' . __( 'Image', 'woocommerce' ) . ''; $show_columns['name'] = __( 'Name', 'woocommerce' ); if ( wc_product_sku_enabled() ) { $show_columns['sku'] = __( 'SKU', 'woocommerce' ); } $show_columns['global_unique_id'] = __( 'GTIN, UPC, EAN, or ISBN', 'woocommerce' ); if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { $show_columns['is_in_stock'] = __( 'Stock', 'woocommerce' ); } $show_columns['price'] = __( 'Price', 'woocommerce' ); if ( $this->cogs_is_enabled ) { $show_columns['cogs_value'] = __( 'Cost', 'woocommerce' ); } $show_columns['product_cat'] = __( 'Categories', 'woocommerce' ); if ( is_object_in_taxonomy( 'product', 'product_tag' ) ) { $show_columns['product_tag'] = __( 'Tags', 'woocommerce' ); } $show_columns['featured'] = '' . __( 'Featured', 'woocommerce' ) . ''; $show_columns['date'] = __( 'Date', 'woocommerce' ); return array_merge( $show_columns, $columns ); } /** * Pre-fetch any data for the row each column has access to it. the_product global is there for bw compat. * * @param int $post_id Post ID being shown. */ protected function prepare_row_data( $post_id ) { global $the_product; if ( empty( $this->object ) || $this->object->get_id() !== $post_id ) { $the_product = wc_get_product( $post_id ); $this->object = $the_product; } } /** * Render column: thumb. */ protected function render_thumb_column() { echo '' . $this->object->get_image( 'thumbnail' ) . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_image() returns HTML, including output from the public product image filter. } /** * Render column: name. */ protected function render_name_column() { global $post; $edit_link = get_edit_post_link( $this->object->get_id() ); $title = _draft_or_post_title(); echo '' . esc_html( $title ) . ''; _post_states( $post ); echo ''; if ( $this->object->get_parent_id() > 0 ) { echo '  ← ' . get_the_title( $this->object->get_parent_id() ) . ''; // @codingStandardsIgnoreLine. } get_inline_data( $post ); $cogs_value_html = $this->cogs_is_enabled ? '
' . esc_html( $this->object->get_cogs_value() ?? '0' ) . '
' : ''; /** * Product represented by the current list-table row. * Narrow the inherited object type without adding a PHPStan baseline entry. * In future we should correct the type of $this->object. * * @var WC_Product $product */ $product = $this->object; $sale_date_from = $product->get_date_on_sale_from( 'edit' ); $sale_date_to = $product->get_date_on_sale_to( 'edit' ); $sale_date_from = $sale_date_from ? date_i18n( 'Y-m-d', $sale_date_from->getOffsetTimestamp() ) : ''; $sale_date_to = $sale_date_to ? date_i18n( 'Y-m-d', $sale_date_to->getOffsetTimestamp() ) : ''; // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- the COGS value is already escaped. /* Custom inline data for woocommerce. */ echo ' '; // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Render column: sku. */ protected function render_sku_column() { echo $this->object->get_sku() ? esc_html( $this->object->get_sku() ) : '–'; } /** * Render column: global_unique_id. */ protected function render_global_unique_id_column() { echo $this->object->get_global_unique_id() ? esc_html( $this->object->get_global_unique_id() ) : '–'; } /** * Render column: price. */ protected function render_price_column() { $html = $this->object->get_price_html(); echo $html ? wp_kses_post( $html ) : '–'; } /** * Render column: cost. */ protected function render_cogs_value_column() { $html = $this->object->get_cogs_value_html(); echo $html ? wp_kses_post( $html ) : '–'; } /** * Render column: product_cat. */ protected function render_product_cat_column() { $terms = get_the_terms( $this->object->get_id(), 'product_cat' ); if ( ! $terms ) { echo '–'; } else { $termlist = array(); foreach ( $terms as $term ) { $termlist[] = '' . esc_html( $term->name ) . ''; } /** * Filters the term list rendered in the products list table taxonomy column. * * @since 3.3.0 * * @param string $term_list Comma separated list of escaped term links. * @param string $taxonomy Taxonomy the terms belong to. * @param int $product_id ID of the product the terms are rendered for. * @param string[] $termlist Individual escaped term links. * @param WP_Term[]|WP_Error $terms Term objects assigned to the product, or the WP_Error returned by get_the_terms(). */ echo apply_filters( 'woocommerce_admin_product_term_list', implode( ', ', $termlist ), 'product_cat', $this->object->get_id(), $termlist, $terms ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Term names and links are escaped above; the filter returns extension-controlled markup by design. } } /** * Render column: product_tag. */ protected function render_product_tag_column() { $terms = get_the_terms( $this->object->get_id(), 'product_tag' ); if ( ! $terms ) { echo '–'; } else { $termlist = array(); foreach ( $terms as $term ) { $termlist[] = '' . esc_html( $term->name ) . ''; } /** * Filters the term list rendered in the products list table taxonomy column. * * @since 3.3.0 * * @param string $term_list Comma separated list of escaped term links. * @param string $taxonomy Taxonomy the terms belong to. * @param int $product_id ID of the product the terms are rendered for. * @param string[] $termlist Individual escaped term links. * @param WP_Term[]|WP_Error $terms Term objects assigned to the product, or the WP_Error returned by get_the_terms(). */ echo apply_filters( 'woocommerce_admin_product_term_list', implode( ', ', $termlist ), 'product_tag', $this->object->get_id(), $termlist, $terms ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Term names and links are escaped above; the filter returns extension-controlled markup by design. } } /** * Render column: featured. */ protected function render_featured_column() { $url = wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_feature_product&product_id=' . $this->object->get_id() ), 'woocommerce-feature-product' ); echo ''; if ( $this->object->is_featured() ) { echo '' . esc_html__( 'Yes', 'woocommerce' ) . ''; } else { echo '' . esc_html__( 'No', 'woocommerce' ) . ''; } echo ''; } /** * Render column: is_in_stock. */ protected function render_is_in_stock_column() { if ( $this->object->is_on_backorder() ) { $stock_html = '' . __( 'On backorder', 'woocommerce' ) . ''; } elseif ( $this->object->is_in_stock() ) { $stock_html = '' . __( 'In stock', 'woocommerce' ) . ''; } else { $stock_html = '' . __( 'Out of stock', 'woocommerce' ) . ''; } if ( $this->object->managing_stock() ) { $stock_html .= ' (' . wc_stock_amount( $this->object->get_stock_quantity() ) . ')'; } echo wp_kses_post( apply_filters( 'woocommerce_admin_stock_html', $stock_html, $this->object ) ); } /** * Query vars for custom searches. * * @param mixed $public_query_vars Array of query vars. * @return array */ public function add_custom_query_var( $public_query_vars ) { $public_query_vars[] = 'sku'; return $public_query_vars; } /** * Render any custom filters and search inputs for the list table. */ protected function render_filters() { $filters = apply_filters( 'woocommerce_products_admin_list_table_filters', array( 'product_category' => array( $this, 'render_products_category_filter' ), 'product_type' => array( $this, 'render_products_type_filter' ), 'stock_status' => array( $this, 'render_products_stock_status_filter' ), ) ); ob_start(); foreach ( $filters as $filter_callback ) { call_user_func( $filter_callback ); } $output = ob_get_clean(); /** * Filters the rendered markup for the products list table filter controls. * * @since 2.1.0 * * @param string|false $output Buffered markup produced by the registered filter renderers, or false when no output buffer was active. */ echo apply_filters( 'woocommerce_product_filters', $output ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $output is buffered admin markup and the filter returns extension-controlled markup by design. } /** * Render the product category filter for the list table. * * @since 3.5.0 */ protected function render_products_category_filter() { $categories_count = (int) wp_count_terms( 'product_cat' ); if ( $categories_count <= apply_filters( 'woocommerce_product_category_filter_threshold', 100 ) ) { wc_product_dropdown_categories( array( 'option_select_text' => __( 'Filter by category', 'woocommerce' ), 'hide_empty' => 0, // Performance note: pad_counts=0 skips the hierarchical count SQL — O(all published products), degrades linearly // with catalog size. show_count=0 suppresses the raw wp_term_taxonomy.count that would otherwise render in its place. 'pad_counts' => 0, 'show_count' => 0, ) ); } else { $current_category_slug = isset( $_GET['product_cat'] ) ? wc_clean( wp_unslash( $_GET['product_cat'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only filters; inputs are unslashed and sanitized. $current_category = $current_category_slug ? get_term_by( 'slug', $current_category_slug, 'product_cat' ) : false; ?> '; foreach ( wc_get_product_types() as $value => $label ) { $output .= '