$candidates Keyword candidates. * @param list $microcopySuggestions Microcopy suggestions. * @param array $buttonLabelSuggestions Button labels by provider. * @param string $source Source token. */ public function __construct( private string $searchKeywords, private array $candidates, private array $microcopySuggestions, private array $buttonLabelSuggestions, private string $source ) {} /** * Build from array response. * * @param array $data Data. * @param string $source Source token. */ public static function fromArray( array $data, string $source ): self { $keywords = isset( $data['search_keywords'] ) && is_scalar( $data['search_keywords'] ) ? trim( (string) $data['search_keywords'] ) : ''; $candidates = self::stringList( $data['candidates'] ?? array(), 5, 120, $keywords ); $microcopy = self::stringList( $data['microcopy_suggestions'] ?? array(), 3, 80 ); $labels = self::buttonLabels( $data['button_label_suggestions'] ?? array() ); return new self( $keywords, $candidates, $microcopy, $labels, $source ); } /** * Primary keywords. */ public function searchKeywords(): string { return $this->searchKeywords; } /** * @return list */ public function candidates(): array { return $this->candidates; } /** * @return list */ public function microcopySuggestions(): array { return $this->microcopySuggestions; } /** * @return array */ public function buttonLabelSuggestions(): array { return $this->buttonLabelSuggestions; } /** * Source token. */ public function source(): string { return $this->source; } /** * @return array */ public function toResponseData(): array { return array( 'search_keywords' => $this->searchKeywords, 'candidates' => $this->candidates, 'microcopy_suggestions' => $this->microcopySuggestions, 'button_label_suggestions' => $this->buttonLabelSuggestions, 'source' => $this->source, ); } /** * Normalize a string list. * * @param mixed $raw Raw list. * @param int $limit Maximum items. * @param int $maxLength Maximum item length. * @param string|null $fallback Optional fallback item. * * @return list */ private static function stringList( mixed $raw, int $limit, int $maxLength, ?string $fallback = null ): array { $list = array(); if ( is_array( $raw ) ) { foreach ( $raw as $value ) { if ( is_array( $value ) ) { $value = $value['keyword'] ?? $value['value'] ?? $value['label'] ?? null; } if ( ! is_scalar( $value ) ) { continue; } $item = self::truncate( trim( (string) $value ), $maxLength ); if ( $item !== '' && ! in_array( $item, $list, true ) ) { $list[] = $item; } if ( count( $list ) >= $limit ) { break; } } } if ( $list === array() && $fallback !== null && trim( $fallback ) !== '' ) { $list[] = self::truncate( trim( $fallback ), $maxLength ); } return $list; } /** * Normalize button labels. * * @param mixed $raw Raw label map. * * @return array */ private static function buttonLabels( mixed $raw ): array { $allowed = array( 'amazon', 'rakuten', 'yahoo_shopping', 'mercari' ); $result = array(); if ( ! is_array( $raw ) ) { return $result; } foreach ( $allowed as $key ) { $value = $raw[ $key ] ?? null; if ( is_array( $value ) ) { $value = self::firstScalar( $value ); } if ( is_scalar( $value ) ) { $label = self::truncate( trim( (string) $value ), 40 ); if ( $label !== '' ) { $result[ $key ] = $label; } } } return $result; } /** * Return the first scalar value in a list-like array. * * @param array $values Candidate values. */ private static function firstScalar( array $values ): mixed { foreach ( $values as $value ) { if ( is_scalar( $value ) ) { return $value; } } return null; } /** * Truncate text. * * @param string $value Text. * @param int $maxLength Max length. */ private static function truncate( string $value, int $maxLength ): string { if ( function_exists( 'mb_strlen' ) && function_exists( 'mb_substr' ) ) { return mb_strlen( $value ) > $maxLength ? mb_substr( $value, 0, $maxLength ) : $value; } return strlen( $value ) > $maxLength ? substr( $value, 0, $maxLength ) : $value; } }