The Main Module PHP File
Every PrestaShop module is built around a single main PHP file whose name matches the module folder. If your module folder is mymodule/, the file must be mymodule/mymodule.php. This file contains a class that extends Module (or a specialisation such as PaymentModule) and acts as the entry point for installation, uninstallation, hook dispatch and configuration.
Always add if (!defined('_PS_VERSION_')) { exit; } immediately after the opening tag. Without it anyone can execute the file directly via the browser.
#Module Class Structure
A minimal module class must extend Module, declare a set of public properties that PrestaShop reads at install-time, and implement a constructor that populates those properties.
#Required Class Properties
| Property | Type | Description |
|---|---|---|
| $name | string | Unique technical name — lowercase, no spaces, matches folder/file name |
| $tab | string | Back-office menu tab (e.g. administration, front_office_features) |
| $version | string | Module version string, e.g. '1.0.0' |
| $author | string | Author name shown in the module manager |
| $need_instance | int | 0 or 1 — whether to instantiate on every page load |
| $ps_versions_compliancy | array | Keys min and max for PrestaShop version range |
| $bootstrap | bool | true if the module configuration page uses Bootstrap 3/4 layout |
#The Constructor
The constructor assigns the properties above and then calls parent::__construct(). After the parent call you have access to $this->l() for translations, so the displayName, description and confirmUninstall strings should be set after the parent call.
<?php
if (!defined('_PS_VERSION_')) {
exit;
}
class MyModule extends Module
{
public function __construct()
{
$this->name = 'mymodule';
$this->tab = 'front_office_features';
$this->version = '1.0.0';
$this->author = 'My Company';
$this->need_instance = 0;
$this->ps_versions_compliancy = [
'min' => '1.7.0.0',
'max' => _PS_VERSION_,
];
$this->bootstrap = true;
parent::__construct();
$this->displayName = $this->l('My Module');
$this->description = $this->l('A short description of what this module does.');
$this->confirmUninstall = $this->l('Are you sure you want to uninstall?');
}
}
#install() and uninstall()
Both methods must return a boolean. Call parent::install() / parent::uninstall() first and chain your own operations. If any step fails, return false so PrestaShop rolls back and shows an error.
<?php
public function install()
{
return parent::install()
&& $this->registerHook('displayHeader')
&& $this->registerHook('displayFooter')
&& $this->createTable()
&& Configuration::updateValue('MYMODULE_ACTIVE', 1);
}
public function uninstall()
{
return parent::uninstall()
&& $this->dropTable()
&& Configuration::deleteByName('MYMODULE_ACTIVE');
}
#Registering Hooks
You can register hooks at install time (as above) or lazily at runtime by calling $this->registerHook() from getContent(). The hook handler method must be named hook + the hook name in CamelCase.
<?php
// Hook handler — called by PrestaShop when displayHeader fires
public function hookDisplayHeader($params)
{
// $params contains context-specific data depending on the hook
$this->context->controller->addCSS($this->_path . 'views/css/mymodule.css');
$this->context->controller->addJS($this->_path . 'views/js/mymodule.js');
}
public function hookDisplayFooter($params)
{
$this->smarty->assign([
'mymodule_active' => (bool) Configuration::get('MYMODULE_ACTIVE'),
'shop_name' => Configuration::get('PS_SHOP_NAME'),
]);
return $this->display(__FILE__, 'views/templates/hook/footer.tpl');
}
#Creating Database Tables
Use Db::getInstance()->execute() inside install() to create your custom tables. Always prefix the table name with _DB_PREFIX_ and use IF NOT EXISTS to avoid errors on re-install. Drop the table on uninstall.
<?php
private function createTable()
{
$sql = '
CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'mymodule_entry` (
`id_entry` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`id_shop` INT(11) UNSIGNED NOT NULL DEFAULT 1,
`label` VARCHAR(255) NOT NULL,
`content` TEXT,
`active` TINYINT(1) NOT NULL DEFAULT 1,
`date_add` DATETIME NOT NULL,
`date_upd` DATETIME NOT NULL,
PRIMARY KEY (`id_entry`),
KEY `id_shop` (`id_shop`)
) ENGINE=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8mb4;
';
return Db::getInstance()->execute($sql);
}
private function dropTable()
{
return Db::getInstance()->execute(
'DROP TABLE IF EXISTS `' . _DB_PREFIX_ . 'mymodule_entry`'
);
}
#getContent() – Configuration Page
Defining getContent() makes the Configure button visible in the module list. The method should handle form submission (POST) and then render the form. Always redirect after a successful POST (Tools::redirectAdmin) to avoid double-submission.
<?php
public function getContent()
{
$output = '';
if (Tools::isSubmit('submit_mymodule')) {
$active = (int) Tools::getValue('MYMODULE_ACTIVE');
Configuration::updateValue('MYMODULE_ACTIVE', $active);
$output .= $this->displayConfirmation($this->l('Settings saved.'));
}
return $output . $this->renderForm();
}
private function renderForm()
{
$helper = new HelperForm();
$helper->module = $this;
$helper->name_controller = $this->name;
$helper->identifier = $this->identifier;
$helper->token = Tools::getAdminTokenLite('AdminModules');
$helper->currentIndex = AdminController::$currentIndex . '&configure=' . $this->name;
$helper->default_form_language = (int) Configuration::get('PS_LANG_DEFAULT');
$helper->allow_employee_form_lang = Configuration::get('PS_BO_ALLOW_EMPLOYEE_FORM_LANG');
$helper->title = $this->displayName;
$helper->submit_action = 'submit_mymodule';
$helper->fields_value['MYMODULE_ACTIVE'] = Configuration::get('MYMODULE_ACTIVE');
$form = [[
'form' => [
'legend' => ['title' => $this->l('Settings'), 'icon' => 'icon-cogs'],
'input' => [
[
'type' => 'switch',
'label' => $this->l('Enable module'),
'name' => 'MYMODULE_ACTIVE',
'values' => [
['id' => 'active_on', 'value' => 1, 'label' => $this->l('Yes')],
['id' => 'active_off', 'value' => 0, 'label' => $this->l('No')],
],
],
],
'submit' => ['title' => $this->l('Save'), 'class' => 'btn btn-default pull-right'],
],
]];
return $helper->generateForm($form);
}
#Tabs (Back-Office Menu Items)
Declare the $tabs property to automatically register back-office menu entries when the module is installed. Each entry maps a controller class name to a label and parent tab.
<?php
// Inside the class, at the property level:
public $tabs = [
[
'name' => ['en' => 'My Module', 'es' => 'Mi Módulo'],
'class_name' => 'AdminMyModule',
'parent_class_name' => 'AdminCatalog',
'visible' => true,
'icon' => 'settings_applications',
],
];
// PrestaShop 1.7.7+ also supports route-based tabs:
// 'route_name' => 'admin_mymodule_index',
#Complete Working Example
<?php
if (!defined('_PS_VERSION_')) {
exit;
}
class MyModule extends Module
{
public $tabs = [
[
'name' => ['en' => 'My Module', 'es' => 'Mi Módulo'],
'class_name' => 'AdminMyModule',
'parent_class_name' => 'AdminCatalog',
'visible' => true,
],
];
public function __construct()
{
$this->name = 'mymodule';
$this->tab = 'front_office_features';
$this->version = '1.0.0';
$this->author = 'My Company';
$this->need_instance = 0;
$this->ps_versions_compliancy = ['min' => '1.7.6.0', 'max' => _PS_VERSION_];
$this->bootstrap = true;
parent::__construct();
$this->displayName = $this->l('My Module');
$this->description = $this->l('Demo module for PrestaShop developers.');
$this->confirmUninstall = $this->l('Delete all module data?');
}
public function install()
{
return parent::install()
&& $this->registerHook('displayHeader')
&& $this->registerHook('displayFooter')
&& $this->createTable()
&& Configuration::updateValue('MYMODULE_ACTIVE', 1)
&& Configuration::updateValue('MYMODULE_TEXT', '');
}
public function uninstall()
{
return parent::uninstall()
&& $this->dropTable()
&& Configuration::deleteByName('MYMODULE_ACTIVE')
&& Configuration::deleteByName('MYMODULE_TEXT');
}
private function createTable()
{
return Db::getInstance()->execute('
CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'mymodule_entry` (
`id_entry` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`id_shop` INT(11) UNSIGNED NOT NULL DEFAULT 1,
`label` VARCHAR(255) NOT NULL,
`active` TINYINT(1) NOT NULL DEFAULT 1,
`date_add` DATETIME NOT NULL,
`date_upd` DATETIME NOT NULL,
PRIMARY KEY (`id_entry`)
) ENGINE=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8mb4
');
}
private function dropTable()
{
return Db::getInstance()->execute(
'DROP TABLE IF EXISTS `' . _DB_PREFIX_ . 'mymodule_entry`'
);
}
public function getContent()
{
$output = '';
if (Tools::isSubmit('submit_mymodule')) {
Configuration::updateValue('MYMODULE_ACTIVE', (int) Tools::getValue('MYMODULE_ACTIVE'));
Configuration::updateValue('MYMODULE_TEXT', pSQL(Tools::getValue('MYMODULE_TEXT')));
$output .= $this->displayConfirmation($this->l('Settings saved.'));
}
return $output . $this->renderForm();
}
private function renderForm()
{
$helper = new HelperForm();
$helper->module = $this;
$helper->token = Tools::getAdminTokenLite('AdminModules');
$helper->currentIndex = AdminController::$currentIndex . '&configure=' . $this->name;
$helper->submit_action = 'submit_mymodule';
$helper->fields_value = [
'MYMODULE_ACTIVE' => Configuration::get('MYMODULE_ACTIVE'),
'MYMODULE_TEXT' => Configuration::get('MYMODULE_TEXT'),
];
return $helper->generateForm([[
'form' => [
'legend' => ['title' => $this->l('Settings')],
'input' => [
['type' => 'switch', 'label' => $this->l('Active'), 'name' => 'MYMODULE_ACTIVE',
'values' => [['id'=>'on','value'=>1,'label'=>$this->l('Yes')],['id'=>'off','value'=>0,'label'=>$this->l('No')]]],
['type' => 'text', 'label' => $this->l('Custom text'), 'name' => 'MYMODULE_TEXT'],
],
'submit' => ['title' => $this->l('Save')],
],
]]);
}
public function hookDisplayHeader($params)
{
if (!Configuration::get('MYMODULE_ACTIVE')) {
return;
}
$this->context->controller->addCSS($this->_path . 'views/css/mymodule.css');
}
public function hookDisplayFooter($params)
{
if (!Configuration::get('MYMODULE_ACTIVE')) {
return '';
}
$this->smarty->assign('mymodule_text', Configuration::get('MYMODULE_TEXT'));
return $this->display(__FILE__, 'views/templates/hook/footer.tpl');
}
}
Place your custom classes under mymodule/classes/ and add require_once dirname(__FILE__).'/classes/MyClass.php'; at the top of the main file, or register a PSR-4 autoloader in composer.json so PrestaShop's autoloader picks them up automatically.
- Security header —
if (!defined('_PS_VERSION_')) { exit; }at the top of every PHP file - $name matches folder — the class name, file name and folder name must all be identical (case-sensitive on Linux)
- parent::__construct() before displayName —
$this->l()is not available until after the parent call - install() chains with && — return false at the first failure to trigger rollback
- pSQL() on every user input — sanitise values before writing to the database
- deleteByName in uninstall() — clean up all Configuration entries to leave no orphan data