Automattic\WooCommerce\Api\Queries\Products
ListProducts::execute │ public │ WC 1.0
[ConnectionOf( Product::class )]Method of the class: ListProducts{}
No Hooks.
Returns
null. Nothing (null).
Usage
$ListProducts = new ListProducts(); $ListProducts->execute( $pagination, #[Unroll] ProductFilterInput $filters, #[Description( foo )] ?ProductType $product_type, ?array $_query_info, ): Connection;
- $pagination(PaginationParams) (required)
- .
[Unroll] ProductFilterInput $filters (required)
: .
[Description( foo )] ?ProductType $product_type
: .
Default: null
- ?array $_query_info
.
Default:null(required)
- .
ListProducts::execute() ListProducts::execute code WC 10.9.1
public function execute(
PaginationParams $pagination,
#[Unroll]
ProductFilterInput $filters,
#[Description( 'Filter by product type.' )]
?ProductType $product_type = null,
?array $_query_info = null,
): Connection {
$first = $pagination->first;
$last = $pagination->last;
$after = $pagination->after;
$before = $pagination->before;
$limit = $first ?? $last ?? PaginationParams::get_default_page_size();
$query_args = array(
'post_type' => 'product',
'posts_per_page' => $limit + 1,
'orderby' => 'ID',
'order' => null !== $last ? 'DESC' : 'ASC',
'post_status' => $filters->status?->value ?? 'any',
);
// Product type filter via taxonomy. `ProductType::Other` is the
// output-only signal for "stored product_type doesn't match any
// known standard" (typically plugin-added types), mirroring how
// `StockStatus::Other` is handled for the meta-query path above.
// Map it to NOT IN the standard slugs rather than the literal
// 'other' term, which wouldn't match anything.
if ( null !== $product_type ) {
if ( ProductType::Other === $product_type ) {
$query_args['tax_query'] = array(
array(
'taxonomy' => 'product_type',
'field' => 'slug',
'terms' => array_values(
array_filter(
array_map(
static fn( ProductType $t ): string => $t->value,
ProductType::cases()
),
static fn( string $slug ): bool => ProductType::Other->value !== $slug
)
),
'operator' => 'NOT IN',
),
);
} else {
$query_args['tax_query'] = array(
array(
'taxonomy' => 'product_type',
'field' => 'slug',
'terms' => $product_type->value,
),
);
}
}
// Stock status filter via meta. `StockStatus::Other` means "stored
// _stock_status isn't one of the three standard WooCommerce values"
// (typically a plugin-added custom status), so it maps to NOT IN
// those three. `default` throws INVALID_ARGUMENT so any future
// enum case added without updating this match fails loudly with a
// clean 400 instead of a PHP-level UnhandledMatchError → HTTP 500.
if ( null !== $filters->stock_status ) {
// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Not HTML; serialized as JSON.
$meta_clause = match ( $filters->stock_status ) {
StockStatus::InStock => array(
'key' => '_stock_status',
'value' => 'instock',
),
StockStatus::OutOfStock => array(
'key' => '_stock_status',
'value' => 'outofstock',
),
StockStatus::OnBackorder => array(
'key' => '_stock_status',
'value' => 'onbackorder',
),
StockStatus::Other => array(
'key' => '_stock_status',
'value' => array( 'instock', 'outofstock', 'onbackorder' ),
'compare' => 'NOT IN',
),
default => throw new ApiException(
sprintf( 'Unsupported stock_status filter value: %s.', $filters->stock_status->name ),
'INVALID_ARGUMENT',
status_code: 400,
),
};
// phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
$query_args['meta_query'] = array( $meta_clause );
}
// Search filter.
if ( null !== $filters->search ) {
$query_args['s'] = $filters->search;
}
// Total count query. Derive from $query_args — which already has
// the tax_query / meta_query / search clauses applied — *before*
// we set cursor query vars on it. Building $count_args from scratch
// with only post_status would drop every user filter and report the
// count of "all products in that status" instead of "all products
// matching the filters", making Relay consumers' "X of Y" wrong.
// Only `found_posts` is read, so posts_per_page => 1 keeps the
// underlying SELECT cheap.
$count_args = $query_args;
$count_args['posts_per_page'] = 1;
$count_args['fields'] = 'ids';
$count_query = new \WP_Query( $count_args );
$total_count = $count_query->found_posts;
// Cursor-based filtering via IdCursorFilter (see class docblock).
if ( null !== $after ) {
$query_args[ IdCursorFilter::AFTER_ID ] = IdCursorFilter::decode_id_cursor( $after, 'after' );
}
if ( null !== $before ) {
$query_args[ IdCursorFilter::BEFORE_ID ] = IdCursorFilter::decode_id_cursor( $before, 'before' );
}
IdCursorFilter::ensure_registered();
$query = new \WP_Query( $query_args );
$posts = $query->posts;
// Determine pagination.
$has_extra = count( $posts ) > $limit;
if ( $has_extra ) {
$posts = array_slice( $posts, 0, $limit );
}
if ( null !== $last ) {
$posts = array_reverse( $posts );
}
// Narrow $_query_info to the per-node selection so each mapped
// product only fetches the subtrees the client actually asked for
// under `nodes { ... }` / `edges { node { ... } }`. Without this,
// ProductMapper::populate_common_fields() hits its null-$query_info
// fallback and runs build_reviews() (plus its count query) for
// every product on the page — N+1 on reviews even when no client
// selected them.
$node_query_info = ProductMapper::connection_node_info( $_query_info );
// Build edges and nodes.
$edges = array();
$nodes = array();
foreach ( $posts as $post ) {
$wc_product = wc_get_product( $post->ID );
if ( ! $wc_product instanceof \WC_Product ) {
continue;
}
$product = ProductMapper::from_wc_product( $wc_product, $node_query_info );
$edge = new Edge();
$edge->cursor = base64_encode( (string) $product->id );
$edge->node = $product;
$edges[] = $edge;
$nodes[] = $product;
}
$page_info = new PageInfo();
// Relay semantics for backward pagination (`last`, `before`): the
// returned window ends just before `$before`, so items after the
// window exist whenever `$before` was supplied — not whenever
// `$after` was. `has_previous_page` in the backward case is driven
// by the "did we fetch limit+1?" sentinel (`$has_extra`).
$page_info->has_next_page = null !== $last ? ( null !== $before ) : $has_extra;
$page_info->has_previous_page = null !== $last ? $has_extra : ( null !== $after );
$page_info->start_cursor = ! empty( $edges ) ? $edges[0]->cursor : null;
$page_info->end_cursor = ! empty( $edges ) ? $edges[ count( $edges ) - 1 ]->cursor : null;
$connection = new Connection();
$connection->edges = $edges;
$connection->nodes = $nodes;
$connection->page_info = $page_info;
$connection->total_count = $total_count;
return $connection;
}