*/ private const VALID_STATES = array( self::FRESH, self::STALE, self::EXPIRED, self::BROKEN, self::ARCHIVED, ); /** * One of the {@see self::FRESH} / {@see self::STALE} / * {@see self::EXPIRED} / {@see self::BROKEN} / * {@see self::ARCHIVED} tokens. */ private string $value; /** * Private constructor — instances are obtained via the static * named constructors {@see self::fresh()} / {@see self::stale()} * / {@see self::expired()} / {@see self::broken()} / * {@see self::archived()} or via {@see self::fromString()} for * deserialization paths. * * @param string $value Pre-validated state token. */ private function __construct( string $value ) { $this->value = $value; } /** * Construct a `fresh` state instance. */ public static function fresh(): self { return new self( self::FRESH ); } /** * Construct a `stale` state instance. */ public static function stale(): self { return new self( self::STALE ); } /** * Construct an `expired` state instance. */ public static function expired(): self { return new self( self::EXPIRED ); } /** * Construct a `broken` state instance. */ public static function broken(): self { return new self( self::BROKEN ); } /** * Construct an `archived` state instance. */ public static function archived(): self { return new self( self::ARCHIVED ); } /** * Construct a state instance from a raw string token, * validating against the closed §7.1 set. * * The token is trimmed first so a carelessly-decoded payload * (trailing newline, padded space) is normalized to the same * canonical form a freshly-constructed state would carry. * * @param string $value Raw state token as supplied by the caller. * * @throws InvalidArgumentException When `$value` is not a * member of the §7.1 closed * set. */ public static function fromString( string $value ): self { $trimmed = trim( $value ); if ( ! in_array( $trimmed, self::VALID_STATES, true ) ) { $message = sprintf( 'SyncState must be one of [%s]; got "%s".', implode( ', ', self::VALID_STATES ), $value ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } return new self( $trimmed ); } /** * Closed set of accepted state tokens, in §7.1 declaration * order. Exposed for tests and for diagnostics surfaces that * need to enumerate the valid tokens without duplicating the * list. * * @return list */ public static function validStates(): array { return self::VALID_STATES; } /** * Canonical state token. */ public function value(): string { return $this->value; } /** * Value-object equality: two states are equal iff their tokens * match exactly. * * @param SyncState $other Other state to compare against. */ public function equals( SyncState $other ): bool { return $this->value === $other->value; } /** * Whether this is the `fresh` state. */ public function isFresh(): bool { return self::FRESH === $this->value; } /** * Whether this is the `stale` state. */ public function isStale(): bool { return self::STALE === $this->value; } /** * Whether this is the `expired` state. */ public function isExpired(): bool { return self::EXPIRED === $this->value; } /** * Whether this is the `broken` state. */ public function isBroken(): bool { return self::BROKEN === $this->value; } /** * Whether this is the `archived` state. */ public function isArchived(): bool { return self::ARCHIVED === $this->value; } /** * Whether the snapshot is displayable in the normal rendering * flow. * * Per §7.2, `fresh` and `stale` are unambiguously displayable. * `expired` is conditionally displayable as a constrained * fallback; this predicate is intentionally restrictive and * returns false for `expired` so call sites that need fallback * rendering must opt in explicitly via {@see self::isExpired()} * rather than receiving it implicitly here. `broken` and * `archived` are never displayable. */ public function isDisplayable(): bool { return self::FRESH === $this->value || self::STALE === $this->value; } /** * Whether the snapshot warrants a resync. * * Returns true for `stale`, `expired`, and `broken` — the three * states where re-fetching the upstream snapshot can plausibly * recover a usable result. Returns false for `fresh` (already * current) and `archived` (operationally retired; resyncing is * out of scope until reactivation). */ public function requiresResync(): bool { return self::STALE === $this->value || self::EXPIRED === $this->value || self::BROKEN === $this->value; } }