$data Stored data. */ public static function fromArray( array $data ): self { $nextPage = isset( $data['next_page'] ) && is_numeric( $data['next_page'] ) ? max( 1, (int) $data['next_page'] ) : 1; $attempts = isset( $data['attempts'] ) && is_numeric( $data['attempts'] ) ? max( 0, (int) $data['attempts'] ) : 0; $nextAvailableAt = null; $rawNext = is_scalar( $data['next_available_at'] ?? null ) ? (string) $data['next_available_at'] : ''; if ( $rawNext !== '' ) { try { $nextAvailableAt = new DateTimeImmutable( $rawNext ); } catch ( Exception $unused ) { unset( $unused ); $nextAvailableAt = null; } } $lastError = is_scalar( $data['last_error'] ?? null ) ? (string) $data['last_error'] : null; return new self( $nextPage, $attempts, $nextAvailableAt, $lastError ); } /** * Serialize for option storage. * * @return array */ public function toArray(): array { return array( 'next_page' => $this->nextPage, 'attempts' => $this->attempts, 'next_available_at' => $this->nextAvailableAt?->format( DATE_ATOM ), 'last_error' => $this->lastError, ); } /** Next product-list cursor page. */ public function nextPage(): int { return $this->nextPage; } /** Consecutive failed attempts. */ public function attempts(): int { return $this->attempts; } /** Earliest retry instant. */ public function nextAvailableAt(): ?DateTimeImmutable { return $this->nextAvailableAt; } /** Last failure summary. */ public function lastError(): ?string { return $this->lastError; } /** * Whether a tick may run now. * * @param DateTimeImmutable $now Current instant. */ public function canRun( DateTimeImmutable $now ): bool { return $this->nextAvailableAt === null || $this->nextAvailableAt->getTimestamp() <= $now->getTimestamp(); } /** * Return state after a successful tick. * * @param int $nextPage Next cursor page. */ public function recordSuccess( int $nextPage ): self { return new self( max( 1, $nextPage ), 0, null, null ); } /** * Return state after a failed tick. * * @param int $attempts Consecutive failed attempts to store. * @param DateTimeImmutable $nextAvailableAt Earliest retry instant. * @param string $lastError Last failure summary. */ public function recordRetry( int $attempts, DateTimeImmutable $nextAvailableAt, string $lastError ): self { return new self( $this->nextPage, max( 0, $attempts ), $nextAvailableAt, substr( $lastError, 0, 300 ) ); } }