prestashop-module-development
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 ininstall()— delegate tosrc/Install/Installer.php - Do NOT add
getTabs()to the main module class — manage tabs entirely viaInstaller::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
hookActionShopDataDuplicationand widget methods likegetWidgetVariables) 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 +instanceofin front-office context (stale container can makehas()returntruewhileget()still throws). Seereferences/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.ymlandconfig/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 noname=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
MetadataListeneror 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 isInstallerSQL 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, noPrestaShopBundledeps) - All
PrestaShopBundle-dependent services go inconfig/admin/services.yml - Always split into component sub-folders under
config/components/— never one flatservices.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 useDbQuerybuilder - File uploads: pass full
$_FILES-compatible array (includingtype,size,error) toImageManager::validateUpload()
5) Hooks & front office integration
Read: references/hooks-and-front-office.md
- Register hooks in
Installer, not ininstall()directly - Load assets only for the relevant controller in
hookDisplayBackOfficeHeader - Implement
WidgetInterfacefor 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(): truein 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.configurationinstead - ❌
Context::getContext()->getTranslator()— inject@translatorinstead
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 actionsQueryBuilder— Doctrine DBAL query with sorting, pagination, and filtersFilters— 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 viased)
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.mdfor 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
- PrestaShop 9 Module Creation
- Module Good Practices
- Official Example Modules
- demosymfonyform — canonical Symfony form config page
- Module Validator
- PrestaShop Developer Slack