repository = $repository; } /** * Read categories for a product. * * @param int $productId Product id. * * @return list */ public function categoriesForProduct( int $productId ): array { return $this->termsForProduct( $productId, self::META_KEY_CATEGORIES ); } /** * Read tags for a product. * * @param int $productId Product id. * * @return list */ public function tagsForProduct( int $productId ): array { return $this->termsForProduct( $productId, self::META_KEY_TAGS ); } /** * Save categories for a product. * * @param int $productId Product id. * @param list $terms Terms. */ public function saveCategoriesForProduct( int $productId, array $terms ): void { $this->saveTermsForProduct( $productId, self::META_KEY_CATEGORIES, $terms ); } /** * Save tags for a product. * * @param int $productId Product id. * @param list $terms Terms. */ public function saveTagsForProduct( int $productId, array $terms ): void { $this->saveTermsForProduct( $productId, self::META_KEY_TAGS, $terms ); } /** * Whether a product has the requested category. * * @param int $productId Product id. * @param string $category Category to check. */ public function productHasCategory( int $productId, string $category ): bool { $needle = self::normalizeComparableTerm( $category ); if ( $needle === '' ) { return false; } foreach ( $this->categoriesForProduct( $productId ) as $term ) { if ( self::normalizeComparableTerm( $term ) === $needle ) { return true; } } return false; } /** * Convert a form value to a normalized term list. * * @param mixed $input Form/list input. * * @return list */ public static function termsFromInput( mixed $input ): array { $rawTerms = array(); if ( is_array( $input ) ) { foreach ( $input as $value ) { if ( is_scalar( $value ) ) { $rawTerms[] = (string) $value; } } } elseif ( is_scalar( $input ) ) { $rawTerms = (array) preg_split( '/[\r\n,]+/u', (string) $input ); } $terms = array(); $seen = array(); foreach ( $rawTerms as $rawTerm ) { $term = self::sanitizeTerm( (string) $rawTerm ); if ( $term === '' ) { continue; } $key = self::normalizeComparableTerm( $term ); if ( array_key_exists( $key, $seen ) ) { continue; } $seen[ $key ] = true; $terms[] = $term; } return $terms; } /** * Join terms for editor display. * * @param list $terms Terms. */ public static function termsToDisplayString( array $terms ): string { return implode( ', ', self::termsFromInput( $terms ) ); } /** * Read terms by meta key. * * @param int $productId Product id. * @param string $metaKey Meta key. * * @return list * * @throws InvalidArgumentException When product id is invalid. */ private function termsForProduct( int $productId, string $metaKey ): array { if ( $productId <= 0 ) { throw new InvalidArgumentException( 'Product id must be positive.' ); } $record = $this->repository->findByProductAndKey( $productId, $metaKey ); return $record === null ? array() : self::decodeTerms( $record->metaValue() ); } /** * Save terms by meta key. * * @param int $productId Product id. * @param string $metaKey Meta key. * @param list $terms Terms. * * @throws InvalidArgumentException When product id is invalid. */ private function saveTermsForProduct( int $productId, string $metaKey, array $terms ): void { if ( $productId <= 0 ) { throw new InvalidArgumentException( 'Product id must be positive.' ); } $now = new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) ); $existing = $this->repository->findByProductAndKey( $productId, $metaKey ); $value = self::encodeTerms( self::termsFromInput( $terms ) ); if ( $existing === null ) { $this->repository->save( ProductMetaRecord::createNew( $productId, $metaKey, $now, array( 'metaValue' => $value ) ) ); return; } $this->repository->save( $existing->withMetaValue( $value )->withUpdatedAt( $now ) ); } /** * Decode stored JSON terms. * * @param string|null $encoded Encoded terms. * * @return list */ private static function decodeTerms( ?string $encoded ): array { if ( $encoded === null || trim( $encoded ) === '' ) { return array(); } $decoded = json_decode( $encoded, true ); if ( is_array( $decoded ) ) { return self::termsFromInput( $decoded ); } return self::termsFromInput( $encoded ); } /** * Encode normalized terms. * * @param list $terms Terms. */ private static function encodeTerms( array $terms ): string { if ( function_exists( 'wp_json_encode' ) ) { $encoded = wp_json_encode( $terms ); } else { // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- Pure-unit fallback when WordPress is unavailable. $encoded = json_encode( $terms ); } return is_string( $encoded ) ? $encoded : '[]'; } /** * Sanitize one term. * * @param string $term Raw term. */ private static function sanitizeTerm( string $term ): string { if ( function_exists( 'sanitize_text_field' ) ) { $term = (string) sanitize_text_field( $term ); } elseif ( function_exists( 'wp_strip_all_tags' ) ) { $term = (string) wp_strip_all_tags( $term ); } else { // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags -- Pure-unit fallback when WordPress is unavailable. $term = strip_tags( $term ); } $term = (string) preg_replace( '/\s+/u', ' ', $term ); $term = trim( $term ); if ( $term === '' ) { return ''; } if ( function_exists( 'mb_strlen' ) && function_exists( 'mb_substr' ) ) { return mb_strlen( $term ) > self::MAX_TERM_LENGTH ? mb_substr( $term, 0, self::MAX_TERM_LENGTH ) : $term; } return strlen( $term ) > self::MAX_TERM_LENGTH ? substr( $term, 0, self::MAX_TERM_LENGTH ) : $term; } /** * Comparable normalized term key. * * @param string $term Raw term. */ private static function normalizeComparableTerm( string $term ): string { $term = self::sanitizeTerm( $term ); return function_exists( 'mb_strtolower' ) ? mb_strtolower( $term ) : strtolower( $term ); } }