*/ private const ATTR_ALIASES = array( 'show_price' => 'showPrice', 'show_brand' => 'showBrand', 'show_description' => 'showDescription', 'show_provider_badge' => 'showProviderBadge', 'show_disclaimer' => 'showDisclaimer', 'open_in_new_tab' => 'openInNewTab', 'tracking_enabled' => 'trackingEnabled', 'button_labels' => 'buttonLabels', 'custom_buttons' => 'customButtons', 'template_variant' => 'templateVariant', 'button_layout' => 'buttonLayout', 'button_shape' => 'buttonShape', 'button_tone' => 'buttonTone', 'microcopy_text' => 'microcopyText', 'microcopy_start_at' => 'microcopyStartAt', 'microcopy_end_at' => 'microcopyEndAt', 'ab_testing_enabled' => 'abTestingEnabled', 'ab_button_label_variant' => 'abButtonLabelVariant', 'ab_microcopy_variant' => 'abMicrocopyVariant', 'custom_css_class' => 'customCssClass', 'item_template' => 'itemTemplate', ); /** * Single-product block renderer used by the front-end callback. */ private ProductCardBlockRenderer $cardRenderer; /** * Construct the adapter with the canonical card block renderer. * * @param ProductCardBlockRenderer $cardRenderer Single-product block renderer. */ public function __construct( ProductCardBlockRenderer $cardRenderer ) { $this->cardRenderer = $cardRenderer; } /** * Render the preview through the canonical block-renderer path. * * @param string $kind Preview kind: `single` or `grid`. * @param list $productIds Product ids from the REST payload. * @param string $template Requested template key. * @param array $attrs Block attributes from the REST payload. * @param int $userId Current user id for cache-key parity. */ public function execute( string $kind, array $productIds, string $template, array $attrs, int $userId ): PreviewResult { $this->assertKind( $kind ); $this->assertTemplate( $template ); $this->assertUserId( $userId ); return $this->renderSingle( $productIds, $template, $attrs, $userId ); } /** * Render a single-product preview. * * @param list $productIds Product ids. * @param string $template Requested template key. * @param array $attrs Block attributes. * @param int $userId Current user id for cache-key parity. */ private function renderSingle( array $productIds, string $template, array $attrs, int $userId ): PreviewResult { if ( count( $productIds ) !== 1 ) { $message = sprintf( 'BlockPreviewRenderer: kind "single" requires exactly one productId; got %d.', count( $productIds ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } $productId = $this->parseProductId( $productIds[0] ); $blockAttributes = self::normalizeBlockAttrs( $attrs ); $blockAttributes['productId'] = $productId; $blockAttributes['template'] = $template; $html = $this->cardRenderer->render( $blockAttributes ); return new PreviewResult( array( 'type' => 'product-card', 'template' => $template, 'product_id' => $productId, 'block_attributes' => $blockAttributes, ), $html, self::composeCacheKey( $userId, 'single', $productIds, $template ), false ); } /** * @param array $attrs Raw attrs. * * @return array */ private static function normalizeBlockAttrs( array $attrs ): array { $normalized = $attrs; foreach ( self::ATTR_ALIASES as $legacyKey => $blockKey ) { if ( array_key_exists( $legacyKey, $attrs ) && ! array_key_exists( $blockKey, $normalized ) ) { $normalized[ $blockKey ] = $attrs[ $legacyKey ]; } } return $normalized; } /** * Validate the preview kind token. * * @param string $kind Preview kind. */ private function assertKind( string $kind ): void { if ( ! in_array( $kind, array( 'single' ), true ) ) { $message = sprintf( 'BlockPreviewRenderer: kind must be "single"; got "%s".', $kind ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } } /** * Validate the requested template key. * * @param string $template Template key. */ private function assertTemplate( string $template ): void { if ( trim( $template ) === '' ) { $message = 'BlockPreviewRenderer: template must be a non-empty string.'; // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } } /** * Validate the current user id used for preview cache-key parity. * * @param int $userId Current user id. */ private function assertUserId( int $userId ): void { if ( $userId < 1 ) { $message = sprintf( 'BlockPreviewRenderer: userId must be >= 1; got %d.', $userId ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } } /** * Parse a REST product-id value into a positive integer. * * @param string $raw Raw product id. */ private function parseProductId( string $raw ): int { $parsed = filter_var( trim( $raw ), FILTER_VALIDATE_INT, array( 'options' => array( 'min_range' => 1 ) ) ); if ( $parsed === false ) { $message = 'BlockPreviewRenderer: productId must be a positive integer string.'; // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } return $parsed; } /** * Compose the preview cache-key string used by the legacy use case. * * @param int $userId Current user id. * @param string $kind Preview kind. * @param list $productIds Validated product-id list. * @param string $template Template key. */ private static function composeCacheKey( int $userId, string $kind, array $productIds, string $template ): string { return 'affilun:render:preview:' . $userId . ':' . $kind . ':' . implode( ',', $productIds ) . ':' . $template . ':v1'; } }