*/ public $operation_params = array(); /** * Cohort spec (array form). * * @var array */ public $cohort_spec = array(); /** * Cohort hash. * * @var string */ public $cohort_hash = ''; /** * Fingerprinted fields. * * @var string[] */ public $fingerprint_fields = array(); /** * Fingerprint version. * * @var int */ public $fp_version = 1; /** * Summary: counts, money, warnings, confirmation_required. * * @var array */ public $summary = array(); /** * Cohort resolution offset cursor. * * @var int */ public $resolve_offset = 0; /** * Highest item id processed by apply. * * @var int */ public $cursor_item_id = 0; /** * Apply chunk size. * * @var int */ public $chunk_size = 25; /** * Apply lanes. * * @var int */ public $lanes = 1; /** * Creator user id. * * @var int */ public $created_by = 0; /** * Timestamps (GMT 'Y-m-d H:i:s' or null). * * @var string|null */ public $created_at = null; /** * Updated. * * @var string|null */ public $updated_at = null; /** * Previewed. * * @var string|null */ public $previewed_at = null; /** * Preview expiry. * * @var string|null */ public $expires_at = null; /** * Apply start. * * @var string|null */ public $started_at = null; /** * Finish. * * @var string|null */ public $finished_at = null; /** * Last error. * * @var string|null */ public $last_error = null; /** * Resolve-time cursor state for background resolution (stored inside summary['resolve_cursor']). * * @return array */ public function resolve_cursor() { return isset( $this->summary['resolve_cursor'] ) && is_array( $this->summary['resolve_cursor'] ) ? $this->summary['resolve_cursor'] : array(); } /** * Builds from a database row. * * @param array|object $row Row. * @return Run */ public static function from_row( $row ) { $row = (array) $row; $run = new self(); foreach ( array( 'id', 'resolve_offset', 'cursor_item_id', 'chunk_size', 'lanes', 'created_by', 'fp_version' ) as $int ) { if ( isset( $row[ $int ] ) ) { $run->$int = (int) $row[ $int ]; } } $run->parent_run_id = isset( $row['parent_run_id'] ) ? (int) $row['parent_run_id'] : null; foreach ( array( 'uuid', 'relation', 'status', 'operation_id', 'cohort_hash' ) as $str ) { if ( isset( $row[ $str ] ) ) { $run->$str = (string) $row[ $str ]; } } foreach ( array( 'created_at', 'updated_at', 'previewed_at', 'expires_at', 'started_at', 'finished_at', 'last_error' ) as $nullable ) { $run->$nullable = isset( $row[ $nullable ] ) ? $row[ $nullable ] : null; } $run->operation_params = Json::decode( isset( $row['operation_params'] ) ? $row['operation_params'] : null, array() ); $run->cohort_spec = Json::decode( isset( $row['cohort_spec'] ) ? $row['cohort_spec'] : null, array() ); $run->fingerprint_fields = Json::decode( isset( $row['fingerprint_fields'] ) ? $row['fingerprint_fields'] : null, array() ); $run->summary = Json::decode( isset( $row['summary'] ) ? $row['summary'] : null, array() ); return $run; } /** * Spec object. * * @return CohortSpec|null */ public function spec() { try { return CohortSpec::from_array( $this->cohort_spec ); } catch ( InvalidSpecException $e ) { return null; } } /** * Counts by item status (from summary). * * @return array */ public function counts() { $counts = isset( $this->summary['counts'] ) && is_array( $this->summary['counts'] ) ? $this->summary['counts'] : array(); foreach ( ItemStatus::all() as $status ) { $counts[ $status ] = isset( $counts[ $status ] ) ? (int) $counts[ $status ] : 0; } $counts['total'] = isset( $this->summary['total'] ) ? (int) $this->summary['total'] : array_sum( $counts ); return $counts; } /** * Whether the preview is still valid for applying. * * @return bool */ public function preview_is_current() { return RunStatus::PREVIEWED === $this->status && ( ! $this->expires_at || strtotime( $this->expires_at . ' UTC' ) > time() ); } /** * Whether apply may start. * * @return bool */ public function can_apply() { if ( ! $this->preview_is_current() ) { return false; } $counts = $this->counts(); return $counts[ ItemStatus::READY ] > 0; } /** * Whether undo may start. * * @return bool */ public function can_undo() { $counts = $this->counts(); return in_array( $this->status, array( RunStatus::APPLIED, RunStatus::APPLIED_WITH_ISSUES, RunStatus::PARTIALLY_UNDONE ), true ) && $counts[ ItemStatus::APPLIED ] > 0 && empty( $this->summary['items_purged_at'] ); } /** * Progress: processed/total for the current phase. * * @return array{percent: int, processed: int, total: int, phase: string} */ public function progress() { $counts = $this->counts(); $total = $counts['total']; if ( in_array( $this->status, RunStatus::previewing(), true ) ) { $processed = $total - $counts[ ItemStatus::PENDING ]; $phase = 'preview'; } else { $processed = $counts[ ItemStatus::APPLIED ] + $counts[ ItemStatus::STALE ] + $counts[ ItemStatus::FAILED ] + $counts[ ItemStatus::SKIPPED ] + $counts[ ItemStatus::REVERTED ]; $total = $counts[ ItemStatus::READY ] + $counts[ ItemStatus::CLAIMED ] + $processed; $phase = 'apply'; } return array( 'percent' => $total > 0 ? (int) floor( $processed * 100 / $total ) : 0, 'processed' => (int) $processed, 'total' => (int) $total, 'phase' => $phase, ); } /** * REST form. * * @return array */ public function to_array() { $spec = $this->spec(); $creator = $this->created_by ? get_userdata( $this->created_by ) : null; $summary = $this->summary; unset( $summary['resolve_cursor'] ); return array( 'id' => $this->id, 'uuid' => $this->uuid, 'relation' => $this->relation, 'parent_run_id' => $this->parent_run_id, 'status' => $this->status, 'operation' => $this->operation_id, 'params' => $this->operation_params, 'spec' => $this->cohort_spec, 'spec_summary' => $spec ? SpecSummary::describe( $spec ) : '', 'cohort_hash' => $this->cohort_hash, 'counts' => $this->counts(), 'summary' => $summary, 'confirmation_required' => isset( $summary['confirmation_required'] ) ? array_values( (array) $summary['confirmation_required'] ) : array(), 'progress' => $this->progress(), 'can_apply' => $this->can_apply(), 'can_undo' => $this->can_undo(), 'created_by' => array( 'id' => $this->created_by, 'name' => $creator ? $creator->display_name : '', ), 'created_at' => $this->created_at, 'previewed_at' => $this->previewed_at, 'expires_at' => $this->expires_at, 'started_at' => $this->started_at, 'finished_at' => $this->finished_at, 'error' => $this->last_error, ); } }