product( $product_id ); return $product && $product->is_type( 'variation' ); } /** * Whether the given ID refers to a variable (parent) product. * * @param int $product_id Product ID. */ public function is_variable( int $product_id ): bool { $product = $this->product( $product_id ); return $product && $product->is_type( 'variable' ); } /** * Return the purchasable-unit IDs for a product. * * Variable products expand to their variation IDs; everything else returns * its own ID. Variations passed directly return themselves. * * @param int $product_id Product ID. * @return int[] */ public function purchasable_units( int $product_id ): array { $product = $this->product( $product_id ); if ( ! $product ) { return array(); } if ( $product->is_type( 'variable' ) ) { return array_map( 'intval', $product->get_children() ); } return array( $product_id ); } /** * A human-readable label for a purchasable unit (parent name + attributes). * * @param int $unit_id Purchasable-unit ID. */ public function unit_label( int $unit_id ): string { $product = $this->product( $unit_id ); if ( ! $product ) { return '#' . $unit_id; } if ( $product->is_type( 'variation' ) ) { $name = $product->get_name(); // Includes attribute summary. return $name ? $name : ( '#' . $unit_id ); } return $product->get_name(); } /** * Current managed stock quantity for a unit (null when stock isn't managed). * * IMPORTANT: a variation whose stock is managed at the PARENT level reports * manage_stock === 'parent' and WooCommerce returns the PARENT's full * quantity from get_stock_quantity(). Summing that across variations would * multiply the parent stock by the number of variations, so such variations * are treated as having no own quantity (null) here. Parent-level totals are * read from the parent product instead (see manages_stock_at_parent()). * * @param int $unit_id Purchasable-unit ID. */ public function stock_quantity( int $unit_id ): ?float { $product = $this->product( $unit_id ); if ( ! $product ) { return null; } // Variation deferring stock to its parent has no independent quantity. if ( $product->is_type( 'variation' ) && true !== $product->get_manage_stock() ) { return null; } $qty = $product->get_stock_quantity(); return null === $qty ? null : (float) $qty; } /** * Whether a variable product manages stock at the parent level (one pooled * quantity) rather than per variation. * * @param int $product_id Product ID. */ public function manages_stock_at_parent( int $product_id ): bool { $product = $this->product( $product_id ); return $product && $product->is_type( 'variable' ) && true === $product->get_manage_stock(); } /** * The parent holding a variation's stock when it draws from a shared * (product-level) quantity, or null when the variation has its own stock or * nobody tracks it. * * @param int $unit_id Variation ID. */ public function shared_stock_parent( int $unit_id ): ?WC_Product { $product = $this->product( $unit_id ); if ( ! $product || ! $product->is_type( 'variation' ) || true === $product->get_manage_stock() ) { return null; } $parent = $this->product( (int) $product->get_parent_id() ); return $parent && true === $parent->get_manage_stock() ? $parent : null; } /** * Variations that draw from a parent's shared stock (those without their own * stock management). * * @param WC_Product $variable Variable product. * @return int[] */ public function shared_stock_variations( WC_Product $variable ): array { $ids = array(); foreach ( $variable->get_children() as $child_id ) { $child = $this->product( (int) $child_id ); if ( $child && true !== $child->get_manage_stock() ) { $ids[] = (int) $child_id; } } return $ids; } /** * The true total stock for any product, avoiding the parent-deferral * multiplication: parent-managed variable products return the parent's * quantity; per-variation ones sum each variation's own stock; simple * products return their own. * * @param int $product_id Product (parent or simple) ID. */ public function total_stock( int $product_id ): ?float { $product = $this->product( $product_id ); if ( ! $product ) { return null; } if ( $product->is_type( 'variable' ) ) { if ( true === $product->get_manage_stock() ) { $qty = $product->get_stock_quantity(); return null === $qty ? null : (float) $qty; } $total = 0.0; $found = false; foreach ( $product->get_children() as $child_id ) { $qty = $this->stock_quantity( (int) $child_id ); if ( null !== $qty ) { $total += $qty; $found = true; } } return $found ? $total : null; } return $this->stock_quantity( $product_id ); } /** * Thumbnail URL for a product or variation, or '' when it has no image. * * A variation without its own image uses its parent's (WooCommerce does this * in the "view" context). No placeholder: callers show no image instead. * * @param int $unit_id Product or variation ID. */ public function image_url( int $unit_id ): string { $product = $this->product( $unit_id ); $image_id = $product ? (int) $product->get_image_id() : 0; $url = $image_id > 0 ? wp_get_attachment_image_url( $image_id, 'thumbnail' ) : false; return $url ? (string) $url : ''; } /** * The unit's SKU. * * @param int $unit_id Purchasable-unit ID. */ public function sku( int $unit_id ): string { $product = $this->product( $unit_id ); return $product ? (string) $product->get_sku() : ''; } /** * The unit's current selling price as a float. * * @param int $unit_id Purchasable-unit ID. */ public function price( int $unit_id ): float { $product = $this->product( $unit_id ); return $product ? (float) $product->get_price() : 0.0; } /** * Days since the unit (or its parent) was created — for forecast confidence. * * @param int $unit_id Purchasable-unit ID. * @return int Days, or a large number when unknown. */ public function created_days( int $unit_id ): int { $product = $this->product( $unit_id ); if ( ! $product ) { return PHP_INT_MAX; } $created = $product->get_date_created(); if ( ! $created && $product->get_parent_id() ) { $parent = $this->product( (int) $product->get_parent_id() ); $created = $parent ? $parent->get_date_created() : null; } if ( ! $created ) { return PHP_INT_MAX; } $seconds = time() - $created->getTimestamp(); return (int) max( 0, floor( $seconds / DAY_IN_SECONDS ) ); } /** * A compact summary array describing a purchasable unit for the UI. * * @param int $unit_id Purchasable-unit ID. * @return array|null */ public function unit_summary( int $unit_id ): ?array { $product = $this->product( $unit_id ); if ( ! $product ) { return null; } return array( 'id' => $unit_id, 'parent_id' => (int) $product->get_parent_id(), 'label' => $this->unit_label( $unit_id ), 'sku' => (string) $product->get_sku(), 'type' => $product->get_type(), 'price' => (float) $product->get_price(), 'manage_stock' => true === $product->get_manage_stock(), 'stock' => $this->stock_quantity( $unit_id ), 'image' => $this->image_url( $unit_id ), ); } }