A module for OpenCart 3 is not only working functionality but also a competent approach to description and support. A poor description can scare off buyers, while the right presentation style increases trust and makes it easier to work with your product.
→A well-made module for OpenCart 3 is clean and structured code with a unified style. This reduces the number of questions, increases trust, and helps sell the module more effectively.
→New for August 2025: Email and subscription confirmation, Admin panel protection, Short links module, Distance-based delivery within the region.
→Best-selling templates and extensions in August 2025: Dream Filter product filter, Strizh: social login, Telegram notifications, Dynamic Color template.
→Best-selling templates and extensions in August 2025: Dream Filter product filter, Strizh: social login, Telegram notifications, Dynamic Color template.
→An OpenCart 3 module is not only working functionality but also a competent approach to code and structure. Poorly thought out code can scare off buyers, while a proper development style increases trust and makes it easier to work with your product. Below are a few tips that may help you in development. If you want to add or change something in the text - write in the comments or by email.
This section sets a general approach to code formatting and shared expectations for the team. Simple rules improve readability and reduce maintenance cost.
Here are short and verifiable rules for formatting and naming. They help keep code in one style.
if
, foreach
, function
).=
, ==
, +
, .
) and after commas.
class ControllerExtensionModuleLiveExample extends Controller { public function index() { $this->load->language('extension/module/live_example');
$data['example_status'] = $this->config->get('module_live_example_status');
if ($data['example_status']) {
return $this->load->view('extension/module/live_example', $data);
}
}
}
We define where to keep logic, data and presentation. A clear structure makes the project predictable and convenient to enhance.
language/
.{{ variable }}
, {% if %}
), not PHP.We outline the roles of the core layers. Clear boundaries simplify testing and reuse.
Model
Task: work with the database, SQL queries and returning data.
public function getCustomerByEmail($email) { $sql = "SELECT * FROM " . DB_PREFIX . "customer WHERE email = '" . $this->db->escape($email) . "'"; $query = $this->db->query($sql); return $query->row; }
Controller
Task: work with $this->request
, call models, form $this->response
. The controller must not contain SQL or complex business logic.
public function getCustomer() { $this->load->model('account/customer');
$email = $this->request->get['email'];
$customer = $this->model_account_customer->getCustomerByEmail($email);
$this->response->setOutput(json_encode($customer));
}
Library
Task: keep business logic of the module that can be used from different places (front, admin, cron).
class Bonus { public function calculate($order_total) { return floor($order_total * 0.05); } }
Benefits of separation
We collect basic practices for protection and compatibility checks. These steps reduce risks and extend code life.
$this->db->escape()
and (int)
for SQL queries.htmlspecialchars
or html_entity_decode
on output.mysql_*
, ereg
, split
).mb_*
for strings.This shows how to safely accept data and prepare queries. The examples can be pasted straight into working code.
Wrong (parameters from request directly in SQL):
$query = $this->db->query("SELECT * FROM " . DB_PREFIX . "customer WHERE email = '" . $this->request->post['email'] . "'");
If an attacker enters ' OR 1=1 --
, the query will return all users.
Right (controller validates, model works with arguments):
Controller:
public function info() { // 1) get and validate email $email = trim($this->request->post['email'] ?? ''); if (!filter_var($email, FILTER_VALIDATE_EMAIL) || mb_strlen($email) > 96) { $this->response->addHeader('Content-Type: application/json'); $this->response->setOutput(json_encode(['error' => 'Invalid email'])); return; }
// 2) work via model
$this->load->model('account/customer');
$customer = $this->model_account_customer->getCustomerByEmail($email);
$this->response->addHeader('Content-Type: application/json');
$this->response->setOutput(json_encode($customer));
}
Model:
public function getCustomerByEmail($email) { $email = $this->db->escape($email);
$query = $this->db->query(
"SELECT * FROM " . DB_PREFIX . "customer WHERE email = '" . $email . "'"
);
return $query->row;
}
For numeric values use explicit casting and simple validation:
Controller:
$order_id = (int)($this->request->get['order_id'] ?? 0); if ($order_id <= 0) { $this->response->addHeader('Content-Type: application/json'); $this->response->setOutput(json_encode(['error' => 'Invalid order_id'])); return; }
$this->load->model('account/order');
$order_info = $this->model_account_order->getOrder($order_id);
$this->response->addHeader('Content-Type: application/json');
$this->response->setOutput(json_encode($order_info));
Model:
public function getOrder($order_id) { $query = $this->db->query( "SELECT * FROM " . DB_PREFIX . "order WHERE order_id = " . (int)$order_id ); return $query->row; }
Golden rule: strings via $this->db->escape()
, numbers via (int)
or (float)
. Do not use $this->request
inside the model.
We show how to format queries so they are easy to read and extend. A unified style speeds up review and reduces errors.
For short queries you can use one line:
$query = $this->db->query("SELECT * FROM " . DB_PREFIX . "customer WHERE customer_id = " . (int)$customer_id);
But for long queries a multiline format is recommended:
$query = $this->db->query("
SELECT p.product_id, p.model, pd.name, p.price
FROM " . DB_PREFIX . "product p
LEFT JOIN " . DB_PREFIX . "product_description pd ON (p.product_id = pd.product_id)
LEFT JOIN " . DB_PREFIX . "product_to_category p2c ON (p.product_id = p2c.product_id)
WHERE pd.language_id = " . (int)$this->config->get('config_language_id') . "
AND p.status = '1'
AND p.date_available <= NOW()
ORDER BY p.date_added DESC
LIMIT 20
");
We examine options for connecting your classes. The choice depends on where the object is needed and when it is better to initialize it.
1. Via the registry in catalog/startup/startup.php
Suitable if the library must be available globally on the front right after startup.
system/library/MyLib.php
or catalog/library/MyLib.php
.catalog/controller/startup/startup.php
add registration:
// If the library is in system/library: // $this->load->library('mylib'); // $this->registry->set('mylib', $this->mylib);
// If the library is in catalog/library:
require_once(modification(DIR_CATALOG . 'library/mylib.php'));
$this->registry->set('mylib', new MyLib($this->registry));
Pros: single initialization point, available everywhere. Cons: the object is created on every request even if not used.
2. Locally via require modification(...)
at the place of use
Suitable for lazy loading only where it is actually needed.
require_once(modification(DIR_SYSTEM . 'library/mylib.php'));
$mylib = new MyLib($this->registry);
$result = $mylib->doWork($params);
Pros: on-demand connection. Cons: you will have to repeat the include or move it to a helper.
3. Through OpenCart loader: $this->load->library()
The standard way if the library is located in system/library
.
$this->load->library('mylib'); // looks for system/library/mylib.php $this->mylib->doWork($params); // the object is available as $this->mylib
Recommendation: if the library lies in system/library
- use $this->load->library()
. If you need early global initialization - register it in startup.php
. If the library is in another directory - connect via require modification(...)
.
Comments complement the code and clarify intent. Short and precise descriptions save reading time.
/** * Getting a product list for example * * @param int $limit Number of products * @return array Product list */ public function getProducts($limit = 10) { $query = $this->db->query("SELECT * FROM " . DB_PREFIX . "product LIMIT " . (int)$limit); return $query->rows; }
The version serves as a guide for updates and support. Transparency reduces the number of support requests.
Show the version number in the module settings. This simplifies support and prevents confusion.
class ControllerExtensionModuleMyModule extends Controller { private $module_version = '1.0.0';
public function index() {
// ... your logic
$data['module_version'] = $this->module_version;
return $this->load->view('extension/module/my_module', $data);
}
}
<div class="text-muted">Module version: {{ module_version }}</div>
Tip: the version number should match the version in the archive and in the CHANGELOG so the client always sees up to date information.
A well made module for OpenCart 3 is clean and structured code with a unified style.
This reduces the number of questions, increases trust and helps sell the module more effectively.