root = self::normalizeRequiredString( 'root', $root ); $this->image = self::normalizeRequiredString( 'image', $image ); $this->body = self::normalizeRequiredString( 'body', $body ); $this->title = self::normalizeRequiredString( 'title', $title ); $this->price = self::normalizeRequiredString( 'price', $price ); $this->buttons = self::normalizeRequiredString( 'buttons', $buttons ); } /** * Construct a CSS-classes view with the canonical §8.2 * defaults derived from a template key and a provider key. * * Produces the §8.2 example structure verbatim: * * - `root` = `affilun-card affilun-card-{template} affilun-provider-{provider}` * - `image` = `affilun-card__image` * - `body` = `affilun-card__body` * - `title` = `affilun-card__title` * - `price` = `affilun-card__price` * - `buttons` = `affilun-card__buttons` * * Both inputs MUST match `^[a-z0-9-]+$` so a template-key or * provider-key typo never reaches the rendered class * attribute. * * @param string $template §6.4 template key (lowercase ASCII / digits / hyphens). * @param string $provider Lowercase provider key (ASCII / digits / hyphens). * * @throws InvalidArgumentException When either input does not * match the `^[a-z0-9-]+$` * grammar. */ public static function defaultsForTemplate( string $template, string $provider ): self { $normalizedTemplate = self::normalizeToken( 'template', $template ); $normalizedProvider = self::normalizeToken( 'provider', $provider ); return new self( sprintf( 'affilun-card affilun-card-%s affilun-provider-%s', $normalizedTemplate, $normalizedProvider ), 'affilun-card__image', 'affilun-card__body', 'affilun-card__title', 'affilun-card__price', 'affilun-card__buttons' ); } /** * Pre-tokenized root-element BEM class string. */ public function root(): string { return $this->root; } /** * Pre-tokenized image-region BEM class string. */ public function image(): string { return $this->image; } /** * Pre-tokenized body-region BEM class string. */ public function body(): string { return $this->body; } /** * Pre-tokenized title-element BEM class string. */ public function title(): string { return $this->title; } /** * Pre-tokenized price-region BEM class string. */ public function price(): string { return $this->price; } /** * Pre-tokenized buttons-region BEM class string. */ public function buttons(): string { return $this->buttons; } /** * Report whether this view equals the supplied one. * * Equality is structural: every field must match with strict * string comparison. * * @param self $other Other view to compare against. */ public function equals( self $other ): bool { return $this->root === $other->root && $this->image === $other->image && $this->body === $other->body && $this->title === $other->title && $this->price === $other->price && $this->buttons === $other->buttons; } /** * 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( 'CssClasses %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 a `^[a-z0-9-]+$` token at construction. * * Used by {@see self::defaultsForTemplate()} for the template- * key and provider-key inputs so a typo (uppercase character, * embedded space, underscore) never reaches the rendered * class attribute. * * @param string $field Field name for the error message. * @param string $value Raw token as supplied by the caller. * * @throws InvalidArgumentException When `$value` does not * match `^[a-z0-9-]+$`. */ private static function normalizeToken( string $field, string $value ): string { if ( preg_match( '/\A[a-z0-9-]+\z/', $value ) !== 1 ) { $message = sprintf( 'CssClasses %s must match /^[a-z0-9-]+$/; got "%s".', $field, $value ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } return $value; } }