Услуги Создание магазина Доработка Интеграция 1С О компании FAQ Блог Кейсы Отзывы Контакты
А
Автор статьи

Как получить минимальную цену товара в категории OpenCart

Как получить минимальную цену товара в категории OpenCart

Минимальную цену товара в категории OpenCart получают через SQL-запрос в модели каталога. Для OC 2.x/3.x — JOIN с таблицей product_special, для OC 4 — подзапрос к product_discount с флагом special = 1. Форматирование цены выполняется в контроллере — это стандартный MVC-подход: данные в модели, оформление в контроллере.

Статья покрывает OpenCart 2.x, 3.x, ocStore и OpenCart 4. Для каждой версии приведён рабочий код, который можно Adapt под свой проект.

Зачем нужна минимальная цена в категории

Минимальная цена пригодится в нескольких случаях:

  • Блок «цены от» — на странице категории или в карточке категории в меню. Покупатель видит минимальный порог входа и принимает решение, стоит ли заходить внутрь.
  • SEO-текст категории — автоматическая подстановка цены в текст, актуальная для каталогов с динамическими ценами.
  • Микроразметка Schema.org — передача минимальной цены в offers.lowPrice для расширенного сниппета в Google и Яндексе.
  • Фильтры и промо-блоки — логика «товары от X рублей» для сторонних модулей фильтрации.

Для магазинов с каталогом от 5 000 товаров такая доработка — один из способов усилить категорийные страницы без замены CMS или тяжёлой кастомной разработки.

Быстрый способ: использовать getProducts

Прежде чем писать новый метод — проверьте, не решает ли задачу уже существующий. В OpenCart 3.x метод getProducts() модели ModelCatalogProduct принимает параметр filter_category_id и filter_sub_category, который использует таблицу category_path для поиска товаров по всем дочерним категориям.

Если минимальная цена нужна только для вывода на странице и каталог небольшой — можно обойтись без нового SQL-запроса:

$this->load->model('catalog/product');

$products = $this->model_catalog_product->getProducts([
    'filter_category_id' => $category_id,
    'filter_sub_category' => true,
    'start' => 0,
    'limit' => 1000
]);

$min_price = PHP_INT_MAX;
foreach ($products as $product) {
    $price = (float)($product['special'] ?: $product['price']);
    if ($price > 0 && $price < $min_price) {
        $min_price = $price;
    }
}

Когда этот вариант подходит: категорий не очень много, товаров до нескольких тысяч, и минимальная цена нужна только для вывода на странице. При каждом запросе пользователя метод обращается к БД — для каталогов от 10 000 товаров лучше написать отдельный оптимизированный запрос.

Функция получения минимальной цены

Для рабочего магазина с большим каталогом лучше написать отдельный метод. Ниже — два варианта: для OpenCart 2.x/3.x и для OpenCart 4.

OpenCart 2.x / 3.x

Модель товара: catalog/model/catalog/product.php. Добавьте метод в класс ModelCatalogProduct:

public function getMinPriceFromCategory($category_id, $include_subcategories = true) {
    $customer_group_id = (int)$this->config->get('config_customer_group_id');

    $category_filter = "p2c.category_id = '" . (int)$category_id . "'";

    if ($include_subcategories) {
        $category_filter .= " OR p2c.category_id IN (
            SELECT category_id FROM " . DB_PREFIX . "category_path
            WHERE path_id = '" . (int)$category_id . "'
        )";
    }

    $sql = "SELECT MIN(COALESCE(ps.price, p.price)) AS min_price
            FROM " . DB_PREFIX . "product p
            LEFT JOIN " . DB_PREFIX . "product_to_category p2c
                ON (p.product_id = p2c.product_id)
            LEFT JOIN " . DB_PREFIX . "product_special ps
                ON (p.product_id = ps.product_id
                    AND ps.customer_group_id = '" . (int)$customer_group_id . "'
                    AND ((ps.date_start = '0000-00-00' OR ps.date_start < NOW())
                    AND (ps.date_end = '0000-00-00' OR ps.date_end > NOW())))
            WHERE p.status = '1'
                AND p.date_available <= NOW()
                AND (" . $category_filter . ")";

    $query = $this->db->query($sql);

    return isset($query->row['min_price']) ? (float)$query->row['min_price'] : 0;
}

Что учитывает запрос: только активные товары (p.status = 1), дату доступности, актуальную специальную цену из product_special для текущей группы покупателей. Если специальная цена не задана или не активна — берётся базовая цена.

Что не учитывает: скидки из таблицы product_discount (они зависят от количества и требуют отдельной логики), остатки товаров. Таблица category_path содержит предрасчитанный путь для каждой категории — подзапрос не вызывает рекурсию на уровне SQL и работает быстро даже при глубокой вложенности.

OpenCart 4

В OpenCart 4 структура БД изменилась: таблица product_special больше не используется. Специальные цены хранятся в таблице product_discount с флагом special = '1'. Также добавлена таблица product_to_store для мультисторности.

Модель товара в OC 4: catalog/model/catalog/product.php с namespace OpencartCatalogModelCatalog.

public function getMinPriceFromCategory(int $category_id, bool $include_subcategories = true): float {
    $customer_group_id = (int)$this->config->get('config_customer_group_id');
    $store_id = (int)$this->config->get('config_store_id');

    $category_filter = "p2c.category_id = '" . (int)$category_id . "'";

    if ($include_subcategories) {
        $category_filter .= " OR p2c.category_id IN (
            SELECT category_id FROM " . DB_PREFIX . "category_path
            WHERE path_id = '" . (int)$category_id . "'
        )";
    }

    $sql = "SELECT MIN(COALESCE(
        (SELECT ps.price FROM " . DB_PREFIX . "product_discount ps
         WHERE ps.product_id = p.product_id
         AND ps.customer_group_id = '" . (int)$customer_group_id . "'
         AND ps.quantity = '1'
         AND ps.special = '1'
         AND ((ps.date_start = '0000-00-00' OR ps.date_start < NOW())
         AND (ps.date_end = '0000-00-00' OR ps.date_end > NOW()))
         ORDER BY ps.priority ASC, ps.price ASC LIMIT 1),
        p.price)) AS min_price
        FROM " . DB_PREFIX . "product p
        LEFT JOIN " . DB_PREFIX . "product_to_category p2c
            ON (p.product_id = p2c.product_id)
        LEFT JOIN " . DB_PREFIX . "product_to_store p2s
            ON (p.product_id = p2s.product_id)
        WHERE p.status = '1'
            AND p.date_available <= NOW()
            AND p2s.store_id = '" . (int)$store_id . "'
            AND (" . $category_filter . ")";

    $query = $this->db->query($sql);

    return isset($query->row['min_price']) ? (float)$query->row['min_price'] : 0.0;
}

Ключевое отличие от OC 2.x/3.x: вместо LEFT JOIN product_special используется подзапрос к product_discount с фильтрами quantity = 1 и special = 1. Также добавлен фильтр по product_to_store — это обязательно для мультисторных установок.

Вызов в контроллере категории

В контроллере catalog/controller/product/category.php вызов добавляется после загрузки модели товара. В стандартном OpenCart модель обычно уже подключена через $this->load->model('catalog/product').

$this->load->model('catalog/product');

$min_price = $this->model_catalog_product->getMinPriceFromCategory($category_id);

$data['min_product_price'] = $min_price > 0
    ? $this->currency->format($min_price, $this->session->data['currency'])
    : '';

// Сырой вариант для микроразметки или JavaScript
$data['min_product_price_raw'] = $min_price;

Форматирование цены выполняется в контроллере, потому что здесь доступен текущий контекст покупателя: валюта, группа, формат отображения. Если минимальная цена нужна в микроразметке Schema.org — используйте $min_product_price_raw (число без форматирования).

Вывод в шаблоне категории

В OpenCart 3 — Twig-шаблон: catalog/view/theme/ВАША_ТЕМА/template/product/category.twig

{% if min_product_price %}
  <div class="category-min-price">Цены от {{ min_product_price }}</div>
{% endif %}

В OpenCart 2 — TPL-шаблон: catalog/view/theme/ВАША_ТЕМА/template/product/category.tpl

<?php if ($min_product_price) { ?>
  <div class="category-min-price">Цены от <?php echo $min_product_price; ?></div>
<?php } ?>

Микроразметка Schema.org

Если минимальная цена выводится на странице категории и вы используете микроразметку ItemList или CollectionPage, можно добавить offers.lowPrice для каждой категории. Это влияет на отображение расширенного сниппета в Google.

Пример в twig-шаблоне (внутри JSON-LD):

{% if min_product_price_raw %}
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "CollectionPage",
  "name": "{{ heading_title }}",
  "offers": {
    "@type": "AggregateOffer",
    "lowPrice": "{{ min_product_price_raw }}",
    "priceCurrency": "{{ currency_code }}"
  }
}
</script>
{% endif %}

Это не заменяет микроразметку товаров (Product), но даёт поисковику общую информацию о диапазоне цен в категории. Проверить корректность разметки можно в Google Rich Results Test — вставьте URL страницы категории и убедитесь, что AggregateOffer распознаётся без ошибок.

Создание модификатора для рабочего магазина

Для рабочего магазина не стоит править файлы ядра напрямую: при обновлении OpenCart правки потеряются. Правильный способ — создать OCMOD-модификатор или отдельный модуль.

Структура OCMOD-файла для OpenCart 2.x/3.x:

<?xml version="1.0" encoding="utf-8"?>
<modification>
    <name>Минимальная цена в категории</name>
    <code>min_price_category</code>
    <version>1.0.0</version>
    <author>Ваше имя</author>
    <link>https://example.com</link>

    <file path="catalog/model/catalog/product.php">
        <operation>
            <search>
                <![CDATA[
                // END
                ]]>
            </search>
            <add position="before">
                <![CDATA[
                public function getMinPriceFromCategory($category_id, $include_subcategories = true) {
                    $customer_group_id = (int)$this->config->get('config_customer_group_id');

                    $category_filter = "p2c.category_id = '" . (int)$category_id . "'";

                    if ($include_subcategories) {
                        $category_filter .= " OR p2c.category_id IN (
                            SELECT category_id FROM " . DB_PREFIX . "category_path
                            WHERE path_id = '" . (int)$category_id . "'
                        )";
                    }

                    $sql = "SELECT MIN(COALESCE(ps.price, p.price)) AS min_price
                            FROM " . DB_PREFIX . "product p
                            LEFT JOIN " . DB_PREFIX . "product_to_category p2c
                                ON (p.product_id = p2c.product_id)
                            LEFT JOIN " . DB_PREFIX . "product_special ps
                                ON (p.product_id = ps.product_id
                                    AND ps.customer_group_id = '" . (int)$customer_group_id . "'
                                    AND ((ps.date_start = '0000-00-00' OR ps.date_start < NOW())
                                    AND (ps.date_end = '0000-00-00' OR ps.date_end > NOW())))
                            WHERE p.status = '1'
                                AND p.date_available <= NOW()
                                AND (" . $category_filter . ")";

                    $query = $this->db->query($sql);

                    return isset($query->row['min_price']) ? (float)$query->row['min_price'] : 0;
                }
                ]]>
            </add>
        </operation>
    </file>
</modification>

После загрузки модификатора через «Модификации» в админке OpenCart нажмите «Обновить» (синяя кнопка с круговыми стрелками). Метод getMinPriceFromCategory появится в модели товара без изменения исходных файлов.

Для OpenCart 4 — та же логика, но с учётом namespace и другого расположения классов. Модификатор будет использовать код из раздела «OpenCart 4» выше.

Оптимизация для больших каталогов

При большом количестве товаров (от 10 000) и частом выводе минимальной цены на многих категориях — каждый запрос к БД создаёт нагрузку. Вот что помогает:

  • Кэширование результата. Сохраняйте минимальную цену по связке category_id + currency_code + customer_group_id. При изменении цен или специальных предложений — сбрасывайте кэш для затронутых категорий. В OpenCart есть встроенный механизм кэширования ($this->cache) — используйте его.
  • Индексы БД. Для быстрой работы запроса важны индексы по product_id, category_id, status, date_available в таблицах product, product_to_category, product_special. В стандартной установке OpenCart основные индексы уже есть, но для каталогов с 50 000+ товаров стоит проверить — об этом подробнее в статье про индексы базы данных OpenCart.
  • Плановый пересчёт. Для магазинов с частым обновлением цен — пересчитывайте минимальную цену при обновлении товаров через админку или API, а не при каждом запросе пользователя. Результат сохраняйте в отдельную таблицу или кэш.

Типовые ошибки при внедрении

На практике я сталкивался с несколькими проблемами, которые повторяются в разных проектах:

  • Забывают про группы покупателей. Если специальная цена назначена для группы «Оптовые покупатели», а запрос не фильтрует по customer_group_id, результат будет некорректным.
  • Не учитывают дату доступности. Товар с date_available в будущем не должен попадать в выборку — иначе покупатель увидит цену товара, который ещё нельзя купить.
  • Правят ядро вместо OCMOD. При обновлении OpenCart все правки в product.php потеряются. Используйте модификаторы или модули.
  • Не тестируют пустые категории. Если в категории нет товаров, запрос вернёт null. В контроллере это обрабатывается условием $min_price > 0 — но если забыть эту проверку, в шаблоне может появиться «Цены от 0 ₽».
  • Не кэшируют при большом каталоге. На каталоге с 20 000 товаров и 500 категориях каждый запрос пользователя генерирует отдельный SQL. Через неделю hosting начнёт жаловаться на нагрузку.

Важные замечания перед внедрением

  • Решите, учитывать ли дочерние категории. Стандартный пример считает товары по связке с category_path. Если ваш магазин не показывает товары из подкатегорий на странице родительской категории — уберите подзапрос к category_path.
  • Проверьте, какая версия OpenCart установлена. В OC 2.x/3.x используется таблица product_special, в OC 4 — product_discount с флагом special = 1. Подробнее о том, как узнать версию OpenCart.
  • Тестируйте на копии сайта. Перед внедрением проверьте: категории без товаров, товары с акциями, разные валюты, отключённые товары, товары с датой доступности в будущем.
  • Проверьте индексы базы данных. Для больших каталогов важны индексы по product_id, category_id, status, date_available и таблицам специальных цен.

FAQ: минимальная цена в категории OpenCart

Почему лучше возвращать число, а не готовую строку с валютой?

Модель отвечает за данные, а не за оформление. Число можно использовать в шаблоне, микроразметке, фильтре, API или JavaScript. Форматирование через валюту выполняется в контроллере, где доступен текущий контекст покупателя.

Учитывает ли код акционные цены?

Да. В OC 2.x/3.x специальные цены из таблицы product_special учитываются, если они активны по датам и подходят текущей группе покупателей. В OC 4 — из product_discount с флагом special = 1. Скидки за количество из product_discount (без флага special) не учитываются — они требуют отдельной логики.

Будет ли код работать в OpenCart 4?

Да, но с изменениями в SQL-запросе. Таблица product_special в OC 4 не используется — специальные цены хранятся в product_discount. Также добавлен фильтр по product_to_store. Точку вызова и способ передачи данных в шаблон нужно адаптировать под namespace. Код для OC 4 приведён в разделе «OpenCart 4» выше.

Можно ли сделать без правки файлов ядра?

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

Как учитывать товары из подкатегорий?

Используйте запрос с обращением к таблице category_path, которая содержит предрасчитанный путь для каждой категории. Подзапрос SELECT category_id FROM category_path WHERE path_id = ... вернёт ID всех дочерних категорий без рекурсии на уровне PHP.

Что делать, если в категории нет товаров?

Запрос вернёт null вместо цены. В контроллере это обрабатывается условием $min_price > 0 — если цена равна 0 или товары отсутствуют, переменная $min_product_price будет пустой, и блок «Цены от» не выведется.

Как правильно отформатировать цены для разных валют?

Используйте $this->currency->format() в контроллере — он подставит текущий символ валюты, разделитель и количество знаков после запятой из настроек магазина. Не форматируйте цену в модели — это нарушит разделение логики MVC.

Полезные материалы для разработчиков OpenCart

Если вам нужна помощь с доработкой магазина на OpenCart — смотрите наши услуги по доработке. Мы работаем с OpenCart с 2009 года и настроили десятки подобных интеграций для клиентов.

← Предыдущая Проблемные модули и шаблоны OpenCart для РФ и РБ: как проверить перед покупкой Следующая → Ошибка 500 в OpenCart: причины и как исправить

Комментарии (0)

Пока нет комментариев. Будьте первым!

Оставить комментарий

Ваш комментарий появится после проверки модератором.