attribute_id; $out['attribute:' . $id] = ['id' => $id, 'taxonomy' => wc_attribute_taxonomy_name($attribute->attribute_name), 'label' => $attribute->attribute_label]; } return $out; } public static function fields(): array { return ['sku' => __('SKU', 'facetcore'), 'image' => __('Image', 'facetcore'), 'name' => __('Product', 'facetcore'), 'price' => __('Price', 'facetcore'), 'stock' => __('Availability', 'facetcore'), 'cart' => __('Add to cart', 'facetcore')]; } public static function defaults(): array { return ['version' => 2, 'source' => 'all', 'categories' => [], 'enable_search' => true, 'views' => ['grid'], 'default_view' => 'grid', 'allow_switch' => false, 'grid_sku' => false, 'grid_attributes' => [], 'filters' => [], 'columns' => [], 'per_page' => 25, 'show_counts' => true, 'hide_impossible' => true, 'custom_css' => '']; } public static function sanitize(array $raw): array { $out = self::defaults(); foreach (['filters'] as $kind) { $out[$kind] = []; foreach (is_array($raw[$kind] ?? null) ? $raw[$kind] : [] as $key) { if (!is_string($key)) { continue; } if (preg_match('/^attribute:[1-9][0-9]*$/D', $key)) { $out[$kind][] = $key; } } $out[$kind] = array_values(array_unique($out[$kind])); } // The existing engine supports ten simultaneous facets. $out['filters'] = array_slice($out['filters'], 0, 10); $out['per_page'] = max(1, min(100, is_scalar($raw['per_page'] ?? null) ? (int) $raw['per_page'] : 25)); foreach (['show_counts', 'hide_impossible'] as $key) { $out[$key] = in_array($raw[$key] ?? false, [true, 1, '1'], true); } $out['source'] = ($raw['source'] ?? 'all') === 'categories' ? 'categories' : 'all'; $out['categories'] = array_values(array_unique(array_filter(array_map(static fn ($id) => is_scalar($id) && preg_match('/^[1-9][0-9]*$/D', (string) $id) ? (int) $id : 0, is_array($raw['categories'] ?? null) ? $raw['categories'] : [])))); $out['categories'] = array_slice($out['categories'], 0, 100); $out['grid_sku'] = in_array($raw['grid_sku'] ?? false, [true, 1, '1'], true); $out['enable_search'] = in_array($raw['enable_search'] ?? true, [true, 1, '1'], true); // Opaque stored extension values survive Lite saves, but are never used without validation. foreach (self::extensionKeys() as $key) { if (array_key_exists($key, $raw)) { $out[$key] = $raw[$key]; } } return apply_filters('facetcore_sanitize_configuration', $out, $raw); } public static function extensionKeys(): array { return array_keys(self::extensionFeatures()); } private static function extensionFeatures(): array { return ['views' => 'table', 'default_view' => 'table', 'allow_switch' => 'view_switch', 'columns' => 'table', 'grid_attributes' => 'card_attributes', 'custom_css' => 'custom_css']; } public static function effective(array $stored): array { $config = self::sanitize($stored); foreach (self::extensionKeys() as $key) { $config[$key] = self::defaults()[$key]; } $config = apply_filters('facetcore_effective_configuration', $config, $stored); foreach (self::extensionFeatures() as $key => $feature) { if (!Features::has($feature)) { $config[$key] = self::defaults()[$key]; } } return $config; } public static function forSave(array $raw, array $old): array { foreach (self::extensionFeatures() as $key => $feature) { if (Features::has($feature)) { continue; } unset($raw[$key]); if (array_key_exists($key, $old)) { $raw[$key] = $old[$key]; } } return self::sanitize($raw); } public static function get(int $id, bool $publishedOnly = true): ?array { if ($id < 1) { return null; } $post = get_post($id); if (!$post || $post->post_type !== self::TYPE || ($publishedOnly && ($post->post_status !== 'publish' || $post->post_password !== ''))) { return null; } $raw = get_post_meta($id, self::META, true); $config = is_array($raw) ? self::sanitize($raw) : self::defaults(); return $publishedOnly ? self::effective($config) : $config; } public static function missing(array $config): array { $attrs = self::attributes(); return array_values(array_filter(array_unique(array_merge($config['filters'], is_array($config['columns']) ? $config['columns'] : [], is_array($config['grid_attributes']) ? $config['grid_attributes'] : [])), static fn ($key) => strpos($key, 'attribute:') === 0 && !isset($attrs[$key]))); } /** Category scope is a union, including descendants; missing selections never widen scope. */ public static function categoryScope(array $config): ?array { if ($config['source'] !== 'categories') { return null; } $ids = []; foreach ($config['categories'] as $id) { if (!term_exists($id, 'product_cat')) { continue; } $ids[] = $id; $children = get_term_children($id, 'product_cat'); if (!is_wp_error($children)) { $ids = array_merge($ids, $children); } } return array_values(array_unique(array_map('intval', $ids))); } public static function resolve(array $config): array { $attrs = self::attributes(); $index = new AttributeIndex(); $filters = []; foreach ($config['filters'] as $key) { if (!isset($attrs[$key])) { continue; } $facet = $index->facet($attrs[$key]['id']); if ($facet) { $facet['name'] = $attrs[$key]['label']; $facet['attribute_id'] = $attrs[$key]['id']; $facet['taxonomy'] = $attrs[$key]['taxonomy']; $filters[] = $facet; } } return apply_filters('facetcore_resolved_configuration', ['facets' => $filters, 'columns' => [], 'grid_attributes' => [], 'settings' => $config], $config, $attrs); } }