, string): array)|null $transport */ public function __construct( private CloudRequestSigner $signer = new CloudRequestSigner(), ?callable $transport = null ) { $this->transport = $transport ?? self::defaultTransport(); } /** * Fetch product-shaped top affiliate candidate rows. * * @param CloudSettings $settings Cloud settings. * @param list $providers Enabled provider keys. * @param int $limit Max candidate sets. * @param int $timestamp Timestamp. * @param string $nonce Nonce. * * @return list> */ public function fetch( CloudSettings $settings, array $providers, int $limit, int $timestamp, string $nonce ): array { if ( ! $settings->isConfigured() ) { throw new InvalidArgumentException( 'affilun Cloud settings are not configured.' ); } $providers = self::normalizeProviders( $providers ); if ( $providers === array() ) { throw new InvalidArgumentException( 'At least one affiliate provider is required.' ); } $limit = max( 1, min( 100, $limit ) ); $path = '/top-affiliate-candidates?providers=' . rawurlencode( implode( ',', $providers ) ) . '&limit=' . rawurlencode( (string) $limit ); $headers = $this->signer->sign( $settings->siteId(), $settings->apiKey(), 'GET', $path, '', $timestamp, $nonce ); $response = ( $this->transport )( $settings->endpoint() . $path, $headers, '' ); $status = isset( $response['status'] ) ? (int) $response['status'] : 0; $data = isset( $response['data'] ) && is_array( $response['data'] ) ? $response['data'] : null; if ( $status < 200 || $status >= 300 || ! is_array( $data ) ) { throw new RuntimeException( 'affilun Cloud top affiliate candidates failed.' ); } return self::rowsFromPayload( $data ); } /** * @param list $providers Providers. * * @return list */ private static function normalizeProviders( array $providers ): array { $normalized = array(); foreach ( $providers as $provider ) { $value = preg_replace( '/[^a-z0-9_]/', '', strtolower( trim( $provider ) ) ) ?? ''; if ( $value !== '' && ! in_array( $value, $normalized, true ) ) { $normalized[] = $value; } } return $normalized; } /** * @param array $payload Payload. * * @return list> */ private static function rowsFromPayload( array $payload ): array { $candidates = isset( $payload['candidates'] ) && is_array( $payload['candidates'] ) ? $payload['candidates'] : array(); $rows = array(); foreach ( $candidates as $candidate ) { if ( ! is_array( $candidate ) ) { continue; } $row = self::rowFromCandidate( $candidate ); if ( $row !== null ) { $rows[] = $row; } } return $rows; } /** * @param array $candidate Candidate. * * @return array|null */ private static function rowFromCandidate( array $candidate ): ?array { $provider = self::stringField( $candidate, 'provider' ); $providerItemId = self::stringField( $candidate, 'provider_item_id' ); $title = self::stringField( $candidate, 'title' ); $detailUrl = self::stringField( $candidate, 'detail_url' ); if ( $provider === '' || $providerItemId === '' || $title === '' || $detailUrl === '' ) { return null; } $rawPayload = $candidate['raw_payload'] ?? $candidate; return array( 'provider' => $provider, 'provider_item_id' => $providerItemId, 'canonical_hash' => self::stringField( $candidate, 'canonical_hash' ) !== '' ? self::stringField( $candidate, 'canonical_hash' ) : hash( 'sha256', $provider . ':' . $providerItemId ), 'title' => $title, 'brand' => self::nullableStringField( $candidate, 'brand' ), 'description' => self::nullableStringField( $candidate, 'description' ), 'image_primary_url' => self::nullableStringField( $candidate, 'image_primary_url' ), 'image_gallery_json' => self::nullableJsonField( $candidate, 'image_gallery_json' ), 'price_amount' => self::nullableScalarField( $candidate, 'price_amount' ), 'price_currency' => self::nullableStringField( $candidate, 'price_currency' ), 'price_label' => self::nullableStringField( $candidate, 'price_label' ), 'availability_status' => self::nullableStringField( $candidate, 'availability_status' ), 'detail_url' => $detailUrl, 'affiliate_url' => self::nullableStringField( $candidate, 'affiliate_url' ), 'raw_payload_json' => self::jsonEncode( $rawPayload ), 'search_keywords' => self::nullableStringField( $candidate, 'search_keywords' ), 'search_keywords_source' => self::nullableStringField( $candidate, 'search_keywords_source' ), 'snapshot_version' => 1, 'sync_state' => 'fresh', 'lifecycle_state' => 'active', ); } /** * @param array $row Row. * @param string $key Field key. */ private static function stringField( array $row, string $key ): string { $value = $row[ $key ] ?? ''; return is_scalar( $value ) ? trim( (string) $value ) : ''; } /** * @param array $row Row. * @param string $key Field key. */ private static function nullableStringField( array $row, string $key ): ?string { $value = self::stringField( $row, $key ); return $value !== '' ? $value : null; } /** * @param array $row Row. * @param string $key Field key. */ private static function nullableScalarField( array $row, string $key ): string|int|float|null { $value = $row[ $key ] ?? null; return is_scalar( $value ) && trim( (string) $value ) !== '' ? $value : null; } /** * @param array $row Row. * @param string $key Field key. */ private static function nullableJsonField( array $row, string $key ): ?string { $value = $row[ $key ] ?? null; if ( is_array( $value ) ) { return self::jsonEncode( $value ); } if ( is_scalar( $value ) ) { $string = trim( (string) $value ); return $string !== '' ? $string : null; } return null; } /** * @param mixed $value Value. */ private static function jsonEncode( mixed $value ): string { if ( function_exists( 'wp_json_encode' ) ) { $json = wp_json_encode( $value ); } else { // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- Pure-unit fallback when WordPress is unavailable. $json = json_encode( $value ); } if ( ! is_string( $json ) ) { throw new RuntimeException( 'Failed to encode top affiliate candidate payload.' ); } return $json; } /** * @return callable(string,array,string):array */ private static function defaultTransport(): callable { return static function ( string $url, array $headers, string $body ): array { unset( $body ); $args = array( 'headers' => $headers, 'timeout' => 10, ); $response = wp_remote_get( $url, $args ); if ( is_wp_error( $response ) ) { return array( 'status' => 0, 'data' => null, 'error' => $response->get_error_message(), ); } $status = (int) wp_remote_retrieve_response_code( $response ); $responseBody = (string) wp_remote_retrieve_body( $response ); $data = json_decode( $responseBody, true ); return array( 'status' => $status, 'data' => is_array( $data ) ? $data : null, ); }; } }