📁 File Manager Pro
v10.0.3 | PHP: 8.2.31
Server: LiteSpeed
2026-07-04 06:38:13
📂
/ (Root)
/
home
/
orkouolp
/
web
/
orkofarms.com
/
wp-content
/
plugins
/
woocommerce
/
src
/
Api
/
Utils
/
Products
📍 /home/orkouolp/web/orkofarms.com/wp-content/plugins/woocommerce/src/Api/Utils/Products
🔄 Refresh
✏️
Editing: ProductMapper.php
Writable
<?php declare(strict_types=1); namespace Automattic\WooCommerce\Api\Utils\Products; use Automattic\WooCommerce\Api\Enums\Products\ProductStatus; use Automattic\WooCommerce\Api\Enums\Products\ProductType; use Automattic\WooCommerce\Api\Enums\Products\StockStatus; use Automattic\WooCommerce\Api\Pagination\Connection; use Automattic\WooCommerce\Api\Pagination\Edge; use Automattic\WooCommerce\Api\Pagination\IdCursorFilter; use Automattic\WooCommerce\Api\Pagination\PageInfo; use Automattic\WooCommerce\Api\Pagination\PaginationParams; use Automattic\WooCommerce\Api\Types\Products\ExternalProduct; use Automattic\WooCommerce\Api\Types\Products\ProductAttribute; use Automattic\WooCommerce\Api\Types\Products\ProductDimensions; use Automattic\WooCommerce\Api\Types\Products\ProductImage; use Automattic\WooCommerce\Api\Types\Products\ProductReview; use Automattic\WooCommerce\Api\Types\Products\ProductVariation; use Automattic\WooCommerce\Api\Types\Products\SelectedAttribute; use Automattic\WooCommerce\Api\Types\Products\SimpleProduct; use Automattic\WooCommerce\Api\Types\Products\VariableProduct; /** * Maps a WC_Product to the appropriate product DTO. */ class ProductMapper { /** * Map a WC_Product to the appropriate product DTO based on its type. * * @param \WC_Product $wc_product The WooCommerce product object. * @param ?array $query_info Unified query info tree from the GraphQL request. * @return object */ public static function from_wc_product( \WC_Product $wc_product, ?array $query_info = null, ): object { $product = match ( $wc_product->get_type() ) { 'external' => self::build_external_product( $wc_product ), 'variable' => self::build_variable_product( $wc_product, $query_info ), 'variation' => self::build_product_variation( $wc_product ), default => new SimpleProduct(), }; self::populate_common_fields( $product, $wc_product, $query_info ); return $product; } /** * Build an ExternalProduct with type-specific fields. * * @param \WC_Product $wc_product The external product. * @return ExternalProduct */ private static function build_external_product( \WC_Product $wc_product ): ExternalProduct { $product = new ExternalProduct(); $url = $wc_product->get_product_url(); $product->product_url = ! empty( $url ) ? $url : null; $text = $wc_product->get_button_text(); $product->button_text = ! empty( $text ) ? $text : null; return $product; } /** * Build a VariableProduct with type-specific fields. * * @param \WC_Product $wc_product The variable product. * @param ?array $query_info Unified query info tree from the GraphQL request. * @return VariableProduct */ private static function build_variable_product( \WC_Product $wc_product, ?array $query_info = null ): VariableProduct { $product = new VariableProduct(); $child_ids = $wc_product->get_children(); $total_count = count( $child_ids ); // Extract the per-variation selection and pagination args from // $query_info up front. Narrowing $query_info keeps recursive // from_wc_product() calls from fetching subtrees the client didn't // request (e.g. reviews for every variation). $variations_info = $query_info['...VariableProduct']['variations'] ?? $query_info['variations'] ?? null; $variation_query_info = self::connection_node_info( $variations_info ); $pagination_args = $variations_info['__args'] ?? array(); // Slice the ID window *before* mapping: otherwise `variations(first: 1)` // on a product with N variations would prime+map all N just to slice // the result down afterwards. The resolver-level validation at // Connection::slice() is now bypassed (we're building a pre-sliced // connection), so call validate_args() explicitly to keep the 0.. // MAX_PAGE_SIZE bounds enforced. PaginationParams::validate_args( $pagination_args ); $page = self::slice_variation_ids( $child_ids, $pagination_args ); // Prime post + meta caches for only the paged subset. if ( ! empty( $page['ids'] ) ) { _prime_post_caches( $page['ids'] ); } $edges = array(); $nodes = array(); foreach ( $page['ids'] as $child_id ) { $child_product = wc_get_product( $child_id ); if ( ! $child_product ) { continue; } $variation = self::from_wc_product( $child_product, $variation_query_info ); $edge = new Edge(); $edge->cursor = base64_encode( (string) $child_id ); $edge->node = $variation; $edges[] = $edge; $nodes[] = $variation; } $page_info = new PageInfo(); $page_info->has_next_page = $page['has_next_page']; $page_info->has_previous_page = $page['has_previous_page']; $page_info->start_cursor = ! empty( $edges ) ? $edges[0]->cursor : null; $page_info->end_cursor = ! empty( $edges ) ? $edges[ count( $edges ) - 1 ]->cursor : null; // total_count reflects the full variation set, not the paged one — // consistent with how the root list resolvers compute it. $product->variations = Connection::pre_sliced( $edges, $page_info, $total_count ); return $product; } /** * Compute a Relay cursor page against a list of variation IDs. * * Mirrors the logic in {@see Connection::slice()} but operates on raw * IDs so the caller can page-down *before* calling `wc_get_product()` * + `from_wc_product()` on each child. Returns the paged IDs and the * corresponding `has_next_page` / `has_previous_page` flags in Relay * semantics. * * @param int[] $child_ids Full variation ID list, in menu_order. * @param array $args `{first?, last?, after?, before?}` raw GraphQL args. * @return array{ids: int[], has_next_page: bool, has_previous_page: bool} */ private static function slice_variation_ids( array $child_ids, array $args ): array { $first = $args['first'] ?? null; $last = $args['last'] ?? null; $after = $args['after'] ?? null; $before = $args['before'] ?? null; // No pagination requested — return the full list as-is. if ( null === $first && null === $last && null === $after && null === $before ) { return array( 'ids' => array_values( $child_ids ), 'has_next_page' => false, 'has_previous_page' => false, ); } // Narrow by `after`: drop IDs up to and including the cursor position. if ( null !== $after ) { $after_id = IdCursorFilter::decode_id_cursor( $after, 'after' ); $idx = array_search( $after_id, $child_ids, true ); $child_ids = false !== $idx ? array_slice( $child_ids, $idx + 1 ) : array(); } // Narrow by `before`: drop IDs from the cursor position onward. if ( null !== $before ) { $before_id = IdCursorFilter::decode_id_cursor( $before, 'before' ); $idx = array_search( $before_id, $child_ids, true ); if ( false !== $idx ) { $child_ids = array_slice( $child_ids, 0, $idx ); } } $total_after_cursors = count( $child_ids ); // Apply first/last limits. if ( null !== $first && $first >= 0 ) { $child_ids = array_slice( $child_ids, 0, $first ); } if ( null !== $last && $last >= 0 ) { $child_ids = array_slice( $child_ids, max( 0, count( $child_ids ) - $last ) ); } // Relay semantics for the forward / backward branches match what // ListProducts / ListCoupons use at the root level. return array( 'ids' => array_values( $child_ids ), 'has_next_page' => null !== $first ? count( $child_ids ) < $total_after_cursors : ( null !== $before ), 'has_previous_page' => null !== $last ? count( $child_ids ) < $total_after_cursors : ( null !== $after ), ); } /** * Build a ProductVariation with type-specific fields. * * @param \WC_Product $wc_product The variation product. * @return ProductVariation */ private static function build_product_variation( \WC_Product $wc_product ): ProductVariation { $product = new ProductVariation(); $product->parent_id = $wc_product->get_parent_id(); $selected_attributes = array(); foreach ( $wc_product->get_attributes() as $taxonomy => $value ) { $attr = new SelectedAttribute(); $attr->name = $taxonomy; // For taxonomy attributes, resolve the slug to a human-readable term name. if ( taxonomy_exists( $taxonomy ) && ! empty( $value ) ) { $term = get_term_by( 'slug', $value, $taxonomy ); if ( $term && ! is_wp_error( $term ) ) { $attr->value = $term->name; } else { $attr->value = $value; } } else { $attr->value = $value; } $selected_attributes[] = $attr; } $product->selected_attributes = $selected_attributes; return $product; } /** * Populate the common fields shared by all product types. * * @param object $product The product DTO to populate. * @param \WC_Product $wc_product The WooCommerce product object. * @param ?array $query_info Unified query info tree from the GraphQL request. */ private static function populate_common_fields( object $product, \WC_Product $wc_product, ?array $query_info, ): void { $raw_status = (string) $wc_product->get_status(); $raw_product_type = (string) $wc_product->get_type(); $product->id = $wc_product->get_id(); $product->name = $wc_product->get_name(); $product->slug = $wc_product->get_slug(); $sku = $wc_product->get_sku(); $product->sku = '' !== $sku ? $sku : null; $product->description = $wc_product->get_description(); $product->short_description = $wc_product->get_short_description(); $product->status = ProductStatus::tryFrom( $raw_status ) ?? ProductStatus::Other; $product->raw_status = $raw_status; $product->product_type = ProductType::tryFrom( $raw_product_type ) ?? ProductType::Other; $product->raw_product_type = $raw_product_type; // Price fields support a "formatted" argument for currency display. // An empty stored value means "not set" and is surfaced as null — // without this, wc_price( (float) '' ) would render as "$0.00" and // be indistinguishable from a genuinely-zero price. $format_regular = $query_info['regular_price']['__args']['formatted'] ?? true; $raw_regular = $wc_product->get_regular_price(); if ( '' === $raw_regular ) { $product->regular_price = null; } else { $product->regular_price = $format_regular ? wc_price( (float) $raw_regular ) : $raw_regular; } $format_sale = $query_info['sale_price']['__args']['formatted'] ?? true; $raw_sale = $wc_product->get_sale_price(); if ( '' === $raw_sale ) { $product->sale_price = null; } else { $product->sale_price = $format_sale ? wc_price( (float) $raw_sale ) : $raw_sale; } $raw_stock_status = (string) $wc_product->get_stock_status(); $product->stock_status = self::map_stock_status( $raw_stock_status ); $product->raw_stock_status = $raw_stock_status; $product->stock_quantity = $wc_product->get_stock_quantity(); // Nested output type: dimensions. $product->dimensions = self::build_dimensions( $wc_product ); // Array of objects: images. $product->images = self::build_images( $wc_product ); // Array of objects: attributes. $product->attributes = self::build_attributes( $wc_product ); // Sub-collection connection: reviews. // Only populate if explicitly requested (optimization via $query_info). if ( null === $query_info || array_key_exists( 'reviews', $query_info ) ) { $product->reviews = self::build_reviews( $wc_product->get_id() ); } else { $product->reviews = self::empty_connection(); } $product->date_created = $wc_product->get_date_created()?->format( \DateTimeInterface::ATOM ); $product->date_modified = $wc_product->get_date_modified()?->format( \DateTimeInterface::ATOM ); // Ignored field — set to null; it won't appear in the schema. $product->internal_notes = null; } /** * Map WooCommerce stock status string to the int-backed StockStatus enum. * * @param string $wc_status The WC stock status string. * @return StockStatus */ private static function map_stock_status( string $wc_status ): StockStatus { return match ( $wc_status ) { 'instock' => StockStatus::InStock, 'outofstock' => StockStatus::OutOfStock, 'onbackorder' => StockStatus::OnBackorder, default => StockStatus::Other, }; } /** * Build product dimensions from a WC_Product. * * @param \WC_Product $wc_product The product. * @return ?ProductDimensions */ private static function build_dimensions( \WC_Product $wc_product ): ?ProductDimensions { $length = $wc_product->get_length(); $width = $wc_product->get_width(); $height = $wc_product->get_height(); $weight = $wc_product->get_weight(); if ( '' === $length && '' === $width && '' === $height && '' === $weight ) { return null; } $dims = new ProductDimensions(); $dims->length = '' !== $length ? (float) $length : null; $dims->width = '' !== $width ? (float) $width : null; $dims->height = '' !== $height ? (float) $height : null; $dims->weight = '' !== $weight ? (float) $weight : null; return $dims; } /** * Build product images from a WC_Product. * * @param \WC_Product $wc_product The product. * @return ProductImage[] */ private static function build_images( \WC_Product $wc_product ): array { $images = array(); $position = 0; // Include the featured image first. $featured_id = $wc_product->get_image_id(); if ( $featured_id ) { $image = self::build_image( (int) $featured_id, $position ); if ( null !== $image ) { $images[] = $image; ++$position; } } // Then gallery images. foreach ( $wc_product->get_gallery_image_ids() as $image_id ) { $image = self::build_image( (int) $image_id, $position ); if ( null !== $image ) { $images[] = $image; ++$position; } } return $images; } /** * Build product attributes from a WC_Product. * * For variations, attributes are simple key→value pairs (handled by selected_attributes), * so this returns an empty array. For other product types, it returns full attribute definitions. * * @param \WC_Product $wc_product The product. * @return ProductAttribute[] */ private static function build_attributes( \WC_Product $wc_product ): array { // Variations store attributes as simple string values, not WC_Product_Attribute objects. if ( 'variation' === $wc_product->get_type() ) { return array(); } $attributes = array(); foreach ( $wc_product->get_attributes() as $wc_attr ) { if ( ! $wc_attr instanceof \WC_Product_Attribute ) { continue; } $attr = new ProductAttribute(); $attr->slug = $wc_attr->get_name(); if ( $wc_attr->is_taxonomy() ) { $attr->name = wc_attribute_label( $wc_attr->get_name() ); $attr->options = array_map( function ( $term ) { return $term->name; }, $wc_attr->get_terms() ? $wc_attr->get_terms() : array() ); } else { $attr->name = $wc_attr->get_name(); $attr->options = $wc_attr->get_options(); } $attr->position = $wc_attr->get_position(); $attr->visible = $wc_attr->get_visible(); $attr->variation = $wc_attr->get_variation(); $attr->is_taxonomy = $wc_attr->is_taxonomy(); $attributes[] = $attr; }//end foreach return $attributes; } /** * Build a single ProductImage from an attachment ID. * * @param int $attachment_id The WordPress attachment ID. * @param int $position The display position. * @return ?ProductImage */ private static function build_image( int $attachment_id, int $position ): ?ProductImage { $url = wp_get_attachment_url( $attachment_id ); if ( ! $url ) { return null; } $image = new ProductImage(); $image->id = $attachment_id; $image->url = $url; $alt = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ); $image->alt = ! empty( $alt ) ? $alt : ''; $image->position = $position; return $image; } /** * Build a reviews connection for a product. * * @param int $product_id The product ID. * @return Connection */ private static function build_reviews( int $product_id ): Connection { $base_args = array( 'post_id' => $product_id, 'type' => 'review', 'status' => 'approve', ); // Separate count query: otherwise `total_count` would be the page // size (capped at 10) instead of the real review total. $total_count = (int) get_comments( $base_args + array( 'count' => true ) ); $comments = get_comments( $base_args + array( 'orderby' => 'comment_date', 'order' => 'DESC', 'number' => 10, ) ); $edges = array(); $nodes = array(); foreach ( $comments as $comment ) { $review = new ProductReview(); $review->id = (int) $comment->comment_ID; $review->product_id = $product_id; $review->reviewer = $comment->comment_author; $review->review = $comment->comment_content; $review->rating = (int) get_comment_meta( $comment->comment_ID, 'rating', true ); $review->date_created = $comment->comment_date_gmt ? ( new \DateTimeImmutable( $comment->comment_date_gmt, new \DateTimeZone( 'UTC' ) ) )->format( \DateTimeInterface::ATOM ) : null; $edge = new Edge(); $edge->cursor = base64_encode( (string) $review->id ); $edge->node = $review; $edges[] = $edge; $nodes[] = $review; } $page_info = new PageInfo(); $page_info->has_next_page = $total_count > count( $comments ); $page_info->has_previous_page = false; $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; } /** * Extract the per-node selection from a connection's query_info entry. * * Connections can be queried via `nodes { ... }` (the plain form) or * `edges { node { ... } }` (Relay form); clients may use either or both. * The per-node selection is what gets forwarded to the recursive * mapper call so each node is built with the right sub-fields. * * @param ?array $connection_info The query_info entry for the connection (e.g. `$query_info['variations']`). * @return ?array The merged per-node selection, or null when the caller didn't request any node fields. */ public static function connection_node_info( ?array $connection_info ): ?array { if ( null === $connection_info ) { return null; } $nodes = is_array( $connection_info['nodes'] ?? null ) ? $connection_info['nodes'] : array(); $edge = is_array( $connection_info['edges']['node'] ?? null ) ? $connection_info['edges']['node'] : array(); if ( empty( $nodes ) && empty( $edge ) ) { return null; } return array_merge( $edge, $nodes ); } /** * Return an empty connection (for skipped sub-collections). * * @return Connection */ private static function empty_connection(): Connection { $page_info = new PageInfo(); $page_info->has_next_page = false; $page_info->has_previous_page = false; $page_info->start_cursor = null; $page_info->end_cursor = null; $connection = new Connection(); $connection->edges = array(); $connection->nodes = array(); $connection->page_info = $page_info; $connection->total_count = 0; return $connection; } }
💾 Save Changes
❌ Cancel