VisualAttributeTermMeta.php
316 lines
| 1 | <?php |
| 2 | /** |
| 3 | * Visual attribute term meta utilities. |
| 4 | * |
| 5 | * @package WooCommerce\Classes |
| 6 | */ |
| 7 | |
| 8 | declare( strict_types = 1 ); |
| 9 | |
| 10 | namespace Automattic\WooCommerce\Internal\ProductAttributes; |
| 11 | |
| 12 | /** |
| 13 | * Utilities for wc-visual attribute term metadata. |
| 14 | * |
| 15 | * @internal |
| 16 | * |
| 17 | * @since 10.9.0 |
| 18 | */ |
| 19 | class VisualAttributeTermMeta { |
| 20 | |
| 21 | /** |
| 22 | * Color visual type. |
| 23 | */ |
| 24 | public const TYPE_COLOR = 'color'; |
| 25 | |
| 26 | /** |
| 27 | * Image visual type. |
| 28 | */ |
| 29 | public const TYPE_IMAGE = 'image'; |
| 30 | |
| 31 | /** |
| 32 | * Empty visual type. |
| 33 | */ |
| 34 | public const TYPE_NONE = 'none'; |
| 35 | |
| 36 | /** |
| 37 | * Get an empty visual term value. |
| 38 | * |
| 39 | * @return array{type: string, value: string} |
| 40 | * |
| 41 | * @since 10.9.0 |
| 42 | */ |
| 43 | public static function get_empty_visual(): array { |
| 44 | return array( |
| 45 | 'type' => self::TYPE_NONE, |
| 46 | 'value' => '', |
| 47 | ); |
| 48 | } |
| 49 | |
| 50 | /** |
| 51 | * Get the normalized visual value for a term. |
| 52 | * |
| 53 | * @param int $term_id Term ID. |
| 54 | * @param string $image_size Image size for image visual URLs. |
| 55 | * @return array{type: string, value: string} |
| 56 | * |
| 57 | * @since 10.9.0 |
| 58 | */ |
| 59 | public static function get_term_visual( int $term_id, string $image_size = 'thumbnail' ): array { |
| 60 | return self::build_term_visual( $term_id, $image_size ); |
| 61 | } |
| 62 | |
| 63 | /** |
| 64 | * Get normalized visual values for the given terms. |
| 65 | * |
| 66 | * @param array $term_ids Term IDs. |
| 67 | * @param string $image_size Image size for image visual URLs. |
| 68 | * @return array<int, array{type: string, value: string}> Map of term ID to visual values. |
| 69 | * |
| 70 | * @since 10.9.0 |
| 71 | */ |
| 72 | public static function get_term_visuals( array $term_ids, string $image_size = 'thumbnail' ): array { |
| 73 | $visuals = array(); |
| 74 | $term_ids = self::prime_term_visual_caches( $term_ids ); |
| 75 | |
| 76 | foreach ( $term_ids as $term_id ) { |
| 77 | $visuals[ $term_id ] = self::build_term_visual( $term_id, $image_size ); |
| 78 | } |
| 79 | |
| 80 | return $visuals; |
| 81 | } |
| 82 | |
| 83 | /** |
| 84 | * Prime caches needed to build visual values for terms. |
| 85 | * |
| 86 | * @param array $term_ids Term IDs. |
| 87 | * @return array<int> Normalized term IDs. |
| 88 | * |
| 89 | * @since 10.9.0 |
| 90 | */ |
| 91 | public static function prime_term_visual_caches( array $term_ids ): array { |
| 92 | $term_ids = array_values( array_unique( array_filter( array_map( 'absint', $term_ids ) ) ) ); |
| 93 | |
| 94 | if ( empty( $term_ids ) ) { |
| 95 | return array(); |
| 96 | } |
| 97 | |
| 98 | update_meta_cache( 'term', $term_ids ); |
| 99 | |
| 100 | $image_ids = array(); |
| 101 | foreach ( $term_ids as $term_id ) { |
| 102 | $image_id = absint( get_term_meta( $term_id, 'image', true ) ); |
| 103 | |
| 104 | if ( $image_id ) { |
| 105 | $image_ids[] = $image_id; |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | $image_ids = array_values( array_unique( $image_ids ) ); |
| 110 | if ( ! empty( $image_ids ) ) { |
| 111 | _prime_post_caches( $image_ids, false, true ); |
| 112 | } |
| 113 | |
| 114 | return $term_ids; |
| 115 | } |
| 116 | |
| 117 | /** |
| 118 | * Build a normalized visual value for a term. |
| 119 | * |
| 120 | * @param int $term_id Term ID. |
| 121 | * @param string $image_size Image size for image visual URLs. |
| 122 | * @return array{type: string, value: string} |
| 123 | */ |
| 124 | private static function build_term_visual( int $term_id, string $image_size ): array { |
| 125 | $image_id = absint( get_term_meta( $term_id, 'image', true ) ); |
| 126 | |
| 127 | if ( $image_id && wp_attachment_is_image( $image_id ) ) { |
| 128 | $image_url = wp_get_attachment_image_url( $image_id, $image_size ); |
| 129 | |
| 130 | if ( $image_url ) { |
| 131 | return array( |
| 132 | 'type' => self::TYPE_IMAGE, |
| 133 | 'value' => $image_url, |
| 134 | ); |
| 135 | } |
| 136 | } |
| 137 | |
| 138 | $color = sanitize_hex_color( get_term_meta( $term_id, 'color', true ) ); |
| 139 | |
| 140 | if ( $color ) { |
| 141 | return array( |
| 142 | 'type' => self::TYPE_COLOR, |
| 143 | 'value' => $color, |
| 144 | ); |
| 145 | } |
| 146 | |
| 147 | return self::get_empty_visual(); |
| 148 | } |
| 149 | |
| 150 | /** |
| 151 | * Check whether a taxonomy is a wc-visual product attribute taxonomy. |
| 152 | * |
| 153 | * @param string $taxonomy Taxonomy name. |
| 154 | * @return bool |
| 155 | * |
| 156 | * @since 10.9.0 |
| 157 | */ |
| 158 | public static function is_visual_attribute_taxonomy( string $taxonomy ): bool { |
| 159 | static $visual_attribute_taxonomies = array(); |
| 160 | static $cache_prefix = ''; |
| 161 | |
| 162 | $current_cache_prefix = \WC_Cache_Helper::get_cache_prefix( 'woocommerce-attributes' ); |
| 163 | if ( $cache_prefix !== $current_cache_prefix ) { |
| 164 | $cache_prefix = $current_cache_prefix; |
| 165 | $visual_attribute_taxonomies = array(); |
| 166 | |
| 167 | foreach ( wc_get_attribute_taxonomies() as $attribute ) { |
| 168 | if ( 'wc-visual' === $attribute->attribute_type ) { |
| 169 | $visual_attribute_taxonomies[ wc_attribute_taxonomy_name( $attribute->attribute_name ) ] = true; |
| 170 | } |
| 171 | } |
| 172 | } |
| 173 | |
| 174 | return isset( $visual_attribute_taxonomies[ $taxonomy ] ); |
| 175 | } |
| 176 | |
| 177 | /** |
| 178 | * Save visual attribute term meta from request data. |
| 179 | * |
| 180 | * @param int $term_id Term ID. |
| 181 | * @param string $taxonomy Taxonomy slug. |
| 182 | * @param array $request_data Request data. |
| 183 | * @return void |
| 184 | * |
| 185 | * @since 10.9.0 |
| 186 | */ |
| 187 | public static function save_term_visual_from_request( int $term_id, string $taxonomy, array $request_data ): void { |
| 188 | if ( ! self::is_visual_attribute_taxonomy( $taxonomy ) || ! self::has_visual_request_data( $request_data ) ) { |
| 189 | return; |
| 190 | } |
| 191 | |
| 192 | $visual_type = isset( $request_data['wc_visual_attribute_type'] ) ? self::sanitize_visual_type( $request_data['wc_visual_attribute_type'] ) : ''; |
| 193 | $color_value = isset( $request_data['term_color'] ) ? sanitize_hex_color( self::sanitize_request_string( $request_data['term_color'] ) ) : ''; |
| 194 | $image_id = isset( $request_data['term_image'] ) ? absint( self::sanitize_request_string( $request_data['term_image'] ) ) : 0; |
| 195 | |
| 196 | if ( '' === $visual_type ) { |
| 197 | $visual_type = $image_id ? self::TYPE_IMAGE : self::TYPE_COLOR; |
| 198 | } |
| 199 | |
| 200 | self::save_term_visual_by_type( $term_id, $visual_type, $color_value ? $color_value : '', $image_id ); |
| 201 | } |
| 202 | |
| 203 | /** |
| 204 | * Check whether request data contains visual fields. |
| 205 | * |
| 206 | * @param array $request_data Request data. |
| 207 | * @return bool |
| 208 | */ |
| 209 | private static function has_visual_request_data( array $request_data ): bool { |
| 210 | return isset( $request_data['wc_visual_attribute_type'] ) || isset( $request_data['term_color'] ) || isset( $request_data['term_image'] ); |
| 211 | } |
| 212 | |
| 213 | /** |
| 214 | * Sanitize a request value to a string. |
| 215 | * |
| 216 | * @param mixed $value Request value. |
| 217 | * @return string |
| 218 | */ |
| 219 | private static function sanitize_request_string( $value ): string { |
| 220 | $value = wp_unslash( $value ); |
| 221 | |
| 222 | return is_scalar( $value ) ? (string) $value : ''; |
| 223 | } |
| 224 | |
| 225 | /** |
| 226 | * Sanitize the visual type request value. |
| 227 | * |
| 228 | * @param mixed $type Visual type value. |
| 229 | * @return string |
| 230 | */ |
| 231 | private static function sanitize_visual_type( $type ): string { |
| 232 | $type = self::sanitize_request_string( $type ); |
| 233 | |
| 234 | return in_array( $type, array( self::TYPE_COLOR, self::TYPE_IMAGE ), true ) ? $type : ''; |
| 235 | } |
| 236 | |
| 237 | /** |
| 238 | * Save mutually exclusive visual attribute term meta. |
| 239 | * |
| 240 | * @param int $term_id Term ID. |
| 241 | * @param string $color Hex color value. |
| 242 | * @param int $image_id Attachment ID for the term image. |
| 243 | * @return void |
| 244 | * |
| 245 | * @since 10.9.0 |
| 246 | */ |
| 247 | public static function save_term_visual( int $term_id, string $color = '', int $image_id = 0 ): void { |
| 248 | if ( $image_id && wp_attachment_is_image( $image_id ) ) { |
| 249 | update_term_meta( $term_id, 'image', absint( $image_id ) ); |
| 250 | delete_term_meta( $term_id, 'color' ); |
| 251 | return; |
| 252 | } |
| 253 | |
| 254 | $sanitized_color = sanitize_hex_color( $color ); |
| 255 | if ( $sanitized_color ) { |
| 256 | update_term_meta( $term_id, 'color', $sanitized_color ); |
| 257 | delete_term_meta( $term_id, 'image' ); |
| 258 | return; |
| 259 | } |
| 260 | |
| 261 | delete_term_meta( $term_id, 'color' ); |
| 262 | delete_term_meta( $term_id, 'image' ); |
| 263 | } |
| 264 | |
| 265 | /** |
| 266 | * Save visual attribute term meta using the selected visual type. |
| 267 | * |
| 268 | * @param int $term_id Term ID. |
| 269 | * @param string $type Selected visual type. |
| 270 | * @param string $color Hex color value. |
| 271 | * @param int $image_id Attachment ID for the term image. |
| 272 | * @return void |
| 273 | * |
| 274 | * @since 10.9.0 |
| 275 | */ |
| 276 | public static function save_term_visual_by_type( int $term_id, string $type, string $color = '', int $image_id = 0 ): void { |
| 277 | if ( self::TYPE_IMAGE === $type ) { |
| 278 | self::save_term_visual( $term_id, '', $image_id ); |
| 279 | return; |
| 280 | } |
| 281 | |
| 282 | self::save_term_visual( $term_id, $color, 0 ); |
| 283 | } |
| 284 | |
| 285 | /** |
| 286 | * Build an inline swatch style from a normalized visual value. |
| 287 | * |
| 288 | * @param array{type?: string, value?: string} $visual Normalized visual value. |
| 289 | * @return string |
| 290 | * |
| 291 | * @since 10.9.0 |
| 292 | */ |
| 293 | public static function get_swatch_style( array $visual ): string { |
| 294 | $type = isset( $visual['type'] ) ? (string) $visual['type'] : self::TYPE_NONE; |
| 295 | $value = isset( $visual['value'] ) ? (string) $visual['value'] : ''; |
| 296 | |
| 297 | if ( self::TYPE_IMAGE === $type ) { |
| 298 | $image = esc_url_raw( $value ); |
| 299 | |
| 300 | if ( $image ) { |
| 301 | return sprintf( "background-image:url('%s')", str_replace( "'", '%27', $image ) ); |
| 302 | } |
| 303 | } |
| 304 | |
| 305 | if ( self::TYPE_COLOR === $type ) { |
| 306 | $color = sanitize_hex_color( $value ); |
| 307 | |
| 308 | if ( $color ) { |
| 309 | return sprintf( 'background-color:%s', $color ); |
| 310 | } |
| 311 | } |
| 312 | |
| 313 | return ''; |
| 314 | } |
| 315 | } |
| 316 |