` (zero-based, contiguous keys); empty list * by default. Nested arrays / non-{@see ImageView} values are * rejected at construction time. * * Instances are immutable. Helper methods {@see self::hasPrimary()}, * {@see self::count()}, and {@see self::isEmpty()} are exposed so * templates and renderers can branch on the canonical "any image * available?" question without manually inspecting both fields. * * @package Affilun\Application\Render\ViewModel\Parts */ declare(strict_types=1); namespace Affilun\Application\Render\ViewModel\Parts; use InvalidArgumentException; /** * Immutable image-set used by §8.2 ProductCardViewModel and the * §10.4 image rendering rules. * * See the file-level docblock for the mapping to * `rendering-and-template-spec.md` §10.4 and the per-field * validation rules. */ final class ImageSet { /** * §10.4 primary image, or null when no image is available. */ private ?ImageView $primary; /** * Ordered list of gallery images. * * @var list */ private array $gallery; /** * Construct an image set with explicit values for every * field. * * Validation summary: * * - `primary` may be null when no image is available. * - `gallery` MUST be a list (zero-based, contiguous keys) * of {@see ImageView} instances; non-{@see ImageView} * entries and non-list shapes are rejected. * * @param ImageView|null $primary §10.4 primary image, or null. * @param list $gallery Gallery images. * * @throws InvalidArgumentException When `$gallery` is not a * list of {@see ImageView}. */ public function __construct( ?ImageView $primary, array $gallery ) { $this->primary = $primary; $this->gallery = self::normalizeGallery( $gallery ); } /** * §10.4 primary image, or null when no image is available. */ public function primary(): ?ImageView { return $this->primary; } /** * Ordered list of gallery images. * * @return list */ public function gallery(): array { return $this->gallery; } /** * Report whether a primary image is available. */ public function hasPrimary(): bool { return $this->primary !== null; } /** * Total number of images carried by this set, counting the * primary (when present) plus every gallery entry. */ public function count(): int { return ( $this->primary !== null ? 1 : 0 ) + count( $this->gallery ); } /** * Report whether this set carries no images at all — neither * a primary nor any gallery entries. */ public function isEmpty(): bool { return $this->primary === null && $this->gallery === array(); } /** * Report whether this set equals the supplied one. * * Equality is structural: the primary slot must agree on * presence and content, and the gallery lists must match * element-by-element with strict {@see ImageView::equals()} * comparisons. * * @param self $other Other set to compare against. */ public function equals( self $other ): bool { if ( ( $this->primary === null ) !== ( $other->primary === null ) ) { return false; } if ( $this->primary !== null && $other->primary !== null && ! $this->primary->equals( $other->primary ) ) { return false; } if ( count( $this->gallery ) !== count( $other->gallery ) ) { return false; } foreach ( $this->gallery as $index => $image ) { if ( ! $image->equals( $other->gallery[ $index ] ) ) { return false; } } return true; } /** * Validate and normalize the `gallery` field at construction. * * Accepts a zero-based contiguous list of {@see ImageView} * instances. Non-list shapes (associative arrays, sparse keys) * and non-{@see ImageView} entries are rejected. * * @param array $gallery Raw gallery list. * * @return list * * @throws InvalidArgumentException When `$gallery` violates * the rules above. */ private static function normalizeGallery( array $gallery ): array { $expected = 0; $result = array(); foreach ( $gallery as $key => $value ) { if ( $key !== $expected ) { $message = 'ImageSet gallery must be a zero-based contiguous list.'; // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } if ( ! $value instanceof ImageView ) { $message = sprintf( 'ImageSet gallery entries must be ImageView instances; got %s at index %d.', is_object( $value ) ? get_class( $value ) : gettype( $value ), $expected ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } $result[] = $value; ++$expected; } return $result; } }