name: prestashop-module-development description: "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, creating cart rules and vouchers, building Symfony console commands, or modernizing existing PrestaShop modules from legacy patterns to current standards."
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)
- Creating and managing PrestaShop cart rules (vouchers, discount codes, promotional codes)
- Implementing Symfony console commands for background tasks and batch operations
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() - If using a shared company group tab, check its existence before creating it — never unconditionally create 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') - Service access: use
$this->has()+ null check in admin context; use plain$this->get()+ null check in front-office context. NEVER useContainerFinder— it is unnecessary. 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/components/sub-folders (imported byconfig/admin/services.yml) andconfig/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 - Prefix table names with a consistent company or module prefix (e.g.
prefix_mymodule_items,prefix_mymodule_items_lang) — to group tables together and avoid conflicts with PS core tables - Lang entity property types must match the DB column nullability: columns declared
NOT NULL DEFAULT ''usestring $field = ''; columns declaredNULLuse?string $field = null TranslatableTypereturnsnullfor languages the user did not fill in — in the Manager, always coerce null to''forNOT NULLstring fields:$row->setName((string) ($value ?? '')). Never pass the raw form value directly to a setter on aNOT NULLcolumn- 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 use
Db::getInstance()raw SQL —SymfonyContainer::getInstance()returnsnullin thepr:moSymfony console context (global $kernelis never set). All Doctrine ORM calls silently do nothing at install time. Seereferences/module-class-and-installer.md→ FixturesInstaller section. Db::getValue()appendsLIMIT 1internally — never writeLIMIT 1in the SQL string passed to it; causes a MariaDB syntax error.
Services split & components architecture
Read: references/services-split.md
Key rules:
- Do NOT create
config/services.yml— onlyconfig/admin/services.ymlandconfig/front/services.ymlare needed; a root-levelconfig/services.ymlis never required and should not exist config/admin/services.ymlis loaded by admin kernel only — import../common.yml+ admin components (nevercommon.ymlwithout the../prefix)- Repository services only in
config/common.ymlcomponents (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 - Only services accessed via
$this->get('service.id')needpublic: true— declare it explicitly on each such service; all other services remain private (omitpublic:). Avoid_defaults: public: trueas a blanket: it is against Symfony best practices and causes a fatal error in PrestaShop's Symfony version when combined withparent:services - Services that use
parent:and need to be public must declarepublic: trueexplicitly on the service definition —publiccannot be inherited from_defaultswhenparentis set (PrestaShop's Symfony version enforces this) - Never point
config/services.ymlatadmin/services.yml— this loads admin-only services into the front kernel, breaking container compilation and making all$this->get()calls fail silently
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
5b) Theme template injection (widget call on install)
Read: references/theme-template-injection.md
- PS8 does not support theme overrides from modules — use marker-based file patching instead
- Two-class design:
ThemeTemplateInjector(service, reusable) +ThemeTemplateInstaller(install orchestrator) - Never use
Theme::getThemes()in install context — legacy class, not autoloaded in Symfony console; usescandir(_PS_ALL_THEMES_DIR_)instead - Wrap install/uninstall calls in try/catch returning
true— theme injection must never block module install
6) Translations
Read: references/translations.md
FrameworkBundleAdminController::trans()signature istrans($id, $domain, $parameters = [])— NOT Symfony's order — always call as$this->trans('Text', 'Modules.Mymodule.Admin')(never$this->trans('Text', [], 'Domain')— passing[]as domain and a string as$parameterscauses a fatal type error)- 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/components/sub-folders (imported byconfig/admin/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
Symfony Console Commands
Read: references/services-and-di.md → Symfony Console Commands section
CRITICAL: Commands must be registered in config/admin/services.yml (NOT common.yml or front/services.yml):
# config/admin/services.yml
mymodule.command.my_command:
class: Vendor\MyModule\Command\MyCommand
arguments: ["@mymodule.service.my_service"]
tags:
- { name: console.command, command: modulename:action }
Key rules:
- Command naming:
modulename:actionformat (e.g.,wsautocartrules:create,ws_keepdblight:cleandb) - Return codes:
0for success,1for failure (NOTCommand::SUCCESS/FAILUREconstants) - Use
SymfonyStylefor rich console output (tables, progress bars, styled messages) - Add
--limitoptions for batch operations to enable testing with small datasets - Always inject services via constructor, never use static calls or
$this->get()
9) Cart Rules & Vouchers
Read: references/cart-rules.md
Complete guide for creating and managing PrestaShop cart rules (discount vouchers) programmatically:
Key patterns:
- Code generation: Use character set
123456789ABCDEFGHIJKLMNPQRSTUVWXYZ(no O/0 to avoid confusion, same as PrestaShop admin.js) - Uniqueness check: Validate with
CartRule::getIdByCode($code)before creation - Multilingual names: Set
namefor all active languages viaLanguage::getLanguages(true) - Customer restriction: Use
$cartRule->id_customerfor customer-specific vouchers - Date range: Both
date_fromanddate_torequired (format:Y-m-d H:i:s) - Reduction types:
reduction_percent,reduction_amount,free_shipping, orgift_product - Essential fields:
quantity,quantity_per_user,priority,highlight,partial_use,active - Restrictions: All default to
false(no restriction); set specific restrictions as needed
Common use cases documented:
- Customer-specific vouchers with percentage discount
- Auto-apply cart rules (no code required)
- Limited-time flash sales with highlight
- Free shipping vouchers
- Gift product vouchers
10) 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
config/components/grid/(factory, query, data, grid, position) + 1 Twig FilesystemLoader - Twig FilesystemLoader path is
%kernel.project_dir%/modules/mymodule/views—%kernel.project_dir%is the PS root (parent ofapp/), somodules/is a direct child. Never use%kernel.project_dir%/../modules/ - 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)
PrestaShop 9 Core Documentation
The following files are bundled with this skill in the ps9-core-ai/ directory.
Read them for deep understanding of PS9 core architecture, conventions, and patterns.
ps9-core-ai/CONTEXT.md— Root AI context for the PS9 codebase: project-wide coding standards, architecture layers (Core/Adapter/Bundle/Legacy), CQRS pattern, branching policy, and the full index of domain and component contexts.ps9-core-ai/STRUCTURE.md— Architecture of the.ai/folder itself: how contexts, skills, and pointer files are organized and how AI tools discover them.
Note for skill maintainers: These files are static snapshots from the official PrestaShop repository. To update them, skill maintainers can run
lotr --installfrom a module directory. This is not a runtime operation — the files are pre-bundled and version-controlled with the skill.
Steering
If your project or organisation defines a steering layer (layered context rules for coding standards, architecture conventions, and project-specific overrides), load the steering files before starting any task.
Finding the resolver — search in this order:
steering/resolver.mdat the module root (custom installation)vendor/websenso/prestashop-module-devtools/steering/resolver.md— canonical path when the devtools are installed as a Composer package (same package that providesvendor/websenso/prestashop-module-devtools/bin/lotr)- Any
vendor/*/*/steering/resolver.md— scan all vendor subfolders for asteering/resolver.mdfile (use the first match found)
If none exists, skip steering silently and apply only the skill defaults.
Typical steering structure (paths relative to wherever the resolver is found):
steering/resolver.md ← load order and conflict rules
steering/company/ ← organisation-wide standards
steering/languages/php/coding-standards.md ← PHP conventions
steering/frameworks/prestashop/ ← PrestaShop-specific rules
Load steering files from lowest to highest priority (company → language → framework → project). Later layers override earlier ones.
Escalation
- PrestaShop 9 Module Creation
- Module Good Practices
- Official Example Modules
- demosymfonyform — canonical Symfony form config page
- Module Validator
- PrestaShop Developer Slack