Кэширование Минимальных и Максимальных Цен для Категорий Товаров

Кэширование Минимальных и Максимальных Цен для Всех Категорий Продуктов (WooCommerce)

Представьте, что у нас есть фильтр продуктов для определенной категории, который позволяет отсортировать товары по цене. Для такого фильтра удобно знать минимальные и максимальные цены продуктов в этой категории.

Можно получать эти цены каждый раз при загрузке страницы категории, но это создаёт нагрузку на сервер. Чтобы улучшить производительность, лучше всего получить минимальные и максимальные цены для каждой категории один раз и использовать их впоследствии в фильтре.

Существует несколько способов кэширования минимальных и максимальных цен. В этой статье я покажу один из них, с которым столкнулся при решении этой задачи.

Принцип Работы

Мы собираем все минимальные и максимальные цены продуктов для каждой категории (на всех уровнях) и для всей таксономии в целом. Затем все эти данные сохраняются в опциях WordPress. Когда продукт обновляется или добавляется, данные обновляются не сразу, а через 60 секунд после изменения записи. Это позволяет обновлять записи массово и не выполнять дорогостоящую операцию при каждом обновлении. Время в 60 секунд можно увеличить до, например, 3600 секунд (1 час).

Есть и другой подход: можно обновлять только данные конкретных категорий, в которых находится обновляемый продукт, сохраняя при этом общие данные для всей таксономии. В этом случае потребуется дополнительная логика, чтобы отслеживать, из какой категории был удалён продукт. Я выбрал первый подход, так как он не имеет таких недостатков.

Пример Кода

Ниже приводится пример кода, который собирает и кэширует минимальные и максимальные значения указанного мета-поля для заданных терминов таксономии. Этот код подходит для получения минимальных и максимальных цен продуктов из категорий.

meta_key или taxonomy.');
        }
        $this->meta_key = $args['meta_key'];
        $this->taxonomy = $args['taxonomy'];
        $this->post_type = $args['post_type'];
        $this->cache_ttl = (int)($args['cache_ttl'] ?? WEEK_IN_SECONDS);
        $this->cache_key = "minmax_{$args['taxonomy']}_{$args['meta_key']}_values";
    }

    public function init(): void {
        add_action('init', [$this, 'check_update_data'], 99);
    }

    public function get_data(): array {
        return ((array)get_transient($this->cache_key)) ?: [];
    }

    public function get_term_minmax($term_id_or_all): array {
        return $this->get_data()[$term_id_or_all] ?? [0, 0];
    }

    public function check_update_data(): void {
        add_action("save_post_{$this->post_type}", [$this, 'mark_data_for_update']);
        add_action('deleted_post', [$this, 'mark_data_for_update']);

        if (time() > ($this->get_data()['valid_until'] ?? 0)) {
            $this->update_data();
        }
    }

    public function mark_data_for_update(): void {
        $minmax_data = $this->get_data();
        $minmax_data['valid_until'] = time() + $this->update_timeout;
        set_transient($this->cache_key, $minmax_data);
    }

    public function update_data(): void {
        $minmax_data = ['valid_until' => time() + $this->cache_ttl];
        $this->add_all_minmax($minmax_data);
        $this->add_terms_minmax($minmax_data);
        set_transient($this->cache_key, $minmax_data);
    }

    private function add_all_minmax(&$minmax_data): void {
        global $wpdb;
        $sql = str_replace('{AND_WHERE}', '', $this->minmax_base_sql());
        $minmax = $wpdb->get_row($sql, ARRAY_A);
        $minmax_data['all'] = [(int)$minmax['min'], (int)$minmax['max']] ?? [0, 0];
    }

    private function add_terms_minmax(&$minmax_data): void {
        global $wpdb;
        $base_sql = $this->minmax_base_sql();

        $terms_data = self::get_terms_post_ids_data($this->taxonomy);
        foreach ($terms_data as $term_id => $post_ids) {
            if (empty($post_ids)) {
                continue;
            }

            $IN_post_ids = implode(',', array_map('intval', $post_ids));
            $minmax = $wpdb->get_row(str_replace('{AND_WHERE}', "AND post_id IN($IN_post_ids)", $base_sql), ARRAY_A);

            if (array_filter($minmax)) {
                $minmax_data[$term_id] = [(int)($minmax['min'] ?? 0), (int)($minmax['max'] ?? 0)];
            }
        }
    }

    private function minmax_base_sql(): string {
        global $wpdb;
        return $wpdb->prepare("
            SELECT MIN(CAST(meta_value as UNSIGNED)) as min, MAX(CAST(meta_value as UNSIGNED)) as max
            FROM $wpdb->postmeta
            WHERE meta_key = %s AND meta_value > 0
            {AND_WHERE}
        ", $this->meta_key);
    }

    private static function get_terms_post_ids_data(string $taxonomy): array {
        global $wpdb;

        $cats_data_sql = $wpdb->prepare("
            SELECT term_id, object_id, parent
            FROM $wpdb->term_taxonomy tax
            LEFT JOIN $wpdb->term_relationships rel ON (rel.term_taxonomy_id = tax.term_taxonomy_id)
            WHERE taxonomy = %s
        ", $taxonomy);

        $terms_data = (array)$wpdb->get_results($cats_data_sql);
        $new_terms_data = [];
        foreach ($terms_data as $data) {
            if (!isset($new_terms_data[$data->term_id])) {
                $new_terms_data[$data->term_id] = (object)[
                    'parent' => $data->parent,
                    'object_id' => [],
                    'child' => [],
                ];
            }
            if ($data->object_id) {
                $new_terms_data[$data->term_id]->object_id[] = $data->object_id;
            }
        }

        foreach ($terms_data as $term_id => $data) {
            if ($data->parent) {
                $terms_data[$data->parent]->child[] = &$terms_data[$term_id]; // ссылка
            }
        }

        $terms_post_ids = [];
        foreach ($terms_data as $term_id => $data) {
            $post_ids = [];
            self::collect_post_ids_recursively($post_ids, $data);
            $terms_post_ids[$term_id] = array_unique($post_ids);
        }

        return $terms_post_ids;
    }

    private static function collect_post_ids_recursively(&$post_ids, $data) {
        if ($data->object_id) {
            $post_ids = array_merge($post_ids, (array)$data->object_id);
        }
        foreach ($data->child as $child_data) {
            self::collect_post_ids_recursively($post_ids, $child_data);
        }
    }
}

Как Использовать

Чтобы инициализировать класс, добавьте следующий код в ваш файл functions.php (в хуке init, чтобы все таксономии были зарегистрированы к этому времени):

global $kama_minmax;
$kama_minmax = new Kama_Minmax_Post_Meta_Values([
    'meta_key' => 'price',
    'taxonomy' => 'product_cat',
    'post_type' => 'product',
    'cache_ttl' => DAY_IN_SECONDS, // по умолчанию WEEK_IN_SECONDS
]);
$kama_minmax->init();

Получение Данных

Чтобы получить данные, используйте следующий код:

global $kama_minmax;
$minmax_data = $kama_minmax->get_data();

В этом случае $minmax_data будет содержать такие данные:

$minmax_data = [
    [uptime] => 1508719235,
    [all] => [80, 68000],
    [1083] => [950, 7300],
    // и так далее...
];

Вы можете получить минимальную и максимальную цену для конкретной категории:

[ $min, $max ] = $kama_minmax->get_term_minmax(123);
echo $min;
echo $max;

Для тестирования кода вы можете использовать параметры GET:

if (isset($_GET['get_minmax_prices_test'])) {
    die(print_r($kama_minmax->get_data()));
}

if (isset($_GET['update_minmax_prices_test'])) {
    $kama_minmax->update_data();
    die(print_r($kama_minmax->get_data()));
}

Заключение

Этот код подходит не только для WooCommerce, но и для любого магазина на платформе WordPress. Основная идея заключается в сборе всех минимальных и максимальных цен из указанного мета-поля для терминов всех уровней вложенности. Это может значительно улучшить производительность вашего интернет-магазина.

Leave a Reply

Ваш адрес email не будет опубликован. Обязательные поля помечены *