= 0` at construction * time so a programming error (signed-overflow, negative * subtraction) surfaces here rather than at the REST boundary. * * - `postIds` is the §6.2 `usage.post_ids` value: a sequential * `list` of WordPress post ids that embed the product, in * the order the controller resolved them. Each entry is * enforced as `>= 1` because a `0` post id is the WordPress * "no post" sentinel, never a valid foreign key. * * Phase-6 callers do not yet populate this VO — usage tracking is * wired in Phase 9 — but the mapper accepts a nullable hint so the * Phase 6 controller can pass {@see self::none()} (or simply null) * and the array shape stays §6.2-compliant from day one. * * @package Affilun\Presentation\Rest\Mapper */ declare(strict_types=1); namespace Affilun\Presentation\Rest\Mapper; use InvalidArgumentException; /** * Immutable usage-hints carrier consumed by the §6.2 * {@see ProductDetailMapper}. * * See the file-level docblock for the mapping to * `rest-api-spec.md` §6.2 and the per-field validation rules. */ final class UsageHints { /** * §6.2 `usage.embed_count` value. Non-negative. */ private int $embedCount; /** * §6.2 `usage.post_ids` value. Sequential list of post ids, * each `>= 1`. * * @var list */ private array $postIds; /** * Construct a usage-hints VO with explicit values for every * field. * * @param int $embedCount Embed-definition count, `>= 0`. * @param list $postIds Sequential list of post ids, * each `>= 1`. * * @throws InvalidArgumentException When `$embedCount` is * negative, when `$postIds` is * not a sequential list, or * when any entry is not an * integer `>= 1`. */ public function __construct( int $embedCount, array $postIds = array() ) { if ( $embedCount < 0 ) { $message = sprintf( 'UsageHints embedCount must be non-negative, got %d.', $embedCount ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } $expectedIndex = 0; foreach ( $postIds as $key => $value ) { if ( $key !== $expectedIndex ) { $message = 'UsageHints postIds must be a sequential list (0-indexed, contiguous).'; // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } if ( ! is_int( $value ) ) { $message = sprintf( 'UsageHints postIds entry at index %d must be an integer.', $expectedIndex ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } if ( $value < 1 ) { $message = sprintf( 'UsageHints postIds entry at index %d must be a positive integer, got %d.', $expectedIndex, $value ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } ++$expectedIndex; } $this->embedCount = $embedCount; /** @var list $postIds */ $this->postIds = $postIds; } /** * Construct an empty usage-hints VO with `embedCount = 0` and * an empty post-id list. Used by Phase-6 controllers that have * not yet wired usage tracking (Phase 9). */ public static function none(): self { return new self( 0, array() ); } /** * §6.2 `usage.embed_count` value. */ public function embedCount(): int { return $this->embedCount; } /** * §6.2 `usage.post_ids` value. * * @return list */ public function postIds(): array { return $this->postIds; } /** * Structural equality: two hints are equal when both the embed * count and the post-id list match exactly (including order). * * @param self $other Other hints to compare with. */ public function equals( self $other ): bool { return $this->embedCount === $other->embedCount && $this->postIds === $other->postIds; } }