News and articles
How to properly structure modules for OpenCart 3

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.

 
 
 
 
 
How to properly create modules for OpenCart 3

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 OpenCart Modules Collection for August 2025

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

Best-selling templates and extensions in August 2025: Dream Filter product filter, Strizh: social login, Telegram notifications, Dynamic Color template.

 
 
 
 
 

How to properly create modules for OpenCart 3

 
How to properly create modules for OpenCart 3
  • 09 September 2025
  • Comment :0
  • Views: 118

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.

Module code style

This section sets a general approach to code formatting and shared expectations for the team. Simple rules improve readability and reduce maintenance cost.

Unified syntax

Here are short and verifiable rules for formatting and naming. They help keep code in one style.

  • Indentation - use 4 spaces, avoid tabs.
  • Braces - place the opening brace on the same line as the construct (if, foreach, function).
  • Spaces - add spaces around operators (=, ==, +, .) and after commas.
  • Naming - classes in CamelCase, variables and array keys in snake_case.
 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);
    }
}


}

Code structure

We define where to keep logic, data and presentation. A clear structure makes the project predictable and convenient to enhance.

  • Controllers - thin, all business logic must be in models.
  • Validation and checks - performed in the model, not in templates.
  • Language files - no text in code or templates, everything goes to language/.
  • Twig templates - use native syntax ({{ variable }}, {% if %}), not PHP.

Separation of concerns: model, controller, library

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

  • Maintainability - changes are easier: SQL in models, logic in libraries, responses in controllers.
  • Testability - you can test the model (SQL), the library (logic) and the controller (API) separately.
  • Reuse - libraries can be connected in the front, in admin and in CRON.

Compatibility and security

We collect basic practices for protection and compatibility checks. These steps reduce risks and extend code life.

  • Use $this->db->escape() and (int) for SQL queries.
  • Apply htmlspecialchars or html_entity_decode on output.
  • Avoid deprecated functions (mysql_*, ereg, split).
  • Use mb_* for strings.
  • Test on current PHP versions (7.4, 8.0, 8.1, 8.2).

Escaping input data

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.

Splitting an SQL query into lines

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
");
  • The structure of the query is clearly visible: SELECT, FROM, JOIN, WHERE, ORDER BY.
  • It is easier to add new conditions without losing readability.
  • Finding SQL errors is simpler.
  • A unified style for all developers on the team.

Connecting custom libraries

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.

  1. Place the library file in system/library/MyLib.php or catalog/library/MyLib.php.
  2. Inside 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

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; } 

Let the customer see your module version

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.

  1. In the admin controller store the version number and pass it to the template:
 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);
}


}
  1. In the admin template output the value:
 <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.


Рекомендуем прочитать
 
 


Yet, no one has left a comment to the entry.