# Technical Development Standards

VERY IMPORTANT

The latest major PrestaShop version (V9.0.0) was launched on June 2025 and its adoption is crucial for PrestaShop ecosystem to continue thriving.

πŸ‘‰ All new products must be compatible to the latest PrestaShop version available, otherwise the submissions will be automatically rejected by PrestaShop Validation team.

πŸ‘‰ For all existing products, from February 1st 2026, all update submissions will be rejected by PrestaShop Validation team if the product is not already compatible with the latest PrestaShop version.

# 🚨 Mandatory Technical Rules

These rules are non-negotiable. Failure to comply will result in automatic rejection.

# Core Module Structure

# 🚨 Module Structure Compliance

my_module/
β”œβ”€β”€ config.xml
β”œβ”€β”€ my_module.php (main module file)
β”œβ”€β”€ index.php (directory protection)
β”œβ”€β”€ views/
β”‚   β”œβ”€β”€ index.php
β”‚   β”œβ”€β”€ templates/
β”‚   └── js/
β”œβ”€β”€ controllers/
β”œβ”€β”€ override/ (if absolutely necessary)
β”œβ”€β”€ docs/ (documentation)
└── .htaccess (security protection)

# 🚨 PrestaShop Context Protection

Every PHP file must include this security check:

<?php
if (!defined('_PS_VERSION_')) {
    exit;
}

# 🚨 Version Compatibility Declaration

Precisely declare your module's compatibility:

public function __construct()
{
    $this->name = 'my_module';
    $this->version = '1.0.0';
    $this->author = 'Your Company';

    // Critical: Declare exact compatibility range
    $this->ps_versions_compliancy = array(
        'min' => '8.0.0',
        'max' => '9.99.99' // Never use _PS_VERSION_ here
    );

    parent::__construct();
}

# Database and Core Integrity

# 🚨 Core Tables Untouchable

Never alter PrestaShop core database tables:

// ❌ FORBIDDEN: Altering core tables
ALTER TABLE ps_customer ADD COLUMN my_field VARCHAR(255);

// βœ… CORRECT: Create your own table with foreign key
CREATE TABLE ps_my_module_data (
    id_customer INT UNSIGNED NOT NULL,
    my_field VARCHAR(255),
    PRIMARY KEY (id_customer),
    FOREIGN KEY (id_customer) REFERENCES ps_customer(id_customer) ON DELETE CASCADE
);

# 🚨 No Module Interference

Never modify other modules or core files:

// ❌ FORBIDDEN: Modifying other modules
include_once(_PS_MODULE_DIR_.'other_module/other_module.php');
$other_module = new OtherModule();
$other_module->some_method = 'modified'; // NEVER DO THIS

// βœ… CORRECT: Use hooks to interact
$this->context->hook->exec('actionMyModuleProcess', array('data' => $my_data));

# Code Quality Standards

# 🚨 Debug Mode Compatibility

Your module must produce zero errors, warnings, or notices when debug mode is enabled:

// Test your module with these settings:
define('_PS_MODE_DEV_', true);
ini_set('display_errors', 'on');
error_reporting(E_ALL);

# 🚨 English Language Requirement

All code, comments, and default text must be in English:

// βœ… CORRECT: English comments and code
/**
 * Process customer registration data
 * @param array $customer_data Customer information
 * @return bool Success status
 */
public function processCustomerRegistration($customer_data)
{
    // Validate email format
    if (!Validate::isEmail($customer_data['email'])) {
        return false;
    }
    // Process registration...
}

// ❌ FORBIDDEN: Non-English comments
/**
 * Traite les donnΓ©es d'inscription client
 */

# Security Requirements

Security is paramount in the PrestaShop ecosystem. Every module is scanned for vulnerabilities:

# SQL Injection Prevention

# 🚨 All SQL Variables Must Be Sanitised

// βœ… CORRECT: String sanitization
$sql = 'SELECT * FROM `' . _DB_PREFIX_ . 'orders`
        WHERE `reference` = "' . pSQL($reference) . '"';

// βœ… CORRECT: Integer sanitization
$sql = 'SELECT * FROM `' . _DB_PREFIX_ . 'orders`
        WHERE `id_order` = ' . (int)$id_order;

// βœ… CORRECT: Array sanitization
$product_ids = array_map('intval', $product_ids);
$sql = 'SELECT * FROM `' . _DB_PREFIX_ . 'product`
        WHERE `id_product` IN (' . implode(',', $product_ids) . ')';

// βœ… CORRECT: Table/field name sanitization
$table = bqSQL($table_name);
$sql = 'SELECT * FROM `' . _DB_PREFIX_ . $table . '`';

// ❌ FORBIDDEN: Unsanitized variables
$sql = 'SELECT * FROM `' . _DB_PREFIX_ . 'orders`
        WHERE `reference` = "' . $reference . '"'; // DANGEROUS!

# XSS Prevention in Templates

# 🚨 Escape All Smarty Variables

{* βœ… CORRECT: HTML content escaping *}
<div class="content">{$user_content|escape:'htmlall':'UTF-8'}</div>

{* βœ… CORRECT: JavaScript variable escaping *}
<script>
    var userData = '{$user_data|escape:'javascript':'UTF-8'}';
</script>

{* βœ… CORRECT: URL parameter escaping *}
<a href="link.php?param={$parameter|escape:'url'}">Link</a>

{* ❌ FORBIDDEN: Unescaped variables *}
<div>{$user_content}</div> <!-- XSS VULNERABILITY! -->

# File Security Protection

# 🚨 Directory Protection

Every directory must contain an index.php file:

<?php
// index.php content for directory protection
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

# 🚨 .htaccess Protection

Root module directory must contain .htaccess:

# Module root .htaccess
# Apache 2.4
<IfModule mod_authz_core.c>
    <Files *.php>
        Require all denied
    </Files>
</IfModule>

# Apache 2.2
<IfModule !mod_authz_core.c>
    <Files *.php>
        Order allow,deny
        Deny from all
    </Files>
</IfModule>

# AJAX and External Call Security

# 🚨 Secure Token Implementation

class MyModuleFrontController extends ModuleFrontController
{
    public function __construct()
    {
        parent::__construct();

        // Generate and verify security token
        if (!$this->verifyToken()) {
            die('Access denied');
        }
    }

    private function verifyToken()
    {
        $expected_token = Tools::hash($this->module->name . Configuration::get('MY_MODULE_SECRET'));
        $received_token = Tools::getValue('token');

        return hash_equals($expected_token, $received_token);
    }
}

# Dangerous Methods Prohibition

# 🚨 Forbidden Functions

// ❌ FORBIDDEN: serialize/unserialize (RCE risk)
$data = serialize($user_input); // DANGEROUS!
$object = unserialize($user_data); // EXTREMELY DANGEROUS!

// βœ… CORRECT: Use JSON instead
$data = json_encode($user_input, JSON_HEX_TAG | JSON_HEX_AMP);
$object = json_decode($user_data, true);

// ❌ FORBIDDEN: eval() function
eval($user_code); // NEVER USE THIS!

// ❌ FORBIDDEN: Unrestricted file operations
file_get_contents($_GET['file']); // Path traversal risk!
include($_POST['file']); // RCE risk!

# Performance Guidelines

# Hook Optimization

# ⚠️ Lightweight Hook Implementation

Hooks are called on every page load. Optimize performance:

// βœ… OPTIMIZED: Early exit for irrelevant contexts
public function hookDisplayAdminAfterHeader()
{
    // Exit early if not the target controller
    if ($this->context->controller->controller_name !== 'AdminProducts') {
        return '';
    }

    // Only load assets when needed
    $this->context->controller->addJquery();
    $this->context->controller->addJS($this->_path . 'js/admin-products.js');

    return $this->display(__FILE__, 'admin-header.tpl');
}

// ❌ INEFFICIENT: Processing on every page
public function hookDisplayAdminAfterHeader()
{
    // This runs on EVERY admin page - wasteful!
    $products = Product::getProducts($this->context->language->id, 0, 100);
    $this->context->smarty->assign('products', $products);
    return $this->display(__FILE__, 'admin-header.tpl');
}

# Database Optimization

# βœ… Efficient Query Patterns

// βœ… OPTIMIZED: Single query with joins
$sql = 'SELECT o.*, c.firstname, c.lastname
        FROM `' . _DB_PREFIX_ . 'orders` o
        LEFT JOIN `' . _DB_PREFIX_ . 'customer` c ON (o.id_customer = c.id_customer)
        WHERE o.date_add >= "' . pSQL($date_from) . '"
        AND o.date_add <= "' . pSQL($date_to) . '"
        LIMIT 0, 50';

// ❌ INEFFICIENT: Multiple queries in loop
foreach ($orders as $order) {
    $customer = new Customer($order['id_customer']); // N+1 query problem!
}

# Asset Management

# βœ… Conditional Asset Loading

// In module constructor
public function __construct()
{
    // ... other initialization ...

    // Only register assets for relevant pages
    if (Tools::getValue('controller') === 'product') {
        $this->context->controller->registerStylesheet(
            'module-mymodule-product',
            'modules/' . $this->name . '/views/css/product.css',
            ['media' => 'all', 'priority' => 150]
        );
    }
}

# Module Architecture Best Practices

# Configuration Management

# βœ… Namespaced Configuration Keys

Prevent conflicts with other modules:

// βœ… CORRECT: Prefixed configuration keys
Configuration::updateValue('MYMODULE_API_KEY', $api_key);
Configuration::updateValue('MYMODULE_WEBHOOK_URL', $webhook_url);
Configuration::updateValue('MYMODULE_ENABLED_FEATURES', json_encode($features));

// ❌ CONFLICT RISK: Generic keys
Configuration::updateValue('API_KEY', $api_key); // Could conflict!
Configuration::updateValue('ENABLED', true); // Too generic!

# Override Minimisation

# ⚠️ Use Hooks Instead of Overrides

Overrides are the primary source of module conflicts:

// βœ… PREFERRED: Hook-based approach
public function hookDisplayProductAdditionalInfo($params)
{
    $product = $params['product'];
    $additional_info = $this->getAdditionalProductInfo($product['id_product']);

    $this->context->smarty->assign([
        'additional_info' => $additional_info,
        'product' => $product
    ]);

    return $this->display(__FILE__, 'product-additional-info.tpl');
}

// ⚠️ USE SPARINGLY: Override only when absolutely necessary
// override/classes/Product.php
class Product extends ProductCore
{
    // Only override if no hook exists for your specific need
    // Document WHY this override is technically unavoidable
}

# Error Handling and Logging

# βœ… Comprehensive Error Management

public function processApiRequest($data)
{
    try {
        $response = $this->callExternalApi($data);

        if (!$response || isset($response['error'])) {
            throw new PrestaShopException('API call failed: ' . ($response['error'] ?? 'Unknown error'));
        }

        return $response;

    } catch (Exception $e) {
        // Log error for debugging
        PrestaShopLogger::addLog(
            'MyModule API Error: ' . $e->getMessage(),
            PrestaShopLogger::LOG_SEVERITY_LEVEL_ERROR,
            null,
            'MyModule',
            null,
            true
        );

        // Return graceful fallback
        return ['success' => false, 'message' => $this->l('Service temporarily unavailable')];
    }
}

# Theme Development Guidelines

# Hook Implementation Strategy

# 🚨 Always Use Hooks, Never Hardcode

Themes must provide flexibility for module integration:

{* βœ… CORRECT: Hook-based integration *}
<div class="product-additional-info">
    {hook h='displayProductAdditionalInfo'}
</div>

{* ❌ FORBIDDEN: Hardcoded module template *}
{include file="modules/my_module/views/templates/hook/product-info.tpl"}

# βœ… Smart Hook Output Checking

Prevent empty HTML structures:

{* βœ… OPTIMIZED: Check hook content before display *}
{capture name="product_tabs"}{hook h='displayProductTab'}{/capture}
{if !empty($smarty.capture.product_tabs|trim)}
    <div class="product-tabs-container">
        <ul class="nav nav-tabs">
            {$smarty.capture.product_tabs nofilter}
        </ul>
    </div>
{/if}

{* ❌ INEFFICIENT: Always render container *}
<div class="product-tabs-container">
    {hook h='displayProductTab'} {* Might be empty! *}
</div>

# Custom Hook Declaration

# βœ… Register Custom Hooks in theme.yml

global_settings:
  hooks:
    custom_hooks:
      - name: displayCustomProductBanner
        title: "Custom Product Banner"
        description: "Display promotional banner on product pages"
      - name: displayCheckoutExtraInfo
        title: "Checkout Additional Information"
        description: "Add extra information during checkout process"

    modules_to_hook:
      displayCustomProductBanner:
        - ps_banner
        - my_promotion_module

# Responsive Image Implementation

# βœ… Modern Image Handling

<!-- βœ… OPTIMIZED: Multiple format support with responsive sizing -->
<picture class="product-image">
    <source
        srcset="{$product.cover.bySize.large_default.sources.avif}"
        type="image/avif">
    <source
        srcset="{$product.cover.bySize.large_default.sources.webp}"
        type="image/webp">
    <img
        src="{$product.cover.bySize.large_default.url}"
        srcset="{$product.cover.bySize.medium_default.url} 768w,
                {$product.cover.bySize.large_default.url} 1200w"
        sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
        width="{$product.cover.bySize.large_default.width}"
        height="{$product.cover.bySize.large_default.height}"
        loading="lazy"
        alt="{$product.name}"
        class="img-fluid">
</picture>