*/ private const VALID_KINDS = array( self::KIND_PROVIDER, self::KIND_SALE, self::KIND_NEW, self::KIND_RANKING, self::KIND_CUSTOM, ); /** * One of the {@see self::KIND_*} tokens. */ private string $kind; /** * Pre-formatted badge label. */ private string $label; /** * Pre-tokenized BEM class string, or null. */ private ?string $className; /** * Construct a badge view with explicit values for every * field. * * Validation summary: * * - `kind` is trimmed and matched against * {@see self::VALID_KINDS}; out-of-set tokens raise * {@see InvalidArgumentException}. * - `label` is trimmed and rejected when empty after * trimming. * - `className`, when non-null, is trimmed and rejected * when the trim leaves an empty string — "present but * empty" is treated as a programming error. * * @param string $kind One of {@see self::VALID_KINDS}. * @param string $label Pre-formatted badge label. * @param string|null $className Pre-tokenized BEM class string, or null. * * @throws InvalidArgumentException When any field violates the * per-field rules summarized * above. */ public function __construct( string $kind, string $label, ?string $className ) { $this->kind = self::normalizeKind( $kind ); $this->label = self::normalizeRequiredString( 'label', $label ); $this->className = self::normalizeOptionalString( 'className', $className ); } /** * Closed set of accepted badge-kind tokens, in declaration * order. * * @return list */ public static function validKinds(): array { return self::VALID_KINDS; } /** * Badge-kind classification token. */ public function kind(): string { return $this->kind; } /** * Pre-formatted badge label. */ public function label(): string { return $this->label; } /** * Pre-tokenized BEM class string, or null. */ public function className(): ?string { return $this->className; } /** * Report whether this view equals the supplied one. * * Equality is structural: every field must match with strict * comparison. * * @param self $other Other view to compare against. */ public function equals( self $other ): bool { return $this->kind === $other->kind && $this->label === $other->label && $this->className === $other->className; } /** * Validate and normalize the `kind` field at construction. * * The token is trimmed first so a carelessly-decoded payload * is normalized to the same canonical form a freshly- * constructed token would carry. The trimmed value MUST * appear in {@see self::VALID_KINDS}. * * @param string $kind Raw token as supplied by the caller. * * @throws InvalidArgumentException When `$kind` is not a * member of the closed set. */ private static function normalizeKind( string $kind ): string { $trimmed = trim( $kind ); if ( ! in_array( $trimmed, self::VALID_KINDS, true ) ) { $message = sprintf( 'BadgeView kind must be one of [%s]; got "%s".', implode( ', ', self::VALID_KINDS ), $kind ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } return $trimmed; } /** * Validate and normalize a required string field at * construction. * * @param string $field Field name for the error message. * @param string $value Raw value as supplied by the caller. * * @throws InvalidArgumentException When `$value` is empty * after trimming. */ private static function normalizeRequiredString( string $field, string $value ): string { $trimmed = trim( $value ); if ( $trimmed === '' ) { $message = sprintf( 'BadgeView %s must be a non-empty string.', $field ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } return $trimmed; } /** * Validate and normalize a nullable string field that, when * non-null, must carry non-empty content. * * @param string $field Field name for the error message. * @param string|null $value Raw value, or null. * * @throws InvalidArgumentException When `$value` is the empty * string after trimming. */ private static function normalizeOptionalString( string $field, ?string $value ): ?string { if ( $value === null ) { return null; } $trimmed = trim( $value ); if ( $trimmed === '' ) { $message = sprintf( 'BadgeView %s must be null or a non-empty string.', $field ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } return $trimmed; } }