batch, 'products', $index ); $plan = Plan::product( $random, $index, $ctx->variable_share, $ctx->decimals ); $words = $ctx->words; $noun = (string) $random->pick( $words['nouns'] ); $plan['name'] = trim( $random->pick( $words['adjectives'] ) . ' ' . $noun ); $plan['sku'] = 'TEST-' . strtoupper( Marker::public_token( $ctx->batch ) ) . '-' . ( $index + 1 ); $plan['category'] = $ctx->terms ? (int) $random->pick( $ctx->terms ) : 0; $plan['description'] = sprintf( /* translators: 1: product name, 2: material. */ __( 'Test product: %1$s made from %2$s. Generated by Admin Powerups test data.', 'admin-powerups-for-woocommerce' ), strtolower( $plan['name'] ), (string) $random->pick( $words['materials'] ) ); $sizes = $random->shuffle( $words['sizes'] ); $colors = $random->shuffle( $words['colours'] ); $plan['attributes'] = array( __( 'Size', 'admin-powerups-for-woocommerce' ) => array_slice( $sizes, 0, $random->int( 2, max( 2, count( $sizes ) ) ) ), __( 'Colour', 'admin-powerups-for-woocommerce' ) => array_slice( $colors, 0, $random->int( 1, min( 3, count( $colors ) ) ) ), ); /** * Filter a generated item's plan before it is created (names, prices, dates, choices). * Keys depend on the type; unknown keys are ignored and values are checked again. * * @since 1.0.0 * @param array $plan Plan. * @param string $type `product`, `customer`, `order`, `coupon` or `review`. * @param int $index Item index in the run. * @param string $batch Batch ID. */ $plan = (array) apply_filters( 'apwc_test_data_item', $plan, 'product', $index, $ctx->batch ); // Resumed step: the product is already there. $existing = (int) wc_get_product_id_by_sku( (string) $plan['sku'] ); if ( $existing && (string) get_post_meta( $existing, Marker::META, true ) === $ctx->batch ) { // SKUs are unique and carry the batch's secret token: a step that died before signing. if ( ! Marker::verified( 'post', $existing, $ctx->batch ) ) { Marker::sign( 'post', $existing, $ctx->batch ); } return array( 'id' => $existing, 'variations' => 0, ); } $variable = ! empty( $plan['variable'] ); $product = $variable ? new \WC_Product_Variable() : new \WC_Product_Simple(); $product->update_meta_data( Marker::META, $ctx->batch ); $product->set_name( mb_substr( sanitize_text_field( (string) $plan['name'] ), 0, 200 ) ); $product->set_status( $ctx->status ); $product->set_description( wp_kses_post( (string) $plan['description'] ) ); $product->set_short_description( '' ); $product->set_date_created( max( 1, $ctx->from - DAY_IN_SECONDS * max( 1, (int) $plan['age_days'] ) ) ); if ( ! empty( $plan['category'] ) ) { $product->set_category_ids( array( (int) $plan['category'] ) ); } if ( ! self::sku( $product, (string) $plan['sku'], $random ) ) { return array( 'id' => 0, 'variations' => 0, ); } if ( $variable ) { $attributes = array(); $position = 0; foreach ( (array) $plan['attributes'] as $name => $options ) { $options = array_values( array_filter( array_map( 'strval', (array) $options ) ) ); if ( ! $options ) { continue; } $attribute = new \WC_Product_Attribute(); $attribute->set_id( 0 ); $attribute->set_name( (string) $name ); $attribute->set_options( $options ); $attribute->set_position( $position++ ); $attribute->set_visible( true ); $attribute->set_variation( true ); $attributes[] = $attribute; } $product->set_attributes( $attributes ); } else { self::prices_and_stock( $product, $plan ); } self::shipping( $product, $plan ); try { $id = (int) $product->save(); } catch ( \Throwable $e ) { self::half_made( (int) $product->get_id(), $ctx->batch ); throw $e; } if ( ! $id ) { return array( 'id' => 0, 'variations' => 0, ); } Marker::sign( 'post', $id, $ctx->batch ); $made = 0; if ( $variable ) { $made = self::variations( $product, $plan, $ctx, $random ); \WC_Product_Variable::sync( $id ); wc_delete_product_transients( $id ); } $ctx->created( 'product', $id ); return array( 'id' => $id, 'variations' => $made, ); } /** * Variations: one per combination of the attributes, at most the plan's count (hard max 25). * * @param \WC_Product $product Parent product. * @param array $plan Plan. * @param Context $ctx Context. * @param Random $random Generator. * @return int Variations made. */ private static function variations( \WC_Product $product, array $plan, Context $ctx, Random $random ): int { $combos = array( array() ); foreach ( $product->get_attributes() as $key => $attribute ) { if ( ! $attribute instanceof \WC_Product_Attribute ) { continue; } $next = array(); foreach ( $combos as $combo ) { foreach ( $attribute->get_options() as $option ) { $next[] = array_merge( $combo, array( (string) $key => (string) $option ) ); } } $combos = $next; } $limit = max( 1, min( Plan::HARD_VARIATIONS, (int) ( $plan['variations'] ?? Plan::MAX_VARIATIONS ) ) ); $made = 0; foreach ( array_slice( $combos, 0, $limit ) as $k => $combo ) { if ( ! $combo ) { continue; } $variation = new \WC_Product_Variation(); $variation->update_meta_data( Marker::META, $ctx->batch ); $variation->set_parent_id( $product->get_id() ); $variation->set_attributes( $combo ); $variation->set_status( 'publish' ); $price = max( 1.0, (float) $plan['price'] + $random->int( -3, 8 ) ); self::prices_and_stock( $variation, array_merge( $plan, array( 'price' => round( $price, max( 0, min( 4, $ctx->decimals ) ) ), 'sale' => null, 'stock' => ! empty( $plan['manage'] ) ? $random->int( 0, 40 ) : null, ) ) ); if ( ! self::sku( $variation, $product->get_sku() . '-' . ( $k + 1 ), $random ) ) { continue; } try { $vid = (int) $variation->save(); } catch ( \Throwable $e ) { self::half_made( (int) $variation->get_id(), $ctx->batch ); $vid = 0; } if ( $vid ) { Marker::sign( 'post', $vid, $ctx->batch ); ++$made; $ctx->created( 'variation', $vid ); } } return $made; } /** * Prices and stock. * * @param \WC_Product $product Product. * @param array $plan Plan. * @return void */ private static function prices_and_stock( \WC_Product $product, array $plan ): void { $price = max( 0.0, (float) ( $plan['price'] ?? 10 ) ); $product->set_regular_price( wc_format_decimal( $price ) ); $sale = isset( $plan['sale'] ) && is_numeric( $plan['sale'] ) ? (float) $plan['sale'] : 0.0; if ( $sale > 0 && $sale < $price ) { $product->set_sale_price( wc_format_decimal( $sale ) ); } if ( ! empty( $plan['manage'] ) && null !== ( $plan['stock'] ?? null ) ) { $product->set_manage_stock( true ); $product->set_stock_quantity( max( 0, (int) $plan['stock'] ) ); } else { $product->set_manage_stock( false ); $product->set_stock_status( 'instock' ); } } /** * Weight, dimensions and virtual. * * @param \WC_Product $product Product. * @param array $plan Plan. * @return void */ private static function shipping( \WC_Product $product, array $plan ): void { if ( ! empty( $plan['virtual'] ) && ! $product->is_type( 'variable' ) ) { $product->set_virtual( true ); return; } if ( isset( $plan['weight'] ) && is_numeric( $plan['weight'] ) ) { $product->set_weight( wc_format_decimal( (float) $plan['weight'] ) ); } $dims = isset( $plan['dimensions'] ) && is_array( $plan['dimensions'] ) ? array_values( $plan['dimensions'] ) : array(); if ( 3 === count( $dims ) ) { $product->set_length( (string) (int) $dims[0] ); $product->set_width( (string) (int) $dims[1] ); $product->set_height( (string) (int) $dims[2] ); } } /** * A save that threw after the insert (a plugin on `woocommerce_new_product`, say) leaves the * product with our marker but no signature: sign it, so Clean up can remove it. * * @since 1.0.0 * @param int $id Product ID (0: never inserted). * @param string $batch Batch ID. * @return void */ public static function half_made( int $id, string $batch ): void { if ( $id && (string) get_post_meta( $id, Marker::META, true ) === $batch ) { Marker::sign( 'post', $id, $batch ); } } /** * Set a SKU; on a clash try once more with a random suffix. * * @param \WC_Product $product Product. * @param string $sku SKU. * @param Random $random Generator. * @return bool Whether a SKU was set. */ private static function sku( \WC_Product $product, string $sku, Random $random ): bool { try { $product->set_sku( $sku ); return true; } catch ( \WC_Data_Exception $e ) { try { $product->set_sku( $sku . '-' . $random->int( 100, 999 ) ); return true; } catch ( \WC_Data_Exception $e ) { return false; } } } }