Skip to main content

Overview

OpenCart follows PSR-12 coding standards with custom framework conventions. All code must pass automated checks before merging.

PHP Standards

File Structure

<?php
namespace Opencart\Catalog\Controller\Product;
/**
 * Class Product
 *
 * @package Opencart\Catalog\Controller\Product
 */
class Product extends \Opencart\System\Engine\Controller {
    /**
     * Index
     *
     * @return ?\Opencart\System\Engine\Action
     */
    public function index() {
        // Method implementation
    }
}

Namespace Conventions

All classes must use proper PHP namespaces:
LocationNamespace PatternExample
Catalog ControllerOpencart\Catalog\Controller\{Path}Opencart\Catalog\Controller\Product\Product
Admin ControllerOpencart\Admin\Controller\{Path}Opencart\Admin\Controller\Sale\Order
Catalog ModelOpencart\Catalog\Model\{Path}Opencart\Catalog\Model\Catalog\Product
Admin ModelOpencart\Admin\Model\{Path}Opencart\Admin\Model\Sale\Order
ExtensionOpencart\{App}\Controller\Extension\{Vendor}\{Type}\{Name}Opencart\Catalog\Controller\Extension\Opencart\Payment\BankTransfer
System EngineOpencart\System\Engine\{Class}Opencart\System\Engine\Controller
System LibraryOpencart\System\Library\{Class}Opencart\System\Library\DB

Type Declarations

Always use type hints for parameters and return types:
// ✅ Correct
public function getProduct(int $product_id): array {
    return $this->db->query("...")->row;
}

public function setStatus(int $product_id, bool $status): void {
    $this->db->query("UPDATE ...");
}

public function getTotal(): float {
    return 123.45;
}

// ❌ Incorrect - missing type hints
public function getProduct($product_id) {
    return $this->db->query("...")->row;
}

Property Declarations

Use typed properties with visibility modifiers:
class Product extends \Opencart\System\Engine\Model {
    // ✅ Correct
    protected \Opencart\System\Engine\Registry $registry;
    private array $data = [];
    
    // ❌ Incorrect - no type declaration
    private $data;
}

Naming Conventions

Classes

  • PascalCase for class names
  • Match filename exactly (case-sensitive)
// File: catalog/controller/product/category.php
class Category extends \Opencart\System\Engine\Controller {
    // ...
}

Methods

  • camelCase for method names
  • Descriptive verb-based names
// ✅ Correct
public function getProduct(int $product_id): array
public function addProduct(array $data): int
public function deleteProduct(int $product_id): void
public function getTotalProducts(array $filter = []): int

// ❌ Incorrect
public function product(int $id): array
public function get_product(int $product_id): array

Variables

  • snake_case for variables
  • Descriptive names
// ✅ Correct
$product_id = 123;
$customer_group_id = 1;
$total_products = 100;

// ❌ Incorrect
$pid = 123;
$cgid = 1;
$ProductId = 123;

Constants

  • SCREAMING_SNAKE_CASE for constants
  • Defined in config files
define('DIR_APPLICATION', '/var/www/html/catalog/');
define('DB_PREFIX', 'oc_');
define('HTTP_SERVER', 'https://example.com/');

Database Queries

Parameterization

Always escape user input:
// ✅ Correct - using escape()
$this->db->query("SELECT * FROM `" . DB_PREFIX . "product` 
    WHERE `product_id` = '" . (int)$product_id . "'
    AND `name` = '" . $this->db->escape($name) . "'");

// ❌ Incorrect - SQL injection risk
$this->db->query("SELECT * FROM product WHERE product_id = $product_id");

Table Prefixes

Always use DB_PREFIX constant:
// ✅ Correct
$this->db->query("SELECT * FROM `" . DB_PREFIX . "product` WHERE ...");

// ❌ Incorrect
$this->db->query("SELECT * FROM `oc_product` WHERE ...");

Query Style

  • Use backticks for table and column names
  • Cast integers explicitly with (int)
  • Escape strings with $this->db->escape()
$query = $this->db->query("
    SELECT DISTINCT *
    FROM `" . DB_PREFIX . "product` `p`
    LEFT JOIN `" . DB_PREFIX . "product_description` `pd` 
        ON (`p`.`product_id` = `pd`.`product_id`)
    WHERE `p`.`product_id` = '" . (int)$product_id . "'
        AND `pd`.`language_id` = '" . (int)$this->config->get('config_language_id') . "'
        AND `p`.`status` = '1'
");

MVC Conventions

Controllers

  • Load models using $this->load->model()
  • Pass data to views as arrays
  • Return output strings or Action objects
public function index(): void {
    // Load dependencies
    $this->load->language('product/product');
    $this->load->model('catalog/product');
    
    // Get data
    $product_info = $this->model_catalog_product->getProduct($product_id);
    
    if (!$product_info) {
        return new \Opencart\System\Engine\Action('error/not_found');
    }
    
    // Prepare view data
    $data['heading_title'] = $product_info['name'];
    $data['product_id'] = $product_info['product_id'];
    
    // Render view
    $this->response->setOutput($this->load->view('product/product', $data));
}

Models

  • Contain only business logic and database operations
  • Return data, never render views
  • Use descriptive method names (get*, add*, edit*, delete*, getTotal*)
public function getProduct(int $product_id): array {
    $query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "product` 
        WHERE `product_id` = '" . (int)$product_id . "'");
    
    return $query->row;
}

public function getTotalProducts(array $filter = []): int {
    $sql = "SELECT COUNT(*) AS total FROM `" . DB_PREFIX . "product`";
    
    $query = $this->db->query($sql);
    
    return (int)$query->row['total'];
}

Views

  • Use Twig templating engine
  • Never contain business logic
  • Access data passed from controller
{# product/product.twig #}
<div class="product">
    <h1>{{ heading_title }}</h1>
    <p>Product ID: {{ product_id }}</p>
</div>

Code Organization

Use Loader Methods

Reference existing models before writing raw SQL:
// ✅ Correct - use existing model
$this->load->model('catalog/product');
$products = $this->model_catalog_product->getProducts($filter);

// ❌ Incorrect - duplicate logic
$query = $this->db->query("SELECT * FROM ...");

Avoid Core Modifications

Never modify files in system/ directory:
  • ✅ Use events to extend functionality
  • ✅ Create extensions in extension/{vendor}/
  • ❌ Don’t edit core controllers or models
  • ❌ Don’t modify system libraries

Extension Structure

Extensions must follow this structure:
extension/{vendor}/
├── admin/
│   ├── controller/{type}/{name}.php
│   ├── model/{type}/{name}.php
│   ├── view/template/{type}/{name}.twig
│   └── language/en-gb/{type}/{name}.php
└── catalog/
    ├── controller/{type}/{name}.php
    └── model/{type}/{name}.php

Documentation

PHPDoc Blocks

All classes and public methods must have PHPDoc comments:
/**
 * Class Product
 *
 * Can be called using $this->load->model('catalog/product');
 *
 * @package Opencart\Catalog\Model\Catalog
 */
class Product extends \Opencart\System\Engine\Model {
    /**
     * Get Product
     *
     * Get the record of the product record in the database.
     *
     * @param int $product_id primary key of the product record
     *
     * @return array<string, mixed> product record that has product ID
     *
     * @example
     *
     * $this->load->model('catalog/product');
     *
     * $product_info = $this->model_catalog_product->getProduct($product_id);
     */
    public function getProduct(int $product_id): array {
        // Implementation
    }
}

Automated Checks

Before committing, run these tools:
find upload -type f -name "*.php" ! -path 'upload/system/storage/vendor/*' -exec php -l -n {} +

Git Workflow

  1. Create a branch from master
    git checkout -b feature/my-feature
    
  2. Commit clearly with imperative messages
    git commit -m "Add product variant support to cart"
    
  3. Keep diffs focused - avoid formatting-only changes
  4. Rebase before PR
    git fetch origin
    git rebase origin/master
    
Pull requests that don’t pass automated checks will not be merged. Run all quality tools locally before submitting.

Best Practices

Access services through $this-> magic method:
// ✅ Correct
$this->db->query("...");
$this->config->get('config_name');
$this->session->data['user_id'];

// ❌ Incorrect
global $db;
$_SESSION['user_id'];
Always validate and sanitize user input:
if (isset($this->request->get['product_id'])) {
    $product_id = (int)$this->request->get['product_id'];
} else {
    $product_id = 0;
}
Never hardcode text strings:
// ✅ Correct
$this->load->language('product/product');
$data['heading_title'] = $this->language->get('heading_title');

// ❌ Incorrect
$data['heading_title'] = 'Product Details';
Use the event system instead of modifying core files:
// In your extension's install method
$this->load->model('setting/event');
$this->model_setting_event->addEvent(
    'my_extension',
    'catalog/model/checkout/order/addOrder/after',
    'extension/myvendor/module/myextension.afterAddOrder'
);

Next Steps

MVC Pattern

Understand the MVC architecture

Routing System

Learn URL routing

Database Layer

Work with models and queries

Event System

Extend with events