next index), targets, options (label, variable_share, guest_share, * days, status), window [from, to], terms {parent, children}, fallback (type => IDs), made, * counts (when finished). * Clean-up: index (batch within the phase), cursor (last ID), skip (kept category IDs), prev * (batch => status before), deleted (type => n), kept (type => reason => n), kept_by_batch, * verify (the real-order walk, RealOrders: cursor, since, checked, real, over, lines). * * @phpstan-type RunState array{token: string, kind: string, batches: list, user_id: int, production: bool, phase: string, total: int, done: int, started: int, last_step: int, types: list, cursor?: array|int, targets?: array, options?: array, window?: array{0: int, 1: int}, terms?: array{parent: int, children: list}, fallback?: array>, made?: array, counts?: array, index?: int, skip?: list, prev?: array, deleted?: array, kept?: array>, kept_by_batch?: array>>, verify?: array} * * @since 1.0.0 */ final class Runner { /** * Run state option (not autoloaded). */ const OPTION = 'apwc_test_data_run'; /** * Step lock option (Support\Lock). */ const LOCK = 'apwc_test_data_lock'; /** * Seconds without a step before a run counts as paused. */ const STALE = 600; /** * Generate phases in order. */ const GENERATE = array( 'terms', 'products', 'coupons', 'customers', 'orders', 'reviews', 'finalize' ); /** * Items per step and type. */ const ITEMS = array( 'generate' => array( 'products' => 10, 'coupons' => 25, 'customers' => 25, 'orders' => 50, 'reviews' => 200, ), 'cleanup' => array( 'reviews' => 100, 'orders' => 50, 'coupons' => 50, 'customers' => 25, 'products' => 25, 'terms' => 20, ), ); /** * Capabilities per type: [ generate, delete ]. * * @since 1.0.0 * @return array */ public static function capabilities(): array { return array( 'products' => array( 'generate' => array( 'edit_products', 'manage_product_terms' ), 'delete' => array( 'delete_products', 'manage_product_terms' ), ), 'customers' => array( 'generate' => array( 'create_users' ), 'delete' => array( is_multisite() ? 'remove_users' : 'delete_users' ), ), 'orders' => array( 'generate' => array( 'edit_shop_orders' ), 'delete' => array( 'delete_shop_orders' ), ), 'coupons' => array( 'generate' => array( 'edit_shop_coupons' ), 'delete' => array( 'delete_shop_coupons' ), ), 'reviews' => array( 'generate' => array( 'moderate_comments' ), 'delete' => array( 'moderate_comments' ), ), ); } /** * Types the current user can't generate or delete, with the missing capability. * * @since 1.0.0 * @param string $kind `generate` or `delete`. * @param string[] $types Types to check. * @return array Type => first missing capability. */ public static function missing( string $kind, array $types ): array { $out = array(); $caps = self::capabilities(); foreach ( $types as $type ) { foreach ( $caps[ $type ][ $kind ] ?? array() as $cap ) { if ( ! current_user_can( $cap ) ) { $out[ $type ] = $cap; break; } } } return $out; } /* ------------------------------------------------------------------ state */ /** * The active run, or null (a broken option counts as none). * * @since 1.0.0 * @return array|null * @phpstan-return RunState|null */ public static function state(): ?array { wp_cache_delete( self::OPTION, 'options' ); // Another request may have changed it. $state = get_option( self::OPTION ); if ( ! is_array( $state ) || ! isset( $state['token'], $state['kind'], $state['batches'], $state['phase'] ) || ! in_array( $state['kind'], array( 'generate', 'cleanup' ), true ) || ! is_array( $state['batches'] ) ) { return null; } $state['batches'] = array_values( array_filter( array_map( 'strval', $state['batches'] ), array( Marker::class, 'valid_batch' ) ) ); $state['types'] = array_values( array_map( 'strval', (array) ( $state['types'] ?? array() ) ) ); foreach ( array( 'user_id', 'total', 'done', 'started', 'last_step' ) as $key ) { $state[ $key ] = (int) ( $state[ $key ] ?? 0 ); } $state['token'] = (string) $state['token']; $state['phase'] = (string) $state['phase']; $state['production'] = ! empty( $state['production'] ); /** @var RunState $state */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort return $state; } /** * The run's (first) batch. * * @since 1.0.0 * @param array $state State. * @phpstan-param RunState $state * @return string */ public static function batch( array $state ): string { return $state['batches'][0] ?? ''; } /** * Whether a token is the active run's. * * @since 1.0.0 * @param string $token Token. * @return bool */ public static function is_current( string $token ): bool { $state = self::state(); return $state && '' !== $token && hash_equals( $state['token'], $token ); } /** * Whether a run has had no step for a while (the tab was closed). * * @since 1.0.0 * @param array $state State. * @phpstan-param RunState $state * @return bool */ public static function is_stale( array $state ): bool { return time() - $state['last_step'] > self::STALE; } /** * Save state, only while it's still the active run. * * @since 1.0.0 * @param array $state State. * @phpstan-param RunState $state * @return bool */ public static function save( array $state ): bool { if ( ! self::is_current( $state['token'] ) ) { return false; } update_option( self::OPTION, $state, false ); return true; } /** * Forget the active run. * * @since 1.0.0 * @return void */ public static function clear(): void { delete_option( self::OPTION ); } /** * Stop any active run and remove the lock (switched off, Data section). * * @since 1.0.0 * @return void */ public static function abort(): void { $state = self::state(); if ( $state ) { self::stop( $state ); } self::clear(); Lock::clear( self::LOCK ); } /** * Start generating. The caller checked access, limits and that no run exists. * * @since 1.0.0 * @param string $batch New batch ID. * @param array $targets Counts per type. * @param array $options label, variable_share, guest_share, days, status. * @param bool $production Whether this site counts as production. * @return array * @phpstan-return RunState */ public static function begin_generate( string $batch, array $targets, array $options, bool $production ): array { $now = time(); $fallback = array(); if ( ! $targets['products'] && ( $targets['orders'] || $targets['reviews'] || $targets['coupons'] ) ) { $fallback['products'] = Marker::pool( 'products', $batch ); } if ( ! $targets['customers'] && ( $targets['orders'] || $targets['reviews'] ) ) { $fallback['customers'] = Marker::pool( 'customers', $batch ); } if ( ! $targets['coupons'] && $targets['orders'] ) { $fallback['coupons'] = Marker::pool( 'coupons', $batch ); } $state = array( 'token' => wp_generate_password( 32, false ), 'kind' => 'generate', 'batches' => array( $batch ), 'user_id' => get_current_user_id(), 'production' => $production, 'phase' => $targets['products'] ? 'terms' : 'products', 'cursor' => array_fill_keys( array_keys( $targets ), 0 ), 'targets' => $targets, 'types' => array_keys( array_filter( $targets ) ), 'options' => $options, 'window' => array( $now - DAY_IN_SECONDS * (int) $options['days'], $now ), 'terms' => array( 'parent' => 0, 'children' => array(), ), 'fallback' => $fallback, 'made' => array_fill_keys( array( 'products', 'variations', 'customers', 'orders', 'refunds', 'coupons', 'reviews', 'terms', 'failed' ), 0 ), 'total' => array_sum( $targets ) + ( $targets['products'] ? 1 : 0 ) + 1, 'done' => 0, 'started' => $now, 'last_step' => $now, ); update_option( self::OPTION, $state, false ); Batches::put( $batch, array( 'label' => (string) $options['label'], 'created_gmt' => $now, 'user_id' => get_current_user_id(), 'env' => Environment::type(), 'status' => 'running', 'requested' => $targets, 'made' => array(), ) ); return $state; } /** * Start cleaning up batches. The caller checked access. * * Phases run across all batches before the next phase (every batch's reviews, then every * batch's orders …), so one batch's orders using another batch's customers or products never * keep those as "has other orders". * * @since 1.0.0 * @param string[] $batches Batch IDs. * @param array> $counts Marker::counts(). * @param bool $production Whether this site counts as production. * @return array * @phpstan-return RunState */ public static function begin_cleanup( array $batches, array $counts, bool $production ): array { $now = time(); $total = 0; $prev = array(); $types = array(); $batches = array_values( array_map( 'strval', $batches ) ); foreach ( $batches as $batch ) { $c = $counts[ $batch ] ?? array(); $total += array_sum( $c ) + 1; foreach ( self::types_of( $c ) as $type ) { $types[ $type ] = true; } $entry = Batches::get( $batch ); $prev[ $batch ] = $entry ? (string) $entry['status'] : ''; if ( $entry ) { Batches::update( $batch, array( 'status' => 'deleting' ) ); } } $state = array( 'token' => wp_generate_password( 32, false ), 'kind' => 'cleanup', 'batches' => $batches, 'user_id' => get_current_user_id(), 'production' => $production, 'index' => 0, 'phase' => 'reviews', 'cursor' => 0, 'skip' => array(), 'types' => array_keys( $types ), 'prev' => $prev, 'deleted' => array(), 'kept' => array(), 'kept_by_batch' => array(), 'total' => max( 1, $total ), 'done' => 0, 'started' => $now, 'last_step' => $now, ); update_option( self::OPTION, $state, false ); return $state; } /** * Capability types with objects in a count list. * * @since 1.0.0 * @param array $counts Marker counts of one batch. * @return list */ public static function types_of( array $counts ): array { $map = array( 'products' => 'products', 'variations' => 'products', 'terms' => 'products', 'customers' => 'customers', 'orders' => 'orders', 'refunds' => 'orders', 'coupons' => 'coupons', 'reviews' => 'reviews', ); $types = array(); foreach ( $counts as $type => $count ) { if ( (int) $count > 0 && isset( $map[ $type ] ) ) { $types[ $map[ $type ] ] = true; } } return array_keys( $types ); } /* ------------------------------------------------------------------ budget */ /** * Seconds a step may take (0 = one item). * * @since 1.0.0 * @return float */ public static function seconds(): float { /** * Filter how long one test data step may run, in seconds (0–20; 0 makes one item per step). * * @since 1.0.0 * @param float $seconds Seconds (default 4). */ $seconds = apply_filters( 'apwc_test_data_step_seconds', 4.0 ); return is_numeric( $seconds ) ? max( 0.0, min( 20.0, (float) $seconds ) ) : 4.0; } /** * Items per step and type. * * @since 1.0.0 * @param string $kind `generate` or `cleanup`. * @return array */ public static function items( string $kind ): array { $defaults = self::ITEMS[ $kind ] ?? array(); /** * Filter the most items of each type one test data step handles (1–500 each). * * @since 1.0.0 * @param array $items Type => items. * @param string $kind `generate` or `cleanup`. */ $filtered = apply_filters( 'apwc_test_data_step_items', $defaults, $kind ); $out = array(); foreach ( $defaults as $type => $default ) { $value = is_array( $filtered ) && isset( $filtered[ $type ] ) && is_numeric( $filtered[ $type ] ) ? (int) $filtered[ $type ] : $default; $out[ $type ] = max( 1, min( 500, $value ) ); } return $out; } /* ------------------------------------------------------------------ steps */ /** * Run one step. The caller holds the lock and the Guard; state is saved here. * * @since 1.0.0 * @param array $state State. * @phpstan-param RunState $state * @return array State after the step (`phase` is `done` when finished). * @phpstan-return RunState */ public static function step( array $state ): array { $budget = new Budget( self::seconds() ); Counters::buffer(); try { $state = 'generate' === $state['kind'] ? self::generate( $state, $budget ) : self::cleanup( $state, $budget ); } finally { Counters::flush(); } $state['last_step'] = time(); if ( 'done' !== $state['phase'] ) { self::save( $state ); } return $state; } /** * Generate within the budget. * * @param array $state State. * @phpstan-param RunState $state * @param Budget $budget Step budget. * @return array * @phpstan-return RunState */ private static function generate( array $state, Budget $budget ): array { $ctx = new Context( $state ); $items = self::items( 'generate' ); $batch = self::batch( $state ); $touched = array(); $made = $state['made'] ?? array(); $cursor = is_array( $state['cursor'] ?? null ) ? $state['cursor'] : array(); while ( 'done' !== $state['phase'] && $budget->has_time() ) { $phase = $state['phase']; if ( 'terms' === $phase ) { $terms = Terms::make( $ctx ); self::announce( $ctx, $batch ); $state['terms'] = array( 'parent' => $terms['parent'], 'children' => array_values( $terms['children'] ), ); $made['terms'] = ( $made['terms'] ?? 0 ) + $terms['made']; $ctx->terms = $terms['children']; $state['phase'] = 'products'; ++$state['done']; $budget->spend(); $state['made'] = $made; self::save( $state ); continue; } if ( 'finalize' === $phase ) { $state['made'] = $made; $state = self::finish_generate( $state ); $budget->spend(); break; } $target = (int) ( $state['targets'][ $phase ] ?? 0 ); $index = (int) ( $cursor[ $phase ] ?? 0 ); $count = 0; while ( $index < $target && $count < ( $items[ $phase ] ?? 1 ) && $budget->has_time() ) { try { $result = self::make( $phase, $index, $ctx ); } catch ( \Throwable $e ) { // An item that always throws must not stall the run: count it failed, move on. Log::failed( 'generate', $phase, $index, $e ); $result = array(); } self::announce( $ctx, $batch ); if ( ! empty( $result['id'] ) ) { $made[ $phase ] = ( $made[ $phase ] ?? 0 ) + 1; foreach ( array( 'variations', 'refunds' ) as $extra ) { $made[ $extra ] = ( $made[ $extra ] ?? 0 ) + (int) ( $result[ $extra ] ?? 0 ); } if ( ! empty( $result['product'] ) ) { $touched[ (int) $result['product'] ] = true; } } else { $made['failed'] = ( $made['failed'] ?? 0 ) + 1; } ++$index; ++$count; ++$state['done']; $budget->spend(); $cursor[ $phase ] = $index; $state['cursor'] = $cursor; $state['made'] = $made; // Saved after every item: a step that dies re-makes at most the item it was on. self::save( $state ); } if ( $index < $target ) { break; // Out of items or time for this step. } $next = array_search( $phase, self::GENERATE, true ); $state['phase'] = self::GENERATE[ false === $next ? count( self::GENERATE ) - 1 : $next + 1 ] ?? 'finalize'; $ctx->reset_pools(); } if ( $touched && class_exists( '\WC_Comments' ) ) { foreach ( array_keys( $touched ) as $product_id ) { \WC_Comments::clear_transients( $product_id ); } } $state['made'] = $made; Batches::update( $batch, array( 'made' => $made ) ); return $state; } /** * Fire `apwc_test_data_created` for what the last item made. Outside the item's error * handling on purpose: a listener that throws fails the step (state was saved before the * item), it isn't counted as a failed item. * * @param Context $ctx Context. * @param string $batch Batch ID. * @return void */ private static function announce( Context $ctx, string $batch ): void { foreach ( $ctx->take_created() as $made ) { /** * Fires after a test object was created. * * @since 1.0.0 * @param string $type `product`, `variation`, `customer`, `order`, `refund`, `coupon`, `review` or `term`. * @param int $id Object ID. * @param string $batch Batch ID. */ do_action( 'apwc_test_data_created', $made[0], $made[1], $batch ); } } /** * Make item $index of a phase. * * @param string $phase Phase. * @param int $index Index. * @param Context $ctx Context. * @return array `id` (0 when not made), `variations`, `refunds`, `product`. */ private static function make( string $phase, int $index, Context $ctx ): array { switch ( $phase ) { case 'products': return Products::make( $index, $ctx ); case 'coupons': return array( 'id' => Coupons::make( $index, $ctx ) ); case 'customers': return array( 'id' => Customers::make( $index, $ctx ) ); case 'orders': return Orders::make( $index, $ctx ); case 'reviews': return Reviews::make( $index, $ctx ); } return array( 'id' => 0 ); } /** * Finish a generate run: record counts from the markers, drop the run. * * @param array $state State. * @phpstan-param RunState $state * @return array * @phpstan-return RunState */ private static function finish_generate( array $state ): array { $batch = self::batch( $state ); $counts = Marker::counts()[ $batch ] ?? array_fill_keys( Marker::TYPES, 0 ); Batches::update( $batch, array( 'status' => 'complete', 'made' => array_merge( $state['made'] ?? array(), $counts ), 'finished_gmt' => time(), ) ); $state['phase'] = 'done'; $state['counts'] = $counts; ++$state['done']; if ( self::is_current( $state['token'] ) ) { self::clear(); } /** * Fires when a test data batch finished generating. * * @since 1.0.0 * @param string $batch Batch ID. * @param array $counts Objects per type (from the markers). * @param string $status `complete`. */ do_action( 'apwc_test_data_batch_finished', $batch, $counts, 'complete' ); return $state; } /** * Clean up within the budget. * * @param array $state State. * @phpstan-param RunState $state * @param Budget $budget Step budget. * @return array * @phpstan-return RunState */ private static function cleanup( array $state, Budget $budget ): array { $items = self::items( 'cleanup' ); $phases = array_keys( Cleaner::PHASES ); $batches = $state['batches']; $used = array(); // Items per phase in this step. while ( 'done' !== $state['phase'] && $budget->has_time() ) { $index = (int) ( $state['index'] ?? 0 ); $batch = $batches[ $index ] ?? ''; $phase = $state['phase']; if ( '' === $batch ) { $state['phase'] = 'done'; break; } if ( 'finalize' === $phase ) { $state = self::finish_batch( $state, $batch ); $budget->spend(); self::save( $state ); continue; } // Only phases whose capabilities were checked for this run (objects of another type // that turned up mid-run stay for the next clean-up). if ( ! in_array( Cleaner::PHASE_TYPES[ $phase ] ?? '', $state['types'], true ) ) { $state = self::next_cleanup( $state, $phases ); continue; } if ( 'verify' === $phase ) { // One walk of order item meta for every batch's products, in bounded ranges. $walk = RealOrders::walk( $state['verify'] ?? null, $batches, $budget ); $state['verify'] = $walk['verify']; if ( ! $walk['done'] ) { self::save( $state ); break; // Out of time: go on next step. } $state['index'] = count( $batches ) - 1; // Done for all batches at once. $state = self::next_cleanup( $state, $phases ); self::save( $state ); continue; } $room = ( $items[ $phase ] ?? 1 ) - ( $used[ $phase ] ?? 0 ); if ( $room <= 0 ) { break; // This phase had its share of the step. } $ids = self::cleanup_ids( $phase, $batch, $state, $room ); if ( ! $ids ) { $state = self::next_cleanup( $state, $phases ); continue; } $verify = null; if ( 'products' === $phase ) { // Catch up with order lines added since the walk (new orders, added items) and, with // HPOS, orders saved since it started; a chunk is only checked once that's done. $walk = RealOrders::walk( $state['verify'] ?? null, $batches, $budget, true ); $state['verify'] = $walk['verify']; if ( ! $walk['done'] ) { self::save( $state ); break; } $verify = $walk['verify']; if ( ! $walk['ok'] ) { // Too many orders saved to re-read: this chunk's products stay as unverified. $verify['over'] = array_values( array_unique( array_merge( $verify['over'], $ids ) ) ); } } elseif ( 'terms' === $phase && wp_defer_term_counting() ) { // Count the terms the deleted products left now, while they all still exist: flushed // after a category is deleted, WooCommerce before 10.2 warns on it (_wc_term_recount()). wp_defer_term_counting( false ); wp_defer_term_counting( true ); } Cleaner::prime( $phase, $ids, $verify ); foreach ( $ids as $id ) { if ( ! $budget->has_time() ) { break; } $result = Cleaner::handle( $phase, $id, $batch ); $key = $result['type']; if ( Cleaner::DELETED === $result['result'] ) { $state['deleted'][ $key ] = ( $state['deleted'][ $key ] ?? 0 ) + 1; } elseif ( Cleaner::KEPT === $result['result'] ) { $reason = $result['reason']; $state['kept'][ $key ][ $reason ] = ( $state['kept'][ $key ][ $reason ] ?? 0 ) + 1; $state['kept_by_batch'][ $batch ][ $key ][ $reason ] = ( $state['kept_by_batch'][ $batch ][ $key ][ $reason ] ?? 0 ) + 1; if ( 'terms' === $phase ) { $state['skip'][] = $id; // Categories are re-read in full each time. } } $state['cursor'] = max( (int) ( $state['cursor'] ?? 0 ), $id ); $state['done'] = min( $state['total'], $state['done'] + 1 ); $used[ $phase ] = ( $used[ $phase ] ?? 0 ) + 1; $budget->spend(); self::save( $state ); } Cleaner::prime( '', array() ); } return $state; } /** * Move a clean-up on: the next batch in this phase, else the next phase from the first batch. * * @param array $state State. * @phpstan-param RunState $state * @param string[] $phases Phases in order. * @return array * @phpstan-return RunState */ private static function next_cleanup( array $state, array $phases ): array { $state['cursor'] = 0; $state['skip'] = array(); $state['index'] = (int) ( $state['index'] ?? 0 ) + 1; if ( $state['index'] < count( $state['batches'] ) ) { return $state; } $state['index'] = 0; $next = array_search( $state['phase'], $phases, true ); $state['phase'] = $phases[ false === $next ? count( $phases ) : $next + 1 ] ?? 'finalize'; return $state; } /** * The next IDs of a clean-up phase: after the cursor, lowest first; categories children first. * * @param string $phase Phase. * @param string $batch Batch. * @param array $state State. * @phpstan-param RunState $state * @param int $limit Most IDs. * @return int[] */ private static function cleanup_ids( string $phase, string $batch, array $state, int $limit ): array { if ( 'terms' !== $phase ) { return Marker::ids( Cleaner::PHASES[ $phase ], $batch, (int) ( $state['cursor'] ?? 0 ), $limit ); } // Categories: a handful per batch; children before parents, kept ones skipped. $ids = array_values( array_diff( Marker::ids( 'terms', $batch, 0, 200 ), $state['skip'] ?? array() ) ); $parents = array(); foreach ( $ids as $id ) { $term = get_term( $id, 'product_cat' ); $parents[ $id ] = $term instanceof \WP_Term ? (int) $term->parent : 0; } usort( $ids, static function ( $a, $b ) use ( $parents ) { $order = ( $parents[ $a ] ? 0 : 1 ) <=> ( $parents[ $b ] ? 0 : 1 ); return 0 !== $order ? $order : $b <=> $a; } ); return array_slice( $ids, 0, $limit ); } /** * Finish one batch of a clean-up: remove it from the list when nothing is left, else mark it * partly deleted with the kept report; move on to the next batch. * * @param array $state State. * @phpstan-param RunState $state * @param string $batch Batch. * @return array * @phpstan-return RunState */ private static function finish_batch( array $state, string $batch ): array { $left = array_sum( Marker::counts()[ $batch ] ?? array() ); $kept = $state['kept_by_batch'][ $batch ] ?? array(); if ( 0 === $left ) { Batches::remove( $batch ); } elseif ( Batches::get( $batch ) ) { Batches::update( $batch, array( 'status' => 'partial', 'kept' => $kept, 'finished_gmt' => time(), ) ); } else { // Found only by its markers: list it now, so the kept report shows. Batches::put( $batch, array( 'label' => '', 'created_gmt' => 0, 'status' => 'partial', 'kept' => $kept, 'finished_gmt' => time(), ) ); } /** * Fires after a test data batch was cleaned up (objects kept by the safety rules stay). * * @since 1.0.0 * @param string $batch Batch ID. * @param array $deleted Objects deleted so far in this run, per type. * @param array $kept Objects of this batch kept, per type and reason. */ do_action( 'apwc_test_data_batch_deleted', $batch, $state['deleted'] ?? array(), $kept ); $state['index'] = (int) ( $state['index'] ?? 0 ) + 1; $state['done'] = min( $state['total'], $state['done'] + 1 ); if ( ! isset( $state['batches'][ $state['index'] ] ) ) { $state['phase'] = 'done'; if ( self::is_current( $state['token'] ) ) { self::clear(); } } return $state; } /** * Stop the active run: a generate batch stays as `stopped` (its objects are tracked), a * clean-up puts batches back to their previous status. * * @since 1.0.0 * @param array $state State. * @phpstan-param RunState $state * @return void */ public static function stop( array $state ): void { if ( 'generate' === $state['kind'] ) { Batches::update( self::batch( $state ), array( 'status' => 'stopped', 'made' => $state['made'] ?? array(), 'finished_gmt' => time(), ) ); } else { foreach ( $state['batches'] as $batch ) { $prev = $state['prev'][ $batch ] ?? ''; if ( Batches::get( $batch ) ) { Batches::update( $batch, array( 'status' => '' !== $prev && 'deleting' !== $prev ? $prev : 'complete' ) ); } } } self::clear(); } /** * Progress numbers for a response. * * @since 1.0.0 * @param array $state State. * @phpstan-param RunState $state * @return array{total: int, left: int} */ public static function progress( array $state ): array { $total = max( 1, $state['total'] ); $done = min( $total, max( 0, $state['done'] ) ); // The real-order walk counts too: ranges read, plus ranges estimated to go. $walk = is_array( $state['verify'] ?? null ) ? RealOrders::normalize( $state['verify'] ) : null; $ranges = $walk ? $walk['ranges'] : 0; $ahead = $walk ? $walk['left'] : 0; return array( 'total' => $total + $ranges + $ahead, 'left' => 'done' === $state['phase'] ? 0 : max( 1, $total - $done ) + $ahead, ); } /** * Translated label of a phase. * * @since 1.0.0 * @param string $kind `generate` or `cleanup`. * @param string $phase Phase. * @return string */ public static function phase_label( string $kind, string $phase ): string { $generate = array( 'terms' => __( 'Making categories', 'admin-powerups-for-woocommerce' ), 'products' => __( 'Making products', 'admin-powerups-for-woocommerce' ), 'coupons' => __( 'Making coupons', 'admin-powerups-for-woocommerce' ), 'customers' => __( 'Making customers', 'admin-powerups-for-woocommerce' ), 'orders' => __( 'Making orders', 'admin-powerups-for-woocommerce' ), 'reviews' => __( 'Making reviews', 'admin-powerups-for-woocommerce' ), 'finalize' => __( 'Finishing', 'admin-powerups-for-woocommerce' ), ); $cleanup = array( 'reviews' => __( 'Deleting reviews', 'admin-powerups-for-woocommerce' ), 'orders' => __( 'Deleting orders', 'admin-powerups-for-woocommerce' ), 'coupons' => __( 'Deleting coupons', 'admin-powerups-for-woocommerce' ), 'customers' => __( 'Deleting customers', 'admin-powerups-for-woocommerce' ), 'verify' => __( 'Checking orders for test products', 'admin-powerups-for-woocommerce' ), 'products' => __( 'Deleting products', 'admin-powerups-for-woocommerce' ), 'terms' => __( 'Deleting categories', 'admin-powerups-for-woocommerce' ), 'finalize' => __( 'Finishing', 'admin-powerups-for-woocommerce' ), ); $labels = 'generate' === $kind ? $generate : $cleanup; return $labels[ $phase ] ?? __( 'Done', 'admin-powerups-for-woocommerce' ); } }