,string):array)|null $transport */ public function __construct( private CloudRequestSigner $signer = new CloudRequestSigner(), ?callable $transport = null ) { $this->transport = $transport ?? self::defaultTransport(); } /** * Normalize product search keywords. * * @param CloudSettings $settings Cloud settings. * @param array $product Product payload. * @param int $timestamp Timestamp. * @param string $nonce Nonce. * * @throws InvalidArgumentException When Cloud settings are incomplete. * @throws RuntimeException When Cloud response fails. */ public function normalize( CloudSettings $settings, array $product, int $timestamp, string $nonce ): string { if ( ! $settings->isConfigured() ) { throw new InvalidArgumentException( 'affilun Cloud settings are not configured.' ); } $path = '/ai/product-keywords'; $body = self::jsonEncode( $product ); $headers = $this->signer->sign( $settings->siteId(), $settings->apiKey(), 'POST', $path, $body, $timestamp, $nonce ); $headers['Content-Type'] = 'application/json'; $response = ( $this->transport )( $settings->endpoint() . $path, $headers, $body ); $status = isset( $response['status'] ) ? (int) $response['status'] : 0; $data = isset( $response['data'] ) && is_array( $response['data'] ) ? $response['data'] : null; $keywords = is_array( $data ) && is_scalar( $data['search_keywords'] ?? null ) ? trim( (string) $data['search_keywords'] ) : ''; if ( $status < 200 || $status >= 300 || $keywords === '' ) { throw new RuntimeException( 'affilun Cloud product keyword normalization failed.' ); } return self::truncate( $keywords, 512 ); } /** * JSON encode request payload. * * @param array $payload Payload. */ private static function jsonEncode( array $payload ): string { if ( function_exists( 'wp_json_encode' ) ) { $body = wp_json_encode( $payload ); } else { // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- Pure-unit fallback when WordPress is unavailable. $body = json_encode( $payload ); } if ( ! is_string( $body ) ) { throw new RuntimeException( 'Failed to encode affilun Cloud request.' ); } return $body; } /** * Default WordPress HTTP transport. * * @return callable * * @phpstan-return callable(string,array,string):array */ private static function defaultTransport(): callable { return static function ( string $url, array $headers, string $body ): array { $response = CloudHttpOptions::safePost( $url, array( 'timeout' => 5, 'headers' => $headers, 'body' => $body, ) ); if ( function_exists( 'is_wp_error' ) && is_wp_error( $response ) ) { return array( 'status' => 503, 'data' => null, ); } $raw = is_array( $response ) ? (string) wp_remote_retrieve_body( $response ) : ''; $decoded = json_decode( $raw, true ); return array( 'status' => is_array( $response ) ? (int) wp_remote_retrieve_response_code( $response ) : 0, 'data' => is_array( $decoded ) ? $decoded : null, ); }; } /** * Truncate text. * * @param string $value Text. * @param int $max Max length. */ private static function truncate( string $value, int $max ): string { if ( function_exists( 'mb_substr' ) ) { return mb_substr( $value, 0, $max, 'UTF-8' ); } return substr( $value, 0, $max ); } }