array('label' => __('Detected groups', 'ai-eshop-optimizer'), 'icon' => 'search'), 'manual' => array('label' => __('Group by hand', 'ai-eshop-optimizer'), 'icon' => 'pencil-line'), 'attributes' => array('label' => __('Attributes', 'ai-eshop-optimizer'), 'icon' => 'tags'), 'log' => array('label' => __('Merge history', 'ai-eshop-optimizer'), 'icon' => 'scroll-text'), ); } public function get_tab_arg() { return 'ds_section'; } public function get_pro_promo() { return array( 'title' => __('Duplicate products', 'ai-eshop-optimizer'), 'description' => __('Finds the groups of products that are really one product sold as several, once per colour or size, shows the evidence behind each group, and merges the group into a single variable product without losing the sales history: past orders keep counting towards the merged product.', 'ai-eshop-optimizer'), 'bullets' => array( __('Reads the difference out of the product titles and names it, by colour, by size, or by both', 'ai-eshop-optimizer'), __('Refuses to merge any group whose difference it cannot name, rather than guessing', 'ai-eshop-optimizer'), __('Always previews first: the product that would be created, each variation with its SKU, price and stock, and how many past order lines are involved', 'ai-eshop-optimizer'), __('Originals are retired, never deleted, so every merge can be undone exactly as it was', 'ai-eshop-optimizer'), __('Groups you know about yourself can be assembled by hand, with a running selection that survives searching again', 'ai-eshop-optimizer'), ), ); } const ENGINE_MARKER = 'AIEO_Duplicate_Product_Merge'; private function engine_present() { return class_exists(self::ENGINE_MARKER); } private function require_pro() { if (!self::is_aieo_pro_user()) { wp_send_json_error(array('message' => __('This feature requires the Pro version.', 'ai-eshop-optimizer'))); } if (!$this->engine_present()) { wp_send_json_error(array('message' => __('Rooster One Pro is not installed. The merge engine ships with the Pro plugin — install and activate it and this screen comes alive.', 'ai-eshop-optimizer'))); } } private function render_engine_missing_notice() { echo '

' . esc_html__('Rooster One Pro is not installed.', 'ai-eshop-optimizer') . ' ' . esc_html__('The duplicate-products engine — group detection, the axis resolver and the reversible merge — ships with the Rooster One Pro plugin. Install and activate it and this screen comes alive without further setup.', 'ai-eshop-optimizer') . '

'; if (class_exists('AIEO_One_Installer')) { AIEO_One_Installer::render('duplicate_simples'); } } public function register_hooks() { add_action('wp_ajax_aieo_dmm_dupsimple_save', array($this, 'ajax_save_settings')); add_action('wp_ajax_aieo_dmm_dupsimple_preview', array($this, 'ajax_preview')); add_action('wp_ajax_aieo_dmm_dupsimple_merge', array($this, 'ajax_merge')); add_action('wp_ajax_aieo_dmm_dupsimple_undo', array($this, 'ajax_undo')); add_action('wp_ajax_aieo_dmm_dupsimple_purge', array($this, 'ajax_purge')); add_action('wp_ajax_aieo_dmm_dupsimple_search', array($this, 'ajax_search')); add_action('wp_ajax_aieo_dmm_dupsimple_bulk_preview', array($this, 'ajax_bulk_preview')); add_action('wp_ajax_aieo_dmm_dupsimple_bulk_merge', array($this, 'ajax_bulk_merge')); add_action('wp_ajax_aieo_dmm_dupsimple_attrs', array($this, 'ajax_save_attributes')); add_action('wp_ajax_aieo_dmm_dupsimple_propose', array($this, 'ajax_propose_attribute')); add_action('wp_ajax_aieo_dmm_dupsimple_create_attr', array($this, 'ajax_create_attribute')); add_action('wp_ajax_aieo_dmm_dupsimple_wcfeature', array($this, 'ajax_enable_wc_feature')); } public function ajax_propose_attribute() { AIEO_DMM::verify_ajax_request(); $this->require_pro(); $ids = $this->posted_ids(); $group = AIEO_Duplicate_Product_Groups::group_for($ids); if (!$group) { wp_send_json_error(array('message' => __('At least two products are needed.', 'ai-eshop-optimizer'))); } $proposal = AIEO_Attribute_Axis_Resolver::propose_terms($group['members']); wp_send_json_success(array( 'html' => $this->render_propose_html($group, $proposal), )); } private function render_propose_html(array $group, array $proposal) { $by_id = array(); foreach ($group['members'] as $m) { $by_id[$m['id']] = $m; } $ids_key = substr(md5(implode(',', wp_list_pluck($group['members'], 'id'))), 0, 10); list($open, $close) = AIEO_DMM_UI::card(array( 'anchor' => 'duplicate-simples-propose-' . $ids_key, 'title' => __('Nothing in your catalogue names this difference yet.', 'ai-eshop-optimizer'), 'level' => 3, 'class' => 'aieo-ds-propose-card', 'flush' => !empty($proposal['ok']), )); ob_start(); echo $open; echo '

' . esc_html__('Create an attribute for it. These products then become variations of it, and any future group using the same words is recognised on its own.', 'ai-eshop-optimizer') . '

'; if (!$proposal['ok']) { echo \Rooster\Admin\Ui::notice(esc_html(sprintf( __('These products cannot be told apart automatically: %s. Group them by hand instead.', 'ai-eshop-optimizer'), $proposal['reason'] )), 'destructive', array('inline' => true)); } else { $rows = array(); foreach ((array) $proposal['terms'] as $id => $term) { $title = isset($by_id[$id]) ? (string) $by_id[$id]['title'] : (string) $id; $edit = get_edit_post_link((int) $id, 'raw'); $rows[] = array( '_name' => $title, 'product' => array('html' => $edit ? '' . esc_html($title) . '' : esc_html($title)), 'value' => array('html' => \Rooster\Admin\Ui::input('', (string) $term, array( 'id' => 'aieo-ds-term-' . $ids_key . '-' . (int) $id, 'class' => 'aieo-ds-term', 'medium' => true, 'aria-label' => __('Its value', 'ai-eshop-optimizer') . ': ' . $title, 'attrs' => array('data-id' => (string) $id), ))), ); } \Rooster\Admin\Markup::out(\Rooster\Admin\Ui::table( array( array('key' => 'product', 'label' => __('Product', 'ai-eshop-optimizer'), 'type' => 'html'), array('key' => 'value', 'label' => __('Its value', 'ai-eshop-optimizer'), 'type' => 'html', 'width' => 'lg'), ), $rows, array('caption' => __('Its value', 'ai-eshop-optimizer')) )); $kinds = array(); foreach (AIEO_Attribute_Axis_Resolver::kinds() as $kind_key => $kind) { $kinds[$kind_key] = (string) $kind['label']; } echo '
' . AIEO_Kit::row(__('Attribute name', 'ai-eshop-optimizer'), \Rooster\Admin\Ui::input('', '', array('id' => 'aieo-ds-attr-name-' . $ids_key, 'class' => 'aieo-ds-attr-name', 'medium' => true, 'placeholder' => __('e.g. Shade', 'ai-eshop-optimizer'))), '', array('id' => 'aieo-ds-attr-name-' . $ids_key)) . AIEO_Kit::row(__('It describes', 'ai-eshop-optimizer'), \Rooster\Admin\Ui::select('', $kinds, '', array('id' => 'aieo-ds-attr-kind-' . $ids_key, 'attrs' => array('data-aieo-ds-attr-kind' => ''))), '', array('id' => 'aieo-ds-attr-kind-' . $ids_key)) . '
'; echo '
' . AIEO_Kit::button(__('Create attribute', 'ai-eshop-optimizer'), array('variant' => 'primary', 'class' => 'aieo-ds-create-attr', 'attrs' => array('data-ids' => implode(',', wp_list_pluck($group['members'], 'id'))))) . '' . '
'; } echo $close; return (string) ob_get_clean(); } public function ajax_create_attribute() { AIEO_DMM::verify_ajax_request(); $this->require_pro(); $label = isset($_POST['label']) ? sanitize_text_field(wp_unslash($_POST['label'])) : ''; $kind = isset($_POST['kind']) ? sanitize_key(wp_unslash($_POST['kind'])) : ''; $raw = isset($_POST['terms']) ? wp_unslash($_POST['terms']) : ''; $decoded = json_decode((string) $raw, true); if (!is_array($decoded)) { wp_send_json_error(array('message' => __('Could not read the terms.', 'ai-eshop-optimizer'))); } $terms = array_map('sanitize_text_field', array_values($decoded)); $result = AIEO_Attribute_Axis_Resolver::create_attribute($label, $kind, $terms); if (is_wp_error($result)) { wp_send_json_error(array('message' => $result->get_error_message())); } AIEO_Duplicate_Product_Groups::flush(); wp_send_json_success(array( 'message' => sprintf( __('Created "%1$s" with %2$s terms, and switched it on. Reload to see this group resolved.', 'ai-eshop-optimizer'), $label, number_format_i18n($result['created']) ), 'taxonomy' => $result['taxonomy'], )); } public function ajax_save_attributes() { AIEO_DMM::verify_ajax_request(); $this->require_pro(); $raw = isset($_POST['map']) ? wp_unslash($_POST['map']) : ''; $decoded = json_decode((string) $raw, true); if (!is_array($decoded)) { wp_send_json_error(array('message' => __('Could not read the selection.', 'ai-eshop-optimizer'))); } $map = array(); foreach ($decoded as $taxonomy => $axis) { $map[sanitize_key($taxonomy)] = sanitize_key($axis); } AIEO_Attribute_Axis_Resolver::set_attribute_map($map); AIEO_Duplicate_Product_Groups::flush(); wp_send_json_success(array( 'message' => __('Saved. The detected groups are rebuilt with this vocabulary next time you open them.', 'ai-eshop-optimizer'), )); } private function posted_groups() { $raw = isset($_POST['groups']) ? wp_unslash($_POST['groups']) : ''; $decoded = json_decode((string) $raw, true); if (!is_array($decoded)) { return array(); } $out = array(); foreach ($decoded as $list) { $ids = array_values(array_unique(array_filter(array_map('intval', explode(',', (string) $list))))); if (count($ids) >= 2) { $out[] = $ids; } } return array_slice($out, 0, 50); } public function ajax_bulk_preview() { AIEO_DMM::verify_ajax_request(); $this->require_pro(); $groups = $this->posted_groups(); if (!$groups) { wp_send_json_error(array('message' => __('Select at least one group.', 'ai-eshop-optimizer'))); } $html = ''; $mergeable = array(); $refused = 0; rooster_admin_builders(); foreach ($groups as $ids) { $preview = AIEO_Duplicate_Product_Merge::preview($ids); if (is_wp_error($preview)) { $refused++; $html .= \Rooster\Admin\Ui::notice('' . esc_html__('Skipped:', 'ai-eshop-optimizer') . ' ' . esc_html($preview->get_error_message()), 'warning', array('inline' => true)); continue; } $mergeable[] = implode(',', $ids); $html .= $this->render_preview_html($preview, false); } wp_send_json_success(array( 'html' => $html, 'icons' => \Rooster\Admin\Ui::icons_of($html), 'mergeable' => $mergeable, 'refused' => $refused, )); } public function ajax_bulk_merge() { AIEO_DMM::verify_ajax_request(); $this->require_pro(); $groups = $this->posted_groups(); if (!$groups) { wp_send_json_error(array('message' => __('Nothing to merge.', 'ai-eshop-optimizer'))); } $merged = 0; $errors = array(); foreach ($groups as $ids) { $result = AIEO_Duplicate_Product_Merge::merge($ids); if (is_wp_error($result)) { $errors[] = $result->get_error_message(); continue; } $merged++; } wp_send_json_success(array( 'merged' => $merged, 'errors' => $errors, 'message' => sprintf( _n('Merged %1$s group. %2$s could not be merged.', 'Merged %1$s groups. %2$s could not be merged.', $merged, 'ai-eshop-optimizer'), number_format_i18n($merged), number_format_i18n(count($errors)) ), )); } private function posted_ids() { $raw = isset($_POST['ids']) ? sanitize_text_field(wp_unslash($_POST['ids'])) : ''; $ids = array_filter(array_map('intval', explode(',', $raw))); return array_values(array_unique($ids)); } public function ajax_preview() { AIEO_DMM::verify_ajax_request(); $this->require_pro(); $primary = isset($_POST['primary_id']) ? (int) $_POST['primary_id'] : 0; $preview = AIEO_Duplicate_Product_Merge::preview($this->posted_ids(), array( 'primary_id' => $primary, 'values' => $this->posted_values(), )); if (is_wp_error($preview)) { wp_send_json_error(array('message' => $preview->get_error_message())); } rooster_admin_builders(); $html = $this->render_preview_html($preview); wp_send_json_success(array( 'html' => $html, 'icons' => \Rooster\Admin\Ui::icons_of($html), )); } private function render_preview_html(array $p, $with_confirm = true) { list($open, $close) = AIEO_DMM_UI::card(array( 'anchor' => 'duplicate-simples-preview-' . (int) $p['primary_id'], 'title' => (string) $p['parent_title'], 'description' => __('is the product that will be created.', 'ai-eshop-optimizer'), 'level' => 3, 'class' => 'aieo-ds-preview-card', )); ob_start(); echo $open; ?> 0) : ?> true)); ?> '; foreach ($p['warnings'] as $w) { $aieo_ds_lost .= '
  • ' . esc_html($w) . '
  • '; } echo \Rooster\Admin\Ui::notice('', 'warning', array('inline' => true, 'title' => __('Merging this group would lose something:', 'ai-eshop-optimizer'), 'detail' => $aieo_ds_lost . '')); ?> ' . wp_kses_post(wp_trim_words($p['description']['text'], 120, '…')) . ''); endif; ?> $v) { $aieo_ds_rows[] = array( '_id' => (string) ($i + 1), 'variation' => implode(' / ', array_values($v['attributes'])), 'sku' => $v['sku'] !== '' ? $v['sku'] : '—', 'price' => $v['price'] !== '' ? $v['price'] : '—', 'stock' => $v['stock'] === null ? '—' : (string) $v['stock'], ); } \Rooster\Admin\Markup::out( \Rooster\Admin\Ui::table( array( array('key' => 'variation', 'label' => __('Variation', 'ai-eshop-optimizer')), array('key' => 'sku', 'label' => __('SKU', 'ai-eshop-optimizer'), 'type' => 'code'), array('key' => 'price', 'label' => __('Price', 'ai-eshop-optimizer'), 'type' => 'num'), array('key' => 'stock', 'label' => __('Stock', 'ai-eshop-optimizer'), 'type' => 'num'), ), $aieo_ds_rows, array( 'id' => 'aieo-ds-preview-' . (int) $p['primary_id'], 'caption' => sprintf( __('Variations of %s', 'ai-eshop-optimizer'), $p['parent_title']), 'cards' => true, ) ) ); ?>

    require_pro(); $primary = isset($_POST['primary_id']) ? (int) $_POST['primary_id'] : 0; $result = AIEO_Duplicate_Product_Merge::merge($this->posted_ids(), array( 'primary_id' => $primary, 'values' => $this->posted_values(), )); if (is_wp_error($result)) { wp_send_json_error(array('message' => $result->get_error_message())); } wp_send_json_success(array( 'message' => sprintf( __('Merged %1$s products. %2$s', 'ai-eshop-optimizer'), number_format_i18n(count($result['retired'])), '' . esc_html__('Open the new product', 'ai-eshop-optimizer') . '' ), 'parent_id' => $result['parent_id'], )); } public function ajax_undo() { AIEO_DMM::verify_ajax_request(); $this->require_pro(); $run_id = isset($_POST['run_id']) ? (int) $_POST['run_id'] : 0; global $wpdb; $was = (string) $wpdb->get_var($wpdb->prepare( 'SELECT status FROM ' . AIEO_Duplicate_Product_Merge::log_table() . ' WHERE id = %d', $run_id )); $result = AIEO_Duplicate_Product_Merge::undo($run_id); if (is_wp_error($result)) { wp_send_json_error(array('message' => $result->get_error_message())); } wp_send_json_success(array( 'message' => (AIEO_Duplicate_Product_Merge::STATUS_REPOINTED === $was) ? __('Done. The sales and views are back on the retired products; the merged product is unchanged.', 'ai-eshop-optimizer') : __('Undone. The original products are back and the merged product has been removed.', 'ai-eshop-optimizer'), )); } public function ajax_purge() { AIEO_DMM::verify_ajax_request(); $this->require_pro(); $run_id = isset($_POST['run_id']) ? (int) $_POST['run_id'] : 0; $confirm = isset($_POST['confirm']) && '1' === (string) $_POST['confirm']; $plan = AIEO_Duplicate_Product_Merge::purge_preview($run_id); if (is_wp_error($plan)) { wp_send_json_error(array('message' => $plan->get_error_message())); } if (!empty($plan['blockers'])) { wp_send_json_error(array( 'message' => implode(' ', $plan['blockers']), 'blockers' => $plan['blockers'], )); } if (!$confirm) { wp_send_json_success(array( 'preview' => true, 'plan' => $plan, 'message' => sprintf( __('%1$d retired products will be deleted. First: %2$d reviews, %3$d wishlist entries, %4$d saved cart lines and %5$d cross-sell lists move to the merged product, and each old address gets a 301. This cannot be undone.', 'ai-eshop-optimizer'), count($plan['members']), (int) $plan['reviews'], (int) $plan['wishlist'], (int) $plan['cart_items'], (int) $plan['crosssell'] ), )); } $res = AIEO_Duplicate_Product_Merge::purge($run_id, false); if (is_wp_error($res)) { wp_send_json_error(array('message' => $res->get_error_message())); } wp_send_json_success(array( 'done' => true, 'result' => $res, 'message' => sprintf( __('%1$d retired products deleted. %2$d reviews and %3$d redirects moved to the merged product.', 'ai-eshop-optimizer'), (int) $res['deleted'], (int) $res['reviews'], (int) $res['redirects'] ), )); } public function ajax_search() { AIEO_DMM::verify_ajax_request(); $this->require_pro(); $term = isset($_POST['term']) ? sanitize_text_field(wp_unslash($_POST['term'])) : ''; if (strlen($term) < 2) { self::send_search_answer(array()); } $query = new WP_Query(array( 'post_type' => 'product', 'post_status' => 'publish', 'posts_per_page' => 30, 's' => $term, 'fields' => 'ids', 'tax_query' => array(array( 'taxonomy' => 'product_type', 'field' => 'slug', 'terms' => 'simple', )), )); $results = array(); foreach ($query->posts as $id) { $product = wc_get_product($id); if (!$product) { continue; } $results[] = array( 'id' => (int) $id, 'title' => $product->get_name(), 'sku' => (string) $product->get_sku(), 'price' => (string) $product->get_regular_price('edit'), 'edit' => (string) get_edit_post_link((int) $id, 'raw'), ); } self::send_search_answer($results); } private static function send_search_answer(array $results) { $table = self::manual_results_table($results); wp_send_json_success(array( 'results' => $results, 'table' => $table, 'icons' => '' !== $table ? \Rooster\Admin\Ui::icons_of($table) : '', )); } private static function manual_columns() { $columns = array( array('key' => 'product', 'label' => __('Product', 'ai-eshop-optimizer'), 'type' => 'text', 'sort' => 'none', 'sort_type' => 'text', 'sort_url' => '#dsm_orderby=product'), array('key' => 'sku', 'label' => __('SKU', 'ai-eshop-optimizer'), 'type' => 'code', 'width' => 'sm', 'sort' => 'none', 'sort_type' => 'text', 'sort_url' => '#dsm_orderby=sku'), ); $row = static function (array $p) { $title = (string) $p['title']; $name = '' !== (string) $p['edit'] ? '' . esc_html($title) . '' : '' . esc_html($title) . ''; return array( '_id' => (string) (int) $p['id'], '_name' => $title, '_attrs' => array( 'data-ra-s' => array('product' => $title, 'sku' => (string) $p['sku'], '_id' => (int) $p['id']), 'data-ra-text' => $title . ' ' . (string) $p['sku'], 'data-aieo-title' => $title, ), 'product' => array('html' => $name), 'sku' => '' !== (string) $p['sku'] ? (string) $p['sku'] : '—', ); }; return array($columns, $row); } private static function manual_results_table(array $results) { if (!function_exists('rooster_admin_builders') || !rooster_admin_builders()) { return ''; } list($columns, $row) = self::manual_columns(); return \Rooster\Admin\Ui::table($columns, array_map($row, $results), array( 'id' => 'aieo-ds-results-table', 'caption' => __('Results', 'ai-eshop-optimizer'), 'selectable' => true, 'local' => array('size' => 0, 'prefix' => 'dsm_'), 'empty' => \Rooster\Admin\States::state('no-results', array('compact' => true, 'title' => __('No simple products matched.', 'ai-eshop-optimizer'), 'text' => '')), )); } public function ajax_save_settings() { AIEO_DMM::verify_ajax_request(); $this->require_pro(); $raw = isset($_POST['threshold']) ? (float) wp_unslash($_POST['threshold']) : 0; if ($raw < 0.5 || $raw > 1) { wp_send_json_error(array( 'message' => __('The threshold must be between 0.50 and 1.00. Below 0.50 everything looks like a duplicate.', 'ai-eshop-optimizer'), )); } update_option(AIEO_Duplicate_Product_Groups::OPTION_THRESHOLD, $raw, false); AIEO_Duplicate_Product_Groups::flush(); wp_send_json_success(array( 'threshold' => $raw, 'message' => __('Saved. Reload to rebuild the list at the new threshold.', 'ai-eshop-optimizer'), )); } public function ajax_enable_wc_feature() { AIEO_DMM::verify_ajax_request(); $this->require_pro(); $key = isset($_POST['feature']) ? sanitize_key(wp_unslash($_POST['feature'])) : ''; $result = AIEO_WC_Feature_Check::enable($key); if (is_wp_error($result)) { wp_send_json_error(array('message' => $result->get_error_message())); } wp_send_json_success(array( 'message' => __('Switched on. Reload the product editor to see it.', 'ai-eshop-optimizer'), )); } private function posted_values() { if (empty($_POST['values']) || !is_array($_POST['values'])) { return array(); } $out = array(); foreach (wp_unslash($_POST['values']) as $axis => $per_product) { $axis = sanitize_key($axis); if ($axis === '' || !is_array($per_product)) { continue; } foreach ($per_product as $id => $value) { $value = trim(sanitize_text_field((string) $value)); if ((int) $id > 0 && $value !== '') { $out[$axis][(int) $id] = $value; } } } return $out; } public function render_subtab() { if (!$this->engine_present()) { $this->render_engine_missing_notice(); return; } $section = isset($_GET['ds_section']) ? sanitize_key(wp_unslash($_GET['ds_section'])) : 'detected'; if (!array_key_exists($section, $this->get_tabs())) { $section = 'detected'; } rooster_admin_table_assets(array('cards')); AIEO_DMM_UI::tab_strip($this->get_id(), $this->get_tabs(), $section, array( 'arg' => $this->get_tab_arg(), 'wrapper' => 'div', 'margin' => '14px', )); if ($section === 'manual') { $this->render_template('duplicate-simples-manual.php', array('module' => $this)); return; } if ($section === 'attributes') { $this->render_template('duplicate-simples-attributes.php', array( 'choices' => AIEO_Attribute_Axis_Resolver::attribute_choices(), 'wc_missing' => class_exists('AIEO_WC_Feature_Check') ? AIEO_WC_Feature_Check::missing() : array(), 'module' => $this, )); return; } if ($section === 'log') { $this->render_template('duplicate-simples-log.php', array( 'runs' => AIEO_Duplicate_Product_Merge::history(100), 'module' => $this, )); return; } $available = class_exists('AIEO_Duplicate_Product_Groups') && AIEO_Duplicate_Product_Groups::available(); $all = $available ? AIEO_Duplicate_Product_Groups::find(array('limit' => 0)) : array(); $buckets = array('all' => array(), 'ready' => array(), 'review' => array(), 'blocked' => array()); foreach ($all as $g) { $buckets['all'][] = $g; if (empty($g['convertible'])) { $buckets['blocked'][] = $g; } elseif (empty($g['warnings'])) { $buckets['ready'][] = $g; } else { $buckets['review'][] = $g; } } $show = isset($_GET['ds_show']) ? sanitize_key(wp_unslash($_GET['ds_show'])) : 'all'; if (!isset($buckets[$show])) { $show = 'all'; } $matching = $buckets[$show]; if ($show === 'all' && count($matching) > self::RENDER_CAP) { $reserved = $buckets['blocked']; $held = array(); foreach ($reserved as $g) { $held[$g['key']] = true; } $room = max(0, self::RENDER_CAP - count($reserved)); $rest = array(); foreach ($matching as $g) { if (isset($held[$g['key']])) { continue; } if (count($rest) >= $room) { break; } $rest[] = $g; } $groups = array_merge($rest, $reserved); usort($groups, function ($a, $b) { if ($a['confidence'] === $b['confidence']) { return count($b['members']) - count($a['members']); } return ($a['confidence'] < $b['confidence']) ? 1 : -1; }); } else { $groups = array_slice($matching, 0, self::RENDER_CAP); } $this->render_template('duplicate-simples.php', array( 'groups' => $groups, 'available' => $available, 'threshold' => class_exists('AIEO_Duplicate_Product_Groups') ? AIEO_Duplicate_Product_Groups::threshold() : 0.95, 'show' => $show, 'counts' => array_map('count', $buckets), 'hidden' => max(0, count($matching) - count($groups)), 'module' => $this, )); } }