Custom OpenCart Module Development
Standard OpenCart functionality covers most basic e-commerce tasks, but eventually requirements emerge that go beyond settings: integration with specific accounting system, non-standard pricing logic, custom order form, custom storefront block. These are extension tasks — the primary mechanism for extending OpenCart.
Types of OpenCart Extensions
OpenCart 4.x structures extensions by type:
| Type | Purpose |
|---|---|
module |
Storefront blocks (banners, best sellers, product selections) |
payment |
Payment gateways |
shipping |
Delivery methods with cost calculation |
total |
Line items in order total (discounts, fees, taxes) |
report |
Reports in admin panel |
event |
System event handlers (hooks) |
theme |
Design themes |
language |
Language packs |
Most custom tasks solved via module (storefront) or event (integrations, business logic modifiers).
Custom Module Structure
Module located in extension/{vendor_name}/ folder:
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
Example: "Related Products from Same Category" Module
Practical example — module showing products from same category as viewed product.
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');
// Module settings from form
$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
{
// Works only on product page
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;
}
}
Event System
To modify existing behavior without editing core — events:
// Register event in module install method:
$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,
]);
Event handler:
// 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
{
// Triggers before loading product/product controller
// Can add data to $this->registry or modify $args
}
public function after(\Opencart\System\Engine\Action &$route, array &$args, mixed &$output): void
{
// Triggers after generating HTML page
// Can inject HTML blocks into $output via str_replace
$output = str_replace(
'<!-- related_products_placeholder -->',
$this->getRelatedCategoryHtml(),
$output
);
}
}
Example: 1C Integration Module (Webhook)
Receiving data from 1C to update inventory:
// catalog/controller/api/stock_update.php
class StockUpdate extends \Opencart\System\Engine\Controller
{
public function index(): void
{
// Check API key
$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]));
}
}
Module Installer (install/uninstall)
public function install(): void
{
// Create tables if needed
$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;
");
// Register events
$this->load->model('setting/event');
// ...
// Default settings
$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');
}
Packaging Module for Extension Installer
ZIP archive structure for uploading via 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"
}
Development Timeline
- Simple storefront module (block on page): 1–2 days
- Module with admin settings + storefront output: 2–3 days
- Payment gateway (new provider): 3–5 days
- Shipping method with API calculation: 2–4 days
- Integration with external system (1C, ERP) via webhook: 3–5 days
- Custom pricing logic (discounts, customer groups): 3–5 days







