AttributeRule. * @param callable $values callable( AttributeRule $rule ): string[] — the product's values for a WC_ATTRIBUTE or FIELD rule. * @param array $ask Attribute code => value chosen for this product (for ASK rules). * @param string[] $skip_codes Attribute codes handled elsewhere (they are neither sent nor reported here). * @param string $channel_label Marketplace name used in the messages. * @return array{attributes:array, errors:array, warnings:array} */ public static function resolve( array $definitions, array $rules, callable $values, array $ask = array(), array $skip_codes = array(), string $channel_label = '' ): array { $out = array( 'attributes' => array(), 'errors' => array(), 'warnings' => array(), ); foreach ( $definitions as $definition ) { $code = (string) ( $definition['code'] ?? '' ); if ( '' === $code || in_array( $code, $skip_codes, true ) ) { continue; } $label = (string) ( $definition['label'] ?? $code ); $validate = (array) ( $definition['validation'] ?? array() ); $required = ! empty( $validate['required'] ); $multiple = ! empty( $validate['allow_multiple_values'] ); $allowed = (array) ( $definition['values'] ?? array() ); $rule = $rules[ $code ] ?? null; if ( null === $rule || AttributeRule::SKIP === $rule->type ) { if ( $required ) { $out['errors'][] = array( 'code' => 'attribute_missing', 'text' => sprintf( 'Required {channel} field "%s" has no source. Set it in the attribute mapping.', $label ), 'args' => array( $label ), ); } continue; } $raw = self::raw_values( $rule, $values, $ask, $code ); if ( ! $raw ) { if ( $required ) { $out['errors'][] = array( 'code' => 'attribute_missing', 'text' => sprintf( 'Required {channel} field "%s" has no value for this product.', $label ), 'args' => array( $label ), ); } continue; } $mapped = array(); $unmapped = array(); foreach ( $raw as $item ) { $value = self::map_value( $item, $rule, $allowed, $validate ); if ( null === $value ) { $unmapped[] = $item; } else { $mapped[ $value ] = true; } } $mapped = array_map( 'strval', array_keys( $mapped ) ); if ( ! $mapped ) { $text = sprintf( 'The value "%1$s" of "%2$s" is not in the {channel} list; map it in the attribute mapping.', implode( ', ', $unmapped ), $label ); if ( $required ) { $out['errors'][] = array( 'code' => 'attribute_unmapped', 'text' => $text, 'args' => array( $label, implode( ', ', $unmapped ) ), ); } else { $out['warnings'][] = array( 'code' => 'attribute_unmapped', 'text' => $text, 'args' => array( $label, implode( ', ', $unmapped ) ), ); } continue; } if ( $unmapped ) { $out['warnings'][] = array( 'code' => 'attribute_unmapped', 'text' => sprintf( 'The value "%1$s" of "%2$s" is not in the {channel} list and was left out.', implode( ', ', $unmapped ), $label ), 'args' => array( $label, implode( ', ', $unmapped ) ), ); } if ( $multiple ) { $out['attributes'][] = array( 'code' => $code, 'values' => $mapped, ); } else { if ( count( $mapped ) > 1 ) { $out['warnings'][] = array( 'code' => 'attribute_many', 'text' => sprintf( '"%s" takes one value; the first was used.', $label ), ); } $out['attributes'][] = array( 'code' => $code, 'value' => $mapped[0], ); } } if ( '' !== $channel_label ) { foreach ( array( 'errors', 'warnings' ) as $kind ) { foreach ( $out[ $kind ] as $i => $entry ) { $out[ $kind ][ $i ]['text'] = Text::channel( $entry['text'], $channel_label ); } } } return $out; } /** * The product's raw values for a rule. * * @param AttributeRule $rule Rule. * @param callable $values Value callback. * @param array $ask Per-product choices. * @param string $code Attribute code. * @return string[] */ private static function raw_values( AttributeRule $rule, callable $values, array $ask, string $code ): array { switch ( $rule->type ) { case AttributeRule::FIXED: $list = array( $rule->fixed ); break; case AttributeRule::ASK: $list = isset( $ask[ $code ] ) ? (array) $ask[ $code ] : array(); break; default: $list = (array) call_user_func( $values, $rule ); } $out = array(); foreach ( $list as $item ) { $item = trim( (string) $item ); if ( '' !== $item ) { $out[] = $item; } } return array_values( array_unique( $out ) ); } /** * One shop value to a marketplace value, or null. * * @param string $item Shop value. * @param AttributeRule $rule Rule. * @param array $allowed Allowed values. * @param array $validate Validation info of the attribute. */ private static function map_value( string $item, AttributeRule $rule, array $allowed, array $validate ): ?string { if ( ! $allowed ) { // Free input (a number or text). if ( ! empty( $validate['numeric'] ) ) { $number = str_replace( ',', '.', $item ); if ( ! is_numeric( $number ) ) { return null; } $number = (float) $number; if ( ( isset( $validate['min'] ) && null !== $validate['min'] && $number < (float) $validate['min'] ) || ( isset( $validate['max'] ) && null !== $validate['max'] && $number > (float) $validate['max'] ) ) { return null; } return (string) $number; } return $item; } $key = Normalizer::key( $item ); if ( isset( $rule->values[ $key ] ) ) { $code = $rule->values[ $key ]; foreach ( $allowed as $value ) { if ( (string) ( $value['code'] ?? '' ) === $code ) { return $code; } } return null; // The saved choice is no longer offered by the marketplace. } foreach ( $allowed as $value ) { if ( (string) ( $value['code'] ?? '' ) === $item ) { return $item; } } return ValueSuggester::suggest( $item, $allowed ); } }