How to Build a PrestaShop 9 Module: From Skeleton to Production
A PrestaShop module can be extremely small.
At the most basic level, the module directory contains a main PHP file with the same name as the directory.
But a production module often needs much more:
- services;
- hooks;
- database tables;
- Symfony controllers;
- CLI commands;
- configuration;
- logs;
- upgrade scripts;
- security checks;
- tests.
PrestaShop 9 uses Symfony 6.4 and continues the platform's migration toward modern Symfony architecture. That makes clean service-oriented module design more important than ever.
This guide walks through the architecture of a serious PrestaShop 9 module rather than stopping at “Hello World.”
Version note: This guide was verified on September 22, 2026 against the current PrestaShop 9 developer documentation.
1. Define the Module Responsibility
Before creating files, define what the module owns.
A good module has a clear business purpose.
Examples:
stock filtering
supplier import
security monitoring
upsell rules
order export
Avoid a module that slowly becomes responsible for unrelated features.
If one module contains:
- SEO;
- stock;
- security;
- shipping;
- analytics;
- import;
it becomes difficult to maintain and test.
2. Choose the Technical Name
PrestaShop's module tutorial requires a technical module name using lowercase alphanumeric characters.
For example:
mymodule
The main module file must have the same name:
/modules/mymodule/mymodule.php
The official documentation discourages underscores in technical names because they do not work well with translation domains.
3. Start with a Clean Folder Structure
A production-oriented structure may look like:
mymodule/
├── mymodule.php
├── composer.json
├── config/
│ ├── services.yml
│ └── routes.yml
├── src/
│ ├── Controller/
│ ├── Service/
│ ├── Repository/
│ ├── Command/
│ ├── Domain/
│ └── ApiPlatform/
├── views/
│ ├── templates/
│ ├── css/
│ └── js/
├── sql/
│ ├── install.php
│ └── uninstall.php
├── upgrade/
└── translations/
You do not need every folder immediately.
Add structure when responsibility appears.
4. Keep the Main Module Class Small
The main module class should not contain all business logic.
It should coordinate things such as:
- metadata;
- installation;
- hook registration;
- configuration entry points.
Avoid writing:
2,000 lines
inside:
class MyModule extends Module
Move application logic into services.
5. Basic Main Module Class
A minimal module begins with something like:
<?php
if (!defined('_PS_VERSION_')) {
exit;
}
class MyModule extends Module
{
public function __construct()
{
$this->name = 'mymodule';
$this->version = '1.0.0';
$this->author = 'Your Company';
$this->bootstrap = true;
parent::__construct();
$this->displayName = $this->l('My Module');
$this->description = $this->l('Example PrestaShop 9 module.');
}
}
The real production class will usually include more metadata and install logic.
6. Design Installation Before Features
Decide what installation creates.
Possible resources include:
- hooks;
- configuration keys;
- database tables;
- tabs;
- permissions.
Installation should fail cleanly.
For example:
public function install()
{
return parent::install()
&& $this->registerHook('displayHeader')
&& $this->installDatabase();
}
If database creation fails, do not leave the module half-installed without reporting the problem.
7. Register Hooks Intentionally
Hooks let modules respond to PrestaShop events or display content.
PrestaShop convention distinguishes:
actionSomething
from:
displaySomething
Action hooks respond to events.
Display hooks render content.
Register hooks during installation:
$this->registerHook('displayHeader');
Then implement:
public function hookDisplayHeader(array $params)
{
// logic
}
8. Avoid Heavy Work in Global Hooks
A hook may run on every page.
Do not place:
- supplier API requests;
- huge database queries;
- full catalog scans;
inside a global display hook.
A display hook should ideally render already prepared data.
If data must be refreshed, use:
- cache;
- cron;
- queue;
- background worker.
9. Create Services for Business Logic
A service class should represent an application responsibility.
Example:
ProductSyncService
IntegrityScanner
UpsellRuleEvaluator
StockFilterService
The controller should call the service.
The service should not depend on HTTP unless the responsibility requires it.
This makes the code reusable from:
- Back Office;
- CLI;
- cron;
- API.
10. Use Dependency Injection
PrestaShop 9's Symfony architecture supports dependency injection.
Instead of:
$repo = new ProductRepository();
$logger = new Logger();
inside every method, inject dependencies.
Conceptually:
public function __construct(
ProductRepository $repository,
LoggerInterface $logger
) {
$this->repository = $repository;
$this->logger = $logger;
}
Benefits include:
- testability;
- clear dependencies;
- easier replacement;
- container validation.
11. Configure Services
A typical module service file might use:
services:
_defaults:
public: false
autowire: true
autoconfigure: true
Vendor\Module\:
resource: '../src/'
Adjust the resource boundaries carefully.
Do not automatically register classes that should not be services.
12. PrestaShop 9 Controllers Must Be Services
The current PrestaShop 9 documentation explicitly notes that modern controllers must be defined as services.
A controller can be configured with:
services:
Vendor\Module\Controller\Admin\ExampleController:
autowire: true
autoconfigure: true
tags:
- { name: controller.service_arguments }
or through service defaults.
If the controller is not registered correctly, it will fail.
13. Keep Controllers Thin
A controller should:
- accept request;
- authorize;
- validate input;
- call service;
- return response.
Do not write raw SQL and import logic directly inside controller methods.
Thin controllers make the module easier to:
- test;
- reuse;
- maintain.
14. Use Routes for Modern Pages
Modern Back Office pages should use Symfony routing.
A route file may look conceptually like:
mymodule_dashboard:
path: /mymodule/dashboard
methods: [GET]
defaults:
_controller: 'Vendor\Module\Controller\Admin\DashboardController::index'
Use route names consistently.
Avoid collisions with other modules.
15. Build Configuration Pages with Scope in Mind
A simple module may store settings in PrestaShop Configuration.
But decide whether values are:
- global;
- shop group;
- shop specific.
Multistore bugs often come from ignoring scope.
A setting such as:
supplier API URL
may be different for each shop.
Another setting such as:
global license server
may be shared.
Define the business meaning first.
16. Design Database Tables for Scale
If the module needs persistence, design tables around real access patterns.
For a queue table:
CREATE TABLE ps_mymodule_queue (
id_queue BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
id_shop INT UNSIGNED NOT NULL,
status VARCHAR(32) NOT NULL,
payload LONGTEXT NULL,
attempts INT UNSIGNED NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
PRIMARY KEY (id_queue)
);
Then ask which queries will run.
If workers frequently filter by:
id_shop + status
add an appropriate index.
17. Avoid Raw User Input in SQL
Never concatenate untrusted values into SQL.
Bad:
$sql = "SELECT * FROM table WHERE name = '" . $_GET['name'] . "'";
Use PrestaShop database helpers and safe escaping or parameterized DBAL queries where appropriate.
Supplier data is also untrusted.
A remote XML feed should not be treated as safe just because it comes from a business partner.
18. Add Upgrade Scripts for Schema Changes
A production module evolves.
When the schema changes, add versioned upgrade logic.
Example:
upgrade/upgrade-1.1.0.php
The upgrade should:
- make one coherent change;
- return failure clearly;
- be safe to rerun where possible.
Do not hide database migrations inside random runtime code.
19. Separate Code Version from Data Migration Logic
A bug fix that changes only PHP does not necessarily require database migration.
A schema change does.
Examples requiring migration:
- new table;
- new column;
- new index;
- new required configuration key.
Keep versioning meaningful.
20. Add CLI Commands for Heavy Work
PrestaShop uses the Symfony Console component.
Module developers can add custom commands.
Use CLI for:
- imports;
- exports;
- integrity scans;
- cleanup;
- image processing;
- queue workers.
A command can be scheduled via cron.
This is much safer than requiring an administrator to keep a browser tab open.
21. Return Useful Exit Codes
CLI commands should return:
0 = success
non-zero = failure
This allows:
- cron;
- monitoring;
- CI/CD;
to detect errors.
Do not print “failed” and still return success.
22. Add Locks to Long Commands
Cron can overlap.
If the command runs every minute but takes three minutes, duplicate workers may appear.
Use:
- database lease;
- Symfony lock;
- Redis lock;
- filesystem lock.
The lock should be recoverable after crashes.
23. Make Batch Jobs Resumable
Do not process millions of records as one opaque operation.
Store:
cursor
processed
failed
retry
Then a crash does not force the job to restart from zero.
24. Log Structured Context
Logs should answer:
- which shop?
- which product?
- which job?
- which external ID?
- which error?
Example:
[run=203]
[shop=2]
[product=119019]
Supplier category mapping missing
This is much more useful than:
Something went wrong
25. Do Not Log Secrets
Never write:
- API token;
- password;
- OAuth client secret;
- payment data;
to logs.
Sanitize exception context before storing it.
26. Add API Resources When Needed
PrestaShop 9 modules can expose resources through the new Admin API.
The convention is:
src/ApiPlatform/Resources
Use this when the module needs structured external integration.
Do not create a custom public PHP endpoint simply because it is faster to prototype.
27. Define Narrow API Scopes
If your module exposes:
run security scan
read scan report
create separate scopes such as:
security_scan_run
security_report_read
Do not require unrelated core permissions.
28. Validate File Uploads
If the module imports files, validate:
- size;
- extension;
- MIME type;
- parser success.
A filename ending in .xml does not prove that the file contains valid XML.
A .jpg URL can return HTML.
Validate actual content.
29. Protect Remote Downloads
Remote URLs can create server-side request forgery risks.
If administrators or suppliers provide URLs, restrict dangerous destinations.
Do not allow a module to fetch arbitrary internal network addresses without controls.
Use:
- URL validation;
- scheme allowlists;
- timeout;
- response-size limits.
30. Handle Multistore Explicitly
Every query should ask:
Is this record global or shop-specific?
If shop-specific, include id_shop in:
- table;
- index;
- unique constraints;
- cache keys.
Example:
UNIQUE(id_shop, supplier_key)
may be correct where:
UNIQUE(supplier_key)
would incorrectly block another store.
31. Test Shop Context in Cron
CLI jobs do not have a human-selected Back Office shop context.
Set or pass shop context explicitly.
Do not rely on whichever shop was last selected by an employee.
32. Build Clean Error Responses
Ajax or API calls should return structured errors.
Example:
{
"success": false,
"code": "CATEGORY_MAPPING_MISSING",
"message": "Supplier category 341 is not mapped."
}
This is easier to handle than a raw Symfony exception page.
33. Test Installation from the Release ZIP
Development environments can hide packaging bugs.
Before release:
- create ZIP;
- install it on a clean PrestaShop 9 instance;
- configure it;
- run critical features.
This catches:
- missing Composer files;
- wrong filename case;
- missing assets;
- ignored build files.
34. Test Upgrades from Previous Versions
A fresh install is not enough.
Install:
v1.0.0
with realistic data.
Then upgrade to:
v1.1.0
Verify:
- schema;
- configuration;
- business data;
- hooks.
Many production bugs exist only in upgrade paths.
35. Add Static Analysis
Use tools such as:
- PHPStan;
- PHP CS Fixer;
- coding standards checks.
Static analysis catches:
- wrong argument types;
- missing classes;
- impossible branches;
- nullable-value problems.
This is especially valuable when supporting multiple PHP versions.
36. Add Automated Tests
Not everything needs a full browser test.
Use:
- unit tests for services;
- repository tests for persistence;
- integration tests for install/upgrade;
- functional tests for controllers.
Focus first on workflows that can damage business data.
Examples:
- order export;
- stock update;
- product creation.
37. Support Only What You Actually Test
Do not declare:
PrestaShop 1.7–9.x
unless you genuinely test that range.
A narrower compatibility promise that works is better than a broad one that generates support tickets.
38. Document Operational Requirements
If your module requires:
- cron;
- CLI;
- writable directory;
- API key;
- PHP extension;
say so clearly.
Administrators should not discover operational requirements from errors.
39. Plan Uninstall Behavior
Decide what uninstall means.
Should it delete:
- configuration?
- queue history?
- audit records?
- business mappings?
For important data, consider offering a separate “delete data” option rather than automatically destroying everything.
40. Production-Ready Module Checklist
Architecture
- main module class small
- business logic in services
- repositories separated
- controllers thin
- dependencies injected
Database
- install schema
- useful indexes
- upgrade migrations
- multistore constraints
Background work
- CLI command
- locking
- batching
- retry state
- progress tracking
Security
- authorization
- CSRF where appropriate
- safe SQL
- file validation
- secret sanitization
Release
- clean ZIP install tested
- upgrade tested
- static analysis
- critical workflow tests
- uninstall tested
Frequently Asked Questions
What is the minimum structure of a PrestaShop module?
A module needs a folder and a main PHP file with the same technical name. Production modules usually add services, routes, controllers and other structure.
Can the technical name contain underscores?
The official PrestaShop 9 tutorial discourages underscores because they do not work correctly with translation domains.
Are Symfony controllers services in PrestaShop 9?
Yes. Current documentation states that PrestaShop 9 controllers must be configured as services.
Should business logic live in the main module class?
Prefer services for substantial business logic. Keep the main module class focused on module lifecycle and integration points.
Can a module add console commands?
Yes. PrestaShop uses Symfony Console, and modules can provide custom commands.
Can modules extend the Admin API?
Yes. PrestaShop 9 can automatically load module API resources from src/ApiPlatform/Resources when the module is installed and enabled.
Should long imports run in a controller?
No. Use CLI, cron or queue workers with batching and progress tracking.
What should I test before selling a module?
At minimum: clean install, configuration, critical features, upgrade, multistore behavior where applicable, packaging and uninstall.