PrestaShop Module Development
When to use
Use this skill for PrestaShop module development tasks such as:
- Creating new modules with modern architecture (Symfony controllers, services, entities)
- Refactoring legacy modules to use modern PrestaShop patterns
- Implementing hooks, actions, and event listeners
- Adding configuration pages (modern Symfony-form approach)
- Creating front office features and widgets
- Setting up database entities and migrations
- Implementing security measures (CSRF, input validation, SQL injection prevention)
- Adding multilingual support and translations
- Converting legacy code patterns (HelperForm, jQuery UI sortable, ObjectModel) to modern equivalents
- Building list pages with the PrestaShop Grid system (filters, pagination, toggle, drag-and-drop position)
Inputs required
- PrestaShop version (target 8.x/9.x for modern development)
- Module scope and functionality requirements
- Existing module path (if updating legacy code)
- Database schema requirements (if applicable)
- Front office integration needs (hooks, widgets, pages)
- Configuration requirements (settings, admin interface)
- Multilingual requirements and supported languages
Procedure
0) Project structure & namespace naming
Read: references/module-structure.md
Key rules:
- Derive PSR-4 namespace from the module name prefix — never use
PrestaShop\Module\
- Use the PrestaShop Module Generator to scaffold new modules
1) Main module class & installer
Read: references/module-class-and-installer.md
Key rules:
- Always
require_once __DIR__ . '/vendor/autoload.php'; after the _PS_VERSION_ guard
- Never put hook registration, DB queries, or
Configuration:: calls directly in install() — delegate to src/Install/Installer.php
- Do NOT add
getTabs() to the main module class — manage tabs entirely via Installer::installTabs() / uninstallTabs()
- Default parent tab is
Adminwswebsenso (shared Websenso group); check existence before creating it
getContent() must only redirect to the Symfony route, never render HTML
- No SQL in the main module class — all database access (including in hooks like
hookActionShopDataDuplication and widget methods like getWidgetVariables) must be delegated to the Repository or Manager class via $this->get('service.id')
- Always guard service access — use
$this->has() + null check in admin context; use try/catch + instanceof in front-office context (stale container can make has() return true while get() still throws). See references/module-class-and-installer.md → Guard patterns section.
2) Modern configuration pages
Read: references/configuration-page.md
Key rules:
- Do NOT use
HelperForm — use Symfony form components + FrameworkBundleAdminController
- Four classes:
DataConfiguration, FormDataProvider, FormType, Controller
- Wire everything in
config/services.yml and config/routes.yml
3) Database operations & entities
Read: references/database-and-entities.md
For translatable entities with Grid: read references/entity-doctrine.md
- Always use Doctrine ORM (Entity + LangEntity + Repository + Manager) for any entity that has a Grid list page or translatable fields
- ObjectModel is legacy — do not use in new or modernised modules
- Entity class name = table name without
_DB_PREFIX_ — PS adds the prefix globally; use @ORM\Table() with no name= parameter
- All table names must start with
ws_ — e.g. ws_mymodule_items, ws_mymodule_items_lang — to group Websenso tables together
- Do NOT create
MetadataListener or Doctrine event listeners for table naming
- Always sanitize raw DBAL SQL: cast IDs with
(int), use bound parameters
- No raw SQL (
Db::getInstance(), pSQL(), _DB_PREFIX_ string concatenation) outside Repository and Manager classes. This applies everywhere: main module class, Installer, FixturesInstaller, hooks, widget methods. The only exception is Installer SQL schema queries (CREATE TABLE, DROP TABLE) which have no Repository equivalent.
- FixturesInstaller must NOT call module-own services — the module's services are not in the container at install time. Instantiate Manager directly using core Doctrine services. See
references/module-class-and-installer.md → FixturesInstaller — service resolution section.
Services split & components architecture
Read: references/services-split.md
Key rules:
- Repository services only in
config/common.yml (Doctrine-level, no PrestaShopBundle deps)
- All
PrestaShopBundle-dependent services go in config/admin/services.yml
- Always split into component sub-folders under
config/components/ — never one flat services.yml
4) Security (mandatory)
Read: references/security.md
- CSRF: handled by Symfony forms automatically; validate manually for raw AJAX endpoints
- SQL injection:
(int) + pSQL() on every value, or use DbQuery builder
- File uploads: pass full
$_FILES-compatible array (including type, size, error) to ImageManager::validateUpload()
5) Hooks & front office integration
Read: references/hooks-and-front-office.md
- Register hooks in
Installer, not in install() directly
- Load assets only for the relevant controller in
hookDisplayBackOfficeHeader
- Implement
WidgetInterface for front office widgets
6) Translations
Read: references/translations.md
- Use
$this->trans('Text', [], 'Modules.Mymodule.Admin') in PHP
- Use
'Text'|trans({}, 'Modules.Mymodule.Admin') in Twig
- Declare
isUsingNewTranslationSystem(): true in the module class
7) Legacy code conversion
Read: references/legacy-conversion.md
Common conversions: HelperForm → Symfony form, jQuery UI sortable → Grid PositionColumn, ModuleAdminController → FrameworkBundleAdminController.
8) Services & dependency injection
Read: references/services-and-di.md
CRITICAL: Never use legacy static calls in services/controllers:
- ❌
Context::getContext() — inject $context: "@=service('prestashop.adapter.legacy.context').getContext()" instead
- ❌
Configuration::get() / updateValue() — inject @prestashop.adapter.legacy.configuration instead
- ❌
Context::getContext()->getTranslator() — inject @translator instead
Key rules:
- Define services in
config/services.yml
- Use
$this->get('service.id') in Symfony controllers
- Use Expression Language (
@=) for computed constructor arguments (context, language ID, shop ID)
- Always inject dependencies via constructor, never use static accessors
9) Grid system (list pages with drag-and-drop position)
Read: references/grid-system.md
Full pattern for building CRUD list pages with the PS Grid system:
GridDefinitionFactory — columns (PositionColumn, ToggleColumn, ActionColumn), filters, row actions
QueryBuilder — Doctrine DBAL query with sorting, pagination, and filters
Filters — default sort/limit settings
- 5 service definitions in
services.yml (factory, query, data, grid, position)
- 4 routes in
routes.yml (index, search, toggle, update-position)
- 4 controller actions (
indexAction, searchAction, toggleStatus, updatePositionAction)
- Pre-built JS bundle (copied from
ws-entity-grid-skeleton, grid ID replaced via sed)
Verification
- Module installs without PHP errors:
php bin/console pr:mo install mymodule
- Configuration saves correctly with proper validation
- Front office features display and function properly
- Translations work in all configured languages
Validation
AI agent rule — NEVER SKIP EITHER STEP. Read references/validation.md for full instructions.
Step 1 — lotr (run from the module root)
vendor/websenso/prestashop-module-devtools/bin/lotr
Expected: 🎉 All commands completed successfully! Executed: 6/6
Step 2 — Install test (run from the PS root)
php bin/console pr:mo install mymodule
Expected: L'action Install sur le module … a réussi.
Failure modes / debugging
Read: references/debugging.md
Common failure areas:
references/debugging.md — all symptom/cause/fix tables (install, config page, Grid, InputBag, ImageManager, lotr steps)
Escalation
1---2name: prestashop-module-development3description: Complete PrestaShop module development workflow using modern architecture and best practices. Use when: creating new PrestaShop modules, updating legacy modules to modern code, implementing hooks and actions, setting up module configuration pages, adding front office features, handling database operations, implementing security measures, managing translations, or modernizing existing PrestaShop modules from legacy patterns to current standards.4---5
6# PrestaShop Module Development
7
8## When to use
9
10Use this skill for PrestaShop module development tasks such as:
11
12- Creating new modules with modern architecture (Symfony controllers, services, entities)
13- Refactoring legacy modules to use modern PrestaShop patterns
14- Implementing hooks, actions, and event listeners
15- Adding configuration pages (modern Symfony-form approach)
16- Creating front office features and widgets
17- Setting up database entities and migrations
18- Implementing security measures (CSRF, input validation, SQL injection prevention)
19- Adding multilingual support and translations
20- Converting legacy code patterns (HelperForm, jQuery UI sortable, ObjectModel) to modern equivalents
21- Building list pages with the PrestaShop Grid system (filters, pagination, toggle, drag-and-drop position)
22
23## Inputs required
24
25- PrestaShop version (target 8.x/9.x for modern development)
26- Module scope and functionality requirements
27- Existing module path (if updating legacy code)
28- Database schema requirements (if applicable)
29- Front office integration needs (hooks, widgets, pages)
30- Configuration requirements (settings, admin interface)
31- Multilingual requirements and supported languages
32
33## Procedure
34
35### 0) Project structure & namespace naming
36
37Read: `references/module-structure.md`
38
39Key rules:
40- Derive PSR-4 namespace from the module name prefix — never use `PrestaShop\Module\`
41- Use the [PrestaShop Module Generator](https://validator.prestashop.com/generator) to scaffold new modules
42
43### 1) Main module class & installer
44
45Read: `references/module-class-and-installer.md`
46
47Key rules:
48- Always `require_once __DIR__ . '/vendor/autoload.php';` after the `_PS_VERSION_` guard
49- Never put hook registration, DB queries, or `Configuration::` calls directly in `install()` — delegate to `src/Install/Installer.php`
50- **Do NOT add `getTabs()`** to the main module class — manage tabs entirely via `Installer::installTabs()` / `uninstallTabs()`
51- Default parent tab is `Adminwswebsenso` (shared Websenso group); check existence before creating it
52- `getContent()` must only redirect to the Symfony route, never render HTML
53- **No SQL in the main module class** — all database access (including in hooks like `hookActionShopDataDuplication` and widget methods like `getWidgetVariables`) must be delegated to the Repository or Manager class via `$this->get('service.id')`
54- **Always guard service access** — use `$this->has()` + null check in admin context; use try/catch + `instanceof` in front-office context (stale container can make `has()` return `true` while `get()` still throws). See `references/module-class-and-installer.md` → *Guard patterns* section.
55
56### 2) Modern configuration pages
57
58Read: `references/configuration-page.md`
59
60Key rules:
61- **Do NOT use `HelperForm`** — use Symfony form components + `FrameworkBundleAdminController`
62- Four classes: `DataConfiguration`, `FormDataProvider`, `FormType`, `Controller`
63- Wire everything in `config/services.yml` and `config/routes.yml`
64
65### 3) Database operations & entities
66
67Read: `references/database-and-entities.md`
68For translatable entities with Grid: read `references/entity-doctrine.md`
69
70- **Always use Doctrine ORM** (Entity + LangEntity + Repository + Manager) for any entity that has a Grid list page or translatable fields
71- ObjectModel is legacy — do not use in new or modernised modules
72- **Entity class name = table name without `_DB_PREFIX_`** — PS adds the prefix globally; use `@ORM\Table()` with no `name=` parameter
73- **All table names must start with `ws_`** — e.g. `ws_mymodule_items`, `ws_mymodule_items_lang` — to group Websenso tables together
74- Do NOT create `MetadataListener` or Doctrine event listeners for table naming
75- Always sanitize raw DBAL SQL: cast IDs with `(int)`, use bound parameters
76- **No raw SQL (`Db::getInstance()`, `pSQL()`, `_DB_PREFIX_` string concatenation) outside Repository and Manager classes.** This applies everywhere: main module class, Installer, FixturesInstaller, hooks, widget methods. The only exception is `Installer` SQL schema queries (`CREATE TABLE`, `DROP TABLE`) which have no Repository equivalent.
77- **FixturesInstaller must NOT call module-own services** — the module's services are not in the container at install time. Instantiate Manager directly using core Doctrine services. See `references/module-class-and-installer.md` → *FixturesInstaller — service resolution* section.
78
79### Services split & components architecture
80
81Read: `references/services-split.md`
82
83Key rules:
84- Repository services only in `config/common.yml` (Doctrine-level, no `PrestaShopBundle` deps)
85- All `PrestaShopBundle`-dependent services go in `config/admin/services.yml`
86- Always split into component sub-folders under `config/components/` — never one flat `services.yml`
87
88### 4) Security (mandatory)
89
90Read: `references/security.md`
91
92- CSRF: handled by Symfony forms automatically; validate manually for raw AJAX endpoints
93- SQL injection: `(int)` + `pSQL()` on every value, or use `DbQuery` builder
94- File uploads: pass full `$_FILES`-compatible array (including `type`, `size`, `error`) to `ImageManager::validateUpload()`
95
96### 5) Hooks & front office integration
97
98Read: `references/hooks-and-front-office.md`
99
100- Register hooks in `Installer`, not in `install()` directly
101- Load assets only for the relevant controller in `hookDisplayBackOfficeHeader`
102- Implement `WidgetInterface` for front office widgets
103
104### 6) Translations
105
106Read: `references/translations.md`
107
108- Use `$this->trans('Text', [], 'Modules.Mymodule.Admin')` in PHP
109- Use `'Text'|trans({}, 'Modules.Mymodule.Admin')` in Twig
110- Declare `isUsingNewTranslationSystem(): true` in the module class
111
112### 7) Legacy code conversion
113
114Read: `references/legacy-conversion.md`
115
116Common conversions: HelperForm → Symfony form, jQuery UI sortable → Grid PositionColumn, ModuleAdminController → FrameworkBundleAdminController.
117
118### 8) Services & dependency injection
119
120Read: `references/services-and-di.md`
121
122**CRITICAL**: Never use legacy static calls in services/controllers:
123- ❌ `Context::getContext()` — inject `$context: "@=service('prestashop.adapter.legacy.context').getContext()"` instead
124- ❌ `Configuration::get()` / `updateValue()` — inject `@prestashop.adapter.legacy.configuration` instead
125- ❌ `Context::getContext()->getTranslator()` — inject `@translator` instead
126
127Key rules:
128- Define services in `config/services.yml`
129- Use `$this->get('service.id')` in Symfony controllers
130- Use Expression Language (`@=`) for computed constructor arguments (context, language ID, shop ID)
131- Always inject dependencies via constructor, never use static accessors
132
133### 9) Grid system (list pages with drag-and-drop position)
134
135Read: `references/grid-system.md`
136
137Full pattern for building CRUD list pages with the PS Grid system:
138- `GridDefinitionFactory` — columns (`PositionColumn`, `ToggleColumn`, `ActionColumn`), filters, row actions
139- `QueryBuilder` — Doctrine DBAL query with sorting, pagination, and filters
140- `Filters` — default sort/limit settings
141- 5 service definitions in `services.yml` (factory, query, data, grid, position)
142- 4 routes in `routes.yml` (index, search, toggle, update-position)
143- 4 controller actions (`indexAction`, `searchAction`, `toggleStatus`, `updatePositionAction`)
144- Pre-built JS bundle (copied from `ws-entity-grid-skeleton`, grid ID replaced via `sed`)
145
146## Verification
147
148- Module installs without PHP errors: `php bin/console pr:mo install mymodule`
149- Configuration saves correctly with proper validation
150- Front office features display and function properly
151- Translations work in all configured languages
152
153## Validation
154
155> **AI agent rule — NEVER SKIP EITHER STEP**. Read `references/validation.md` for full instructions.
156
157### Step 1 — lotr (run from the module root)
158
159```bash
160vendor/websenso/prestashop-module-devtools/bin/lotr
161```
162
163Expected: `🎉 All commands completed successfully! Executed: 6/6`
164
165### Step 2 — Install test (run from the PS root)
166
167```bash
168php bin/console pr:mo install mymodule
169```
170
171Expected: `L'action Install sur le module … a réussi.`
172
173## Failure modes / debugging
174
175Read: `references/debugging.md`
176
177Common failure areas:
178- `references/debugging.md` — all symptom/cause/fix tables (install, config page, Grid, InputBag, ImageManager, lotr steps)
179
180## Escalation
181
182- [PrestaShop 9 Module Creation](https://devdocs.prestashop-project.org/9/modules/creation/)
183- [Module Good Practices](https://devdocs.prestashop-project.org/9/modules/creation/good-practices/)
184- [Official Example Modules](https://github.com/PrestaShop/example-modules)
185- [demosymfonyform](https://github.com/PrestaShop/example-modules/tree/master/demosymfonyform) — canonical Symfony form config page
186- [Module Validator](https://validator.prestashop.com/)
187- [PrestaShop Developer Slack](https://www.prestashop-project.org/slack/)