'1', 'show_new' => '1', 'show_favourites' => '1', 'show_top' => '0', 'new_mode' => 'new_product', 'limit_per_rail' => '10', 'slides_per_view' => '5', 'top_limit' => '0', 'featured_limit' => '0', 'new_limit' => '0', 'favourites_limit' => '0', 'top_slides_per_view' => '0', 'featured_slides_per_view' => '0', 'new_slides_per_view' => '0', 'favourites_slides_per_view' => '0', ), $atts, 'aieo_brand_rails'); $to_bool = function ($v) { return in_array(strtolower((string) $v), array('1', 'true', 'yes', 'on'), true); }; return render_block(array( 'blockName' => 'aieo/brand-rails', 'attrs' => array( 'showFeatured' => $to_bool($atts['show_featured']), 'showNew' => $to_bool($atts['show_new']), 'showFavourites' => $to_bool($atts['show_favourites']), 'showTop' => $to_bool($atts['show_top']), 'newMode' => in_array($atts['new_mode'], array('new_product', 'new_brand'), true) ? $atts['new_mode'] : 'new_product', 'limitPerRail' => (int) $atts['limit_per_rail'], 'slidesPerView' => (int) $atts['slides_per_view'], 'topLimit' => (int) $atts['top_limit'], 'featuredLimit' => (int) $atts['featured_limit'], 'newLimit' => (int) $atts['new_limit'], 'favouritesLimit' => (int) $atts['favourites_limit'], 'topSlidesPerView' => (int) $atts['top_slides_per_view'], 'featuredSlidesPerView' => (int) $atts['featured_slides_per_view'], 'newSlidesPerView' => (int) $atts['new_slides_per_view'], 'favouritesSlidesPerView'=> (int) $atts['favourites_slides_per_view'], ), 'innerBlocks' => array(), 'innerHTML' => '', 'innerContent' => array(), )); } public function shortcode_brand_slider($atts = array()) { $atts = shortcode_atts(array( 'mode' => 'new_product', 'limit' => '10', 'slides_per_view' => '5', 'title' => '', 'subtitle' => '', ), $atts, 'aieo_brand_slider'); $valid_modes = array('top', 'featured', 'new_product', 'new_brand', 'favourites'); $mode = in_array($atts['mode'], $valid_modes, true) ? $atts['mode'] : 'new_product'; $block_attrs = array( 'mode' => $mode, 'limit' => max(3, min(30, (int) $atts['limit'])), 'slidesPerView' => max(2, min(8, (int) $atts['slides_per_view'])), ); if ($atts['title'] !== '') { $block_attrs['title'] = (string) $atts['title']; } if ($atts['subtitle'] !== '') { $block_attrs['subtitle'] = (string) $atts['subtitle']; } return render_block(array( 'blockName' => 'aieo/brand-slider', 'attrs' => $block_attrs, 'innerBlocks' => array(), 'innerHTML' => '', 'innerContent' => array(), )); } public static function table() { global $wpdb; return $wpdb->prefix . self::TABLE_NAME; } public static function table_exists() { static $cached = null; if ($cached !== null) { return $cached; } if (function_exists('aieo_dmm_table_exists')) { return $cached = aieo_dmm_table_exists(self::table()); } global $wpdb; $found = (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s", self::table() )); return $cached = ($found > 0); } public function get_all_grouped() { $cache_key = self::CACHE_KEY_ALL . ':' . $this->cache_version(); $cached = wp_cache_get($cache_key, self::CACHE_GROUP); if (is_array($cached)) { return $cached; } $out = array(); foreach (range('A', 'Z') as $L) { $out[$L] = array('count' => 0, 'brands' => array()); } $out['#'] = array('count' => 0, 'brands' => array()); if (!self::table_exists()) { return $out; } global $wpdb; $table = self::table(); $rows = $wpdb->get_results( "SELECT brand_id, brand_name, brand_slug, letter, product_count, in_stock_count, total_sales, hero_image_id, logo_image_id, rating_avg, rating_count, promo, priority, brand_created_at, newest_product_at FROM {$table} WHERE product_count > 0 ORDER BY letter ASC, brand_name ASC", ARRAY_A ); if (!is_array($rows)) { return $out; } foreach ($rows as $r) { $L = isset($r['letter']) ? strtoupper((string) $r['letter']) : '#'; if (!isset($out[$L])) { $L = '#'; } $out[$L]['count']++; $out[$L]['brands'][] = $this->shape_row($r); } wp_cache_set($cache_key, $out, self::CACHE_GROUP, self::CACHE_TTL); return $out; } public function get_top($limit = 10) { if (!self::table_exists()) { return array(); } $limit = max(1, (int) $limit); global $wpdb; $table = self::table(); $rows = $wpdb->get_results($wpdb->prepare( "SELECT * FROM {$table} WHERE product_count > 0 ORDER BY total_sales DESC, product_count DESC LIMIT %d", $limit ), ARRAY_A); return $this->shape_rows($rows); } public function get_newest_brand($limit = 10) { if (!self::table_exists()) { return array(); } $limit = max(1, (int) $limit); global $wpdb; $table = self::table(); $rows = $wpdb->get_results($wpdb->prepare( "SELECT * FROM {$table} WHERE product_count > 0 AND brand_created_at IS NOT NULL ORDER BY brand_created_at DESC, product_count DESC LIMIT %d", $limit ), ARRAY_A); return $this->shape_rows($rows); } public function get_newest_product($limit = 10) { if (!self::table_exists()) { return array(); } $limit = max(1, (int) $limit); global $wpdb; $table = self::table(); $rows = $wpdb->get_results($wpdb->prepare( "SELECT * FROM {$table} WHERE product_count > 0 AND newest_product_at IS NOT NULL ORDER BY newest_product_at DESC LIMIT %d", $limit ), ARRAY_A); return $this->shape_rows($rows); } public function get_for_user($user_id, $limit = 5) { $user_id = (int) $user_id; $limit = max(1, (int) $limit); if ($user_id <= 0 || !self::table_exists()) { return array(); } if (!class_exists('AIEO_Customer_Intelligence_Query')) { return array(); } $top_ids = AIEO_Customer_Intelligence_Query::get_top_brands($user_id, $limit); if (empty($top_ids)) { return array(); } return $this->get_by_ids($top_ids); } public function get_by_ids(array $brand_ids) { $ids = array_values(array_filter(array_map('intval', $brand_ids), function ($v) { return $v > 0; })); if (empty($ids)) { return array(); } if (!self::table_exists()) { return array(); } global $wpdb; $table = self::table(); $placeholders = implode(',', array_fill(0, count($ids), '%d')); $sql = $wpdb->prepare( "SELECT * FROM {$table} WHERE brand_id IN ({$placeholders})", $ids ); $rows = $wpdb->get_results($sql, ARRAY_A); $by_id = array(); if (is_array($rows)) { foreach ($rows as $r) { $by_id[(int) $r['brand_id']] = $r; } } $out = array(); foreach ($ids as $id) { if (isset($by_id[$id]) && (int) $by_id[$id]['product_count'] > 0) { $out[] = $this->shape_row($by_id[$id]); } } return $out; } public function get_by_id($brand_id) { $brand_id = (int) $brand_id; if ($brand_id <= 0 || !self::table_exists()) { return null; } global $wpdb; $table = self::table(); $row = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$table} WHERE brand_id = %d", $brand_id ), ARRAY_A); return $row ? $this->shape_row($row) : null; } private function shape_row(array $r) { static $permalink_memo = array(); $brand_id = (int) $r['brand_id']; if (!isset($permalink_memo[$brand_id])) { $link = get_term_link($brand_id, isset($r['taxonomy']) ? (string) $r['taxonomy'] : 'product_brand'); $permalink_memo[$brand_id] = is_wp_error($link) ? '' : (string) $link; } $hero = $r['hero_image_id'] ? wp_get_attachment_image_src((int) $r['hero_image_id'], 'medium_large') : false; $logo = $r['logo_image_id'] ? wp_get_attachment_image_src((int) $r['logo_image_id'], 'medium') : false; return array( 'term_id' => $brand_id, 'name' => (string) $r['brand_name'], 'slug' => (string) $r['brand_slug'], 'permalink' => $permalink_memo[$brand_id], 'letter' => isset($r['letter']) ? (string) $r['letter'] : '#', 'product_count' => (int) $r['product_count'], 'in_stock_count' => (int) $r['in_stock_count'], 'total_sales' => (int) ($r['total_sales'] ?? 0), 'hero_image_id' => (int) $r['hero_image_id'], 'logo_image_id' => (int) $r['logo_image_id'], 'hero_url' => $hero ? (string) $hero[0] : '', 'hero_w' => $hero ? (int) $hero[1] : 0, 'hero_h' => $hero ? (int) $hero[2] : 0, 'logo_url' => $logo ? (string) $logo[0] : '', 'logo_w' => $logo ? (int) $logo[1] : 0, 'logo_h' => $logo ? (int) $logo[2] : 0, 'rating_avg' => isset($r['rating_avg']) && $r['rating_avg'] !== null ? (float) $r['rating_avg'] : null, 'rating_count' => (int) ($r['rating_count'] ?? 0), 'promo' => (int) ($r['promo'] ?? 0), 'priority' => (int) ($r['priority'] ?? 0), 'brand_created_at' => (string) ($r['brand_created_at'] ?? ''), 'newest_product_at' => (string) ($r['newest_product_at'] ?? ''), ); } private function shape_rows($rows) { $out = array(); if (!is_array($rows)) { return $out; } foreach ($rows as $r) { $out[] = $this->shape_row($r); } return $out; } public static function dims_attr(array $brand, $kind = 'logo') { $w = isset($brand[$kind . '_w']) ? (int) $brand[$kind . '_w'] : 0; $h = isset($brand[$kind . '_h']) ? (int) $brand[$kind . '_h'] : 0; return ($w > 0 && $h > 0) ? ' width="' . $w . '" height="' . $h . '"' : ''; } public function refresh($brand_ids = null) { global $wpdb; $started = microtime(true); if (!self::table_exists()) { return array('rows_written' => 0, 'duration' => 0.0, 'scope' => 'no_table'); } $table = self::table(); $vitals = $wpdb->prefix . 'aieo_core_product_vitals'; $variations = $wpdb->prefix . 'aieo_core_variation_vitals'; $popularity = $wpdb->prefix . 'aieo_core_category_brand_popularity'; $brand_extra = $wpdb->prefix . 'aieo_dmm_brand_extras'; $rating_stat = $wpdb->prefix . 'aieo_rating_stats'; $taxonomy = 'product_brand'; $scope_clause = ''; if (is_array($brand_ids) && count($brand_ids) > 0) { $ids = array_values(array_filter(array_map('intval', $brand_ids), function ($v) { return $v > 0; })); if (empty($ids)) { return array('rows_written' => 0, 'duration' => 0.0, 'scope' => 'noop'); } $list = implode(',', $ids); $scope_clause = " AND v.brand_id IN ({$list})"; } $stock_subq = " SELECT pv.product_id AS parent_id, CASE WHEN pv.variation_count > 0 THEN COALESCE(SUM(CASE WHEN var.stock > 0 THEN 1 ELSE 0 END), 0) ELSE CASE WHEN (pv.stock > 0 OR pv.stock_status = 'instock') THEN 1 ELSE 0 END END AS in_stock FROM {$vitals} pv LEFT JOIN {$variations} var ON var.parent_id = pv.product_id WHERE pv.parent_id = pv.product_id AND pv.brand_id IS NOT NULL AND pv.brand_id > 0 GROUP BY pv.product_id, pv.variation_count, pv.stock, pv.stock_status "; $sql = " INSERT INTO {$table} (brand_id, brand_name, brand_slug, taxonomy, letter, product_count, in_stock_count, total_sales, total_turnover, brand_created_at, newest_product_at, hero_image_id, logo_image_id, rating_avg, rating_count, promo, priority, last_computed) SELECT v.brand_id AS brand_id, COALESCE(MAX(v.brand), '') AS brand_name, COALESCE(MAX(v.brand_slug), '') AS brand_slug, '{$taxonomy}' AS taxonomy, CASE WHEN MAX(v.brand) REGEXP '^[A-Za-z]' THEN UPPER(LEFT(MAX(v.brand), 1)) ELSE '#' END AS letter, COUNT(DISTINCT v.product_id) AS product_count, COALESCE(SUM(stk.in_stock), 0) AS in_stock_count, COALESCE(MAX(pop.total_sales), 0) AS total_sales, COALESCE(MAX(pop.total_turnover), 0.00) AS total_turnover, MIN(v.product_creation_date) AS brand_created_at, MAX(v.product_creation_date) AS newest_product_at, COALESCE(MAX(be.hero_image_id), 0) AS hero_image_id, COALESCE(MAX(be.logo_image_id), 0) AS logo_image_id, CASE WHEN MAX(rs.count_total) > 0 THEN ROUND(MAX(rs.sum_total) / MAX(rs.count_total), 2) ELSE NULL END AS rating_avg, COALESCE(MAX(rs.count_total), 0) AS rating_count, COALESCE(MAX(be.promo), 0) AS promo, COALESCE(MAX(be.priority), 0) AS priority, UTC_TIMESTAMP() AS last_computed FROM {$vitals} v LEFT JOIN ({$stock_subq}) stk ON stk.parent_id = v.product_id LEFT JOIN {$popularity} pop ON pop.brand_id = v.brand_id AND pop.category_id IS NULL LEFT JOIN {$brand_extra} be ON be.term_id = v.brand_id AND be.taxonomy = '{$taxonomy}' LEFT JOIN {$rating_stat} rs ON rs.product_id = v.brand_id AND rs.object_type = 'brand' WHERE v.parent_id = v.product_id AND v.brand_id IS NOT NULL AND v.brand_id > 0 {$scope_clause} GROUP BY v.brand_id ON DUPLICATE KEY UPDATE brand_name = VALUES(brand_name), brand_slug = VALUES(brand_slug), taxonomy = VALUES(taxonomy), letter = VALUES(letter), product_count = VALUES(product_count), in_stock_count = VALUES(in_stock_count), total_sales = VALUES(total_sales), total_turnover = VALUES(total_turnover), brand_created_at = VALUES(brand_created_at), newest_product_at = VALUES(newest_product_at), hero_image_id = VALUES(hero_image_id), logo_image_id = VALUES(logo_image_id), rating_avg = VALUES(rating_avg), rating_count = VALUES(rating_count), promo = VALUES(promo), priority = VALUES(priority), last_computed = VALUES(last_computed) "; $rows = (int) $wpdb->query($sql); if ($scope_clause === '') { $wpdb->query( "DELETE bv FROM {$table} bv LEFT JOIN (SELECT DISTINCT brand_id FROM {$vitals} WHERE parent_id = product_id AND brand_id IS NOT NULL AND brand_id > 0) live ON live.brand_id = bv.brand_id WHERE live.brand_id IS NULL" ); } $this->bump_version(); $duration = round(microtime(true) - $started, 2); if (function_exists('aieo_debug_log')) { aieo_debug_log('AIEO_Core_Brand_Vitals::refresh ' . wp_json_encode(array( 'scope' => $scope_clause === '' ? 'full' : 'incremental', 'rows' => $rows, 'duration' => $duration, ))); } return array( 'rows_written' => $rows, 'duration' => $duration, 'scope' => $scope_clause === '' ? 'full' : 'incremental', ); } public function drain_dirty() { $ids = $this->get_dirty(); if (empty($ids)) { return 0; } $this->refresh($ids); delete_option(self::DIRTY_QUEUE_OPT); return count($ids); } public function mark_dirty(array $brand_ids) { $existing = $this->get_dirty(); $merged = array_unique(array_merge($existing, array_map('intval', $brand_ids))); $merged = array_values(array_filter($merged, function ($v) { return $v > 0; })); if (empty($merged)) { delete_option(self::DIRTY_QUEUE_OPT); return; } update_option(self::DIRTY_QUEUE_OPT, $merged, false); } public function get_dirty() { $raw = get_option(self::DIRTY_QUEUE_OPT, array()); return is_array($raw) ? array_values(array_filter(array_map('intval', $raw), function ($v) { return $v > 0; })) : array(); } public function cache_version() { $v = wp_cache_get(self::CACHE_KEY_VER, self::CACHE_GROUP); if ($v === false) { $v = (int) get_option('aieo_brand_vitals_cache_version', 1); wp_cache_set(self::CACHE_KEY_VER, $v, self::CACHE_GROUP, 0); } return (int) $v; } public function bump_version() { $v = $this->cache_version() + 1; update_option('aieo_brand_vitals_cache_version', $v, false); wp_cache_set(self::CACHE_KEY_VER, $v, self::CACHE_GROUP, 0); } public function on_product_save($product_id) { $product_id = (int) $product_id; if ($product_id <= 0) { return; } $terms = wp_get_post_terms($product_id, 'product_brand', array('fields' => 'ids')); if (is_wp_error($terms) || empty($terms)) { return; } $this->mark_dirty(array_map('intval', $terms)); } public function on_brand_term_change($term_id) { $term_id = (int) $term_id; if ($term_id <= 0) { return; } $this->mark_dirty(array($term_id)); } public function ajax_brand_slider_bundle() { check_ajax_referer('aieo_brand_slider', 'nonce'); $mode = isset($_POST['mode']) ? sanitize_key((string) wp_unslash($_POST['mode'])) : 'top'; $limit = isset($_POST['limit']) ? max(1, min(50, (int) wp_unslash($_POST['limit']))) : 10; switch ($mode) { case 'new_brand': $brands = $this->get_newest_brand($limit); break; case 'new_product': $brands = $this->get_newest_product($limit); break; case 'favourites': $brands = $this->get_for_user(get_current_user_id(), $limit); break; case 'manual': $raw = isset($_POST['brand_ids']) ? (array) wp_unslash($_POST['brand_ids']) : array(); $brands = $this->get_by_ids(array_map('intval', $raw)); if (count($brands) > $limit) { $brands = array_slice($brands, 0, $limit); } break; case 'top': default: $brands = $this->get_top($limit); } wp_send_json_success(array('mode' => $mode, 'brands' => $brands)); } } if (defined('WP_CLI') && WP_CLI && !class_exists('AIEO_CLI_Brand_Vitals')) { class AIEO_CLI_Brand_Vitals { public function refresh($args, $assoc_args) { $bv = AIEO_Core_Brand_Vitals::instance(); if (!empty($assoc_args['drain-dirty'])) { $n = $bv->drain_dirty(); \WP_CLI::success(sprintf('Drained dirty queue: %d brands.', $n)); return; } $brand_ids = null; if (!empty($assoc_args['brand_ids'])) { $brand_ids = array_map('intval', explode(',', (string) $assoc_args['brand_ids'])); } $result = $bv->refresh($brand_ids); \WP_CLI::success(sprintf( '%s refresh: %d rows, %.2fs', $result['scope'], $result['rows_written'], $result['duration'] )); } public function dirty($args, $assoc_args) { $bv = AIEO_Core_Brand_Vitals::instance(); $ids = $bv->get_dirty(); \WP_CLI::log('Dirty queue depth: ' . count($ids)); if (!empty($ids)) { \WP_CLI::log('Brand IDs: ' . implode(',', $ids)); } } } \WP_CLI::add_command('aieo brand-vitals', 'AIEO_CLI_Brand_Vitals'); }