Разработка кастомного модуля OpenCart
Стандартный функционал OpenCart покрывает большинство базовых e-commerce задач, но рано или поздно возникают требования, которые выходят за рамки настроек: интеграция с конкретной учётной системой, нестандартная логика ценообразования, специфичная форма заказа, кастомный блок на витрине. Это задачи для расширений (extension) — основного механизма расширения OpenCart.
Типы расширений OpenCart
OpenCart 4.x структурирует расширения по типам:
| Тип | Назначение |
|---|---|
module |
Блоки на витрине (баннеры, хиты, выборки товаров) |
payment |
Платёжные шлюзы |
shipping |
Методы доставки с расчётом стоимости |
total |
Позиции в итоговой сумме заказа (скидки, сборы, налоги) |
report |
Отчёты в панели администратора |
event |
Обработчики событий системы (хуки) |
theme |
Темы оформления |
language |
Языковые пакеты |
Большинство кастомных задач решается через module (витрина) или event (интеграции, модификаторы бизнес-логики).
Структура кастомного модуля
Модуль располагается в папке extension/{vendor_name}/:
extension/
└── myvendor/
├── admin/
│ ├── controller/
│ │ └── module/
│ │ └── mymodule.php
│ ├── language/
│ │ ├── en-gb/module/mymodule.php
│ │ └── ru-ru/module/mymodule.php
│ ├── model/
│ │ └── module/
│ │ └── mymodule.php
│ └── view/
│ └── template/module/
│ └── mymodule.twig
└── catalog/
├── controller/
│ └── module/
│ └── mymodule.php
├── language/
│ ├── en-gb/module/mymodule.php
│ └── ru-ru/module/mymodule.php
├── model/
│ └── module/
│ └── mymodule.php
└── view/
└── template/module/
└── mymodule.twig
Пример: модуль «Товары из той же категории»
Практический пример — модуль, показывающий товары из той же категории, что и просматриваемый товар.
Admin Controller (admin/controller/module/related_category.php):
<?php
namespace Opencart\Admin\Controller\Extension\Myvendor\Module;
class RelatedCategory extends \Opencart\System\Engine\Controller
{
public function index(): void
{
$this->load->language('extension/myvendor/module/related_category');
$this->document->setTitle($this->language->get('heading_title'));
$data['heading_title'] = $this->language->get('heading_title');
// Настройки модуля из формы
$data['limit'] = $this->config->get('module_related_category_limit') ?: 6;
$data['status'] = $this->config->get('module_related_category_status');
$data['action'] = $this->url->link(
'extension/myvendor/module/related_category.save',
'user_token=' . $this->session->data['user_token']
);
$this->response->setOutput($this->load->view(
'extension/myvendor/module/related_category',
$data
));
}
public function save(): void
{
$this->load->language('extension/myvendor/module/related_category');
if ($this->request->server['REQUEST_METHOD'] == 'POST') {
$this->load->model('setting/setting');
$this->model_setting_setting->editSetting('module_related_category', [
'module_related_category_status' => $this->request->post['status'],
'module_related_category_limit' => (int) $this->request->post['limit'],
]);
$this->response->redirect($this->url->link(
'marketplace/extension',
'user_token=' . $this->session->data['user_token'] . '&type=module'
));
}
}
}
Catalog Controller (catalog/controller/module/related_category.php):
<?php
namespace Opencart\Catalog\Controller\Extension\Myvendor\Module;
class RelatedCategory extends \Opencart\System\Engine\Controller
{
public function index(array $setting): string
{
// Работает только на странице товара
if ($this->router->getPath() !== 'product/product') {
return '';
}
$product_id = (int) ($this->request->get['product_id'] ?? 0);
if (!$product_id) {
return '';
}
$this->load->model('extension/myvendor/module/related_category');
$this->load->model('tool/image');
$limit = $setting['limit'] ?? 6;
$products = $this->model_extension_myvendor_module_related_category
->getProductsFromSameCategory($product_id, $limit);
if (!$products) {
return '';
}
$data['products'] = [];
foreach ($products as $product) {
$data['products'][] = [
'product_id' => $product['product_id'],
'name' => $product['name'],
'href' => $this->url->link('product/product', 'product_id=' . $product['product_id']),
'thumb' => $this->model_tool_image->resize(
$product['image'],
$this->config->get('config_image_related_width'),
$this->config->get('config_image_related_height')
),
'price' => $this->currency->format(
$this->tax->calculate($product['price'], $product['tax_class_id'], true),
$this->session->data['currency']
),
];
}
return $this->load->view(
'extension/myvendor/module/related_category',
$data
);
}
}
Catalog Model:
<?php
namespace Opencart\Catalog\Model\Extension\Myvendor\Module;
class RelatedCategory extends \Opencart\System\Engine\Model
{
public function getProductsFromSameCategory(int $productId, int $limit): array
{
$query = $this->db->query("
SELECT DISTINCT p.product_id, pd.name, p.image, p.price, p.tax_class_id
FROM oc_product p
JOIN oc_product_description pd
ON p.product_id = pd.product_id AND pd.language_id = '" . (int) $this->config->get('config_language_id') . "'
JOIN oc_product_to_category ptc
ON p.product_id = ptc.product_id
WHERE ptc.category_id IN (
SELECT category_id FROM oc_product_to_category WHERE product_id = '" . $productId . "'
)
AND p.product_id != '" . $productId . "'
AND p.status = 1
AND p.date_available <= NOW()
ORDER BY RAND()
LIMIT " . $limit . "
");
return $query->rows;
}
}
Система событий (Events)
Для модификации существующего поведения без правки ядра — события:
// Регистрация события в install-методе модуля:
$this->load->model('setting/event');
$this->model_setting_event->addEvent([
'code' => 'myvendor_related_category',
'description' => 'Add category data to product page',
'trigger' => 'catalog/controller/product/product/before',
'action' => 'extension/myvendor/event/product.before',
'status' => true,
'sort_order' => 0,
]);
Обработчик события:
// catalog/controller/event/product.php
class Product extends \Opencart\System\Engine\Controller
{
public function before(\Opencart\System\Engine\Action &$route, array &$args, mixed &$output): void
{
// Срабатывает до загрузки контроллера product/product
// Можно добавить данные в $this->registry или модифицировать $args
}
public function after(\Opencart\System\Engine\Action &$route, array &$args, mixed &$output): void
{
// Срабатывает после генерации HTML страницы товара
// Здесь можно инжектировать HTML-блоки в $output через str_replace
$output = str_replace(
'<!-- related_products_placeholder -->',
$this->getRelatedCategoryHtml(),
$output
);
}
}
Пример: модуль интеграции с 1С (webhook)
Приём данных от 1С для обновления остатков:
// catalog/controller/api/stock_update.php
class StockUpdate extends \Opencart\System\Engine\Controller
{
public function index(): void
{
// Проверка API-ключа
$apiKey = $this->request->server['HTTP_X_API_KEY'] ?? '';
if ($apiKey !== $this->config->get('module_1c_api_key')) {
$this->response->addHeader('HTTP/1.0 403 Forbidden');
$this->response->setOutput(json_encode(['error' => 'Unauthorized']));
return;
}
$payload = json_decode(file_get_contents('php://input'), true);
$this->load->model('catalog/product');
$updated = 0;
foreach ($payload['items'] ?? [] as $item) {
$products = $this->model_catalog_product->getProductsByModel($item['sku']);
foreach ($products as $product) {
$this->model_catalog_product->editProduct($product['product_id'], [
'quantity' => max(0, (int) $item['quantity']),
]);
$updated++;
}
}
$this->response->addHeader('Content-Type: application/json');
$this->response->setOutput(json_encode(['updated' => $updated]));
}
}
Установщик модуля (install/uninstall)
public function install(): void
{
// Создаём таблицы если нужны
$this->db->query("
CREATE TABLE IF NOT EXISTS `oc_mymodule_log` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`message` TEXT NOT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
// Регистрируем события
$this->load->model('setting/event');
// ...
// Настройки по умолчанию
$this->load->model('setting/setting');
$this->model_setting_setting->editSetting('module_mymodule', [
'module_mymodule_status' => 1,
'module_mymodule_limit' => 6,
]);
}
public function uninstall(): void
{
$this->db->query("DROP TABLE IF EXISTS `oc_mymodule_log`");
$this->load->model('setting/event');
$this->model_setting_event->deleteEventByCode('myvendor_mymodule');
}
Упаковка модуля для Extension Installer
Структура ZIP-архива для загрузки через Extension Installer:
mymodule.ocmod.zip
├── extension/
│ └── myvendor/
│ ├── admin/...
│ └── catalog/...
└── install.json
install.json:
{
"name": "My Custom Module",
"version": "1.0.0",
"author": "My Company",
"link": "https://mycompany.by"
}
Сроки разработки
- Простой витринный модуль (блок на страницу): 1–2 дня
- Модуль с настройками в админке + вывод на витрине: 2–3 дня
- Платёжный шлюз (новый провайдер): 3–5 дней
- Метод доставки с расчётом через API: 2–4 дня
- Интеграция с внешней системой (1С, ERP) через webhook: 3–5 дней
- Кастомная логика ценообразования (скидки, группы клиентов): 3–5 дней







