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
- Use the PrestaShop Module Generator to scaffold new modules
1) Main module class & installer
Read:
references/module-class-and-installer.md
MANDATORY patterns:
- install() method: ALWAYS
if (!parent::install()) { return false; }
FIRST, THEN delegate to installer
- uninstall() method: ALWAYS installer FIRST, THEN with chaining:
return $installer->uninstall() && parent::uninstall();
- Hooks: Define as class property array
private array $hooks = [...]
in Installer, use $module->registerHook($this->hooks)
to register
- Tabs: Define as class property array
private array $tabs = [...]
with structured data: , , , ,
- Parent group tab: Auto-check if exists, if not, prepend to tabs array with
array_unshift($this->tabs, $parentTab)
- No getTabs(): NEVER add method to module class — all tab management in Installer only
Key rules:
- Always
require_once __DIR__ . '/vendor/autoload.php';
after the guard
- Never put hook registration, DB queries, or calls directly in — delegate to
src/Install/Installer.php
- must only redirect to the Symfony route using the modern Symfony router:
$router = $this->get('router'); $route = $router->generate('route_name'); Tools::redirectAdmin($route);
— NEVER use $this->context->link->getAdminLink()
- No SQL in the main module class — all database access (including in hooks like
hookActionShopDataDuplication
and widget methods like ) must be delegated to the Repository or Manager class via
- Service access: use + null check in admin context; use plain + null check in front-office context. NEVER use — it is unnecessary. See
references/module-class-and-installer.md
→ Guard patterns section.
1b) Configuration variable management
Read:
references/configuration-management.md
MANDATORY pattern: YAML-based configuration with ConfigurationManager service
Key rules:
- NEVER use
Configuration::updateValue()
or directly in business logic
- ALWAYS create to define all configuration keys and default values (single source of truth)
- ALWAYS use service for install/uninstall operations
- ALWAYS use service to read configuration (from database or YAML fallback)
- Structure:
- — all keys + defaults
src/Install/ConfigurationInstaller.php
— install/uninstall config values
src/Configuration/ConfigurationManager.php
— read config (DB or YAML)
config/components/configuration/configuration_manager.yml
— register service
- Benefits: Single source of truth, type-safe access, no hardcoded keys, easy to test and maintain
2) Modern configuration pages
Read:
references/configuration-page.md
MANDATORY pattern: FormHandler with constructor dependency injection
Key rules:
- Do NOT use — use Symfony form components +
FrameworkBundleAdminController
- Do NOT access services via in controllers — Controllers have a limited service locator. Use constructor dependency injection with FormHandler instead
- Five classes: , , , (PrestaShop Core), (with DI)
- Wire everything in
config/components/configuration/services.yml
(imported by config/admin/services.yml
) and
- FormHandler: Use
PrestaShop\PrestaShop\Core\Form\Handler
service that wraps form creation, validation, and saving
- Controller registration: Must be registered as service with
controller.service_arguments
tag
- FormType registration: Must have
parent: 'form.type.translatable.aware'
and tags: [{ name: form.type }]
- Twig template form wrapping: ALWAYS use
{% form_theme configurationForm '@PrestaShop/Admin/TwigTemplateForm/prestashop_ui_kit.html.twig' %}
before ; place BEFORE and AFTER — form tags must wrap the entire card structure at the same nesting level to prevent broken HTML
- Controller pattern:
php
private FormHandlerInterface $formHandler;
public function __construct(FormHandlerInterface $formHandler)
{
$this->formHandler = $formHandler;
}
public function indexAction(Request $request): Response
{
$form = $this->formHandler->getForm();
// ... handle request and save via $this->formHandler->save()
}
- CategoryChoiceTreeType: PrestaShop's form type for hierarchical category selection trees. Requires specific setup:
- Valid form options: for multi-selection; for optional. DO NOT use option — it does not exist for this type
- Template requirements: Add
{% form_theme configurationForm '@PrestaShop/Admin/TwigTemplateForm/prestashop_ui_kit.html.twig' %}
before directive; include JS bundle in
- JavaScript initialization: Create
views/js/mymodule.form.bundle.js
with window.prestashop.component.initComponents(['ChoiceTree'])
and new window.prestashop.component.ChoiceTree('#form_categories').enableAutoCheckChildren()
— selector must match the HTML attribute (typically )
- Form data handling: Returns array of category IDs; store as comma-separated string via
implode(',', $data['categories'])
; retrieve and convert back with array_filter(array_map('intval', explode(',', $storedValue)))
- Common issues: Tree renders as plain checkboxes → missing form theme; expand/collapse doesn't work → missing JS initialization; "ChoiceTree is not defined" → use
window.prestashop.component.ChoiceTree
not plain
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 — PS adds the prefix globally; use with no parameter
- Prefix table names with a consistent company or module prefix (e.g. ,
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 use ; columns declared use
- returns for languages the user did not fill in — in the Manager, always coerce null to for string fields:
$row->setName((string) ($value ?? ''))
. Never pass the raw form value directly to a setter on a column
- Do NOT create or Doctrine event listeners for table naming
- Always sanitize raw DBAL SQL: cast IDs with , use bound parameters
- No raw SQL (, , string concatenation) outside Repository and Manager classes. This applies everywhere: main module class, Installer, FixturesInstaller, hooks, widget methods. The only exception is SQL schema queries (, ) which have no Repository equivalent.
- FixturesInstaller must use raw SQL —
SymfonyContainer::getInstance()
returns in the Symfony console context ( is never set). All Doctrine ORM calls silently do nothing at install time. See references/module-class-and-installer.md
→ FixturesInstaller section.
- appends internally — never write in 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 — only
config/admin/services.yml
and config/front/services.yml
are needed; a root-level is never required and should not exist
config/admin/services.yml
is loaded by admin kernel only — import + admin components (never without the prefix)
- Repository services only in components (Doctrine-level, no deps)
- All -dependent services go in
config/admin/services.yml
- Always split into component sub-folders under — never one flat
- Only services accessed via need — declare it explicitly on each such service; all other services remain private (omit ). Avoid as a blanket: it is against Symfony best practices and causes a fatal error in PrestaShop's Symfony version when combined with services
- Services that use and need to be public must declare explicitly on the service definition — cannot be inherited from when is set (PrestaShop's Symfony version enforces this)
- Never point at — this loads admin-only services into the front kernel, breaking container compilation and making all calls fail silently
4) Security (mandatory)
- CSRF: handled by Symfony forms automatically; validate manually for raw AJAX endpoints
- SQL injection: + on every value, or use builder
- File uploads: pass full -compatible array (including , , ) to
ImageManager::validateUpload()
5) Hooks & front office integration
Read:
references/hooks-and-front-office.md
- CRITICAL: For front-office CSS/JS registration, ALWAYS use
actionFrontControllerSetMedia
hook, NEVER use or — use $this->context->controller->registerStylesheet()
and in that hook
- Register hooks in , not in directly
- When implementing , DO NOT define explicit hook methods like that just call — PrestaShop automatically calls for all registered display hooks
- For front-office Smarty templates: use
{l s='Text' d='Modules.Modulename.Front'}
directly in template, NEVER pass pre-translated strings as Smarty variables from — only pass dynamic data (URLs, IDs, etc.), not static translatable UI text
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: (service, reusable) + (install orchestrator)
- Never use in install context — legacy class, not autoloaded in Symfony console; use
scandir(_PS_ALL_THEMES_DIR_)
instead
- Wrap install/uninstall calls in try/catch returning — theme injection must never block module install
6) Translations
Read:
references/translations.md
FrameworkBundleAdminController::trans()
signature is trans($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 causes a fatal type error)
- 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:
- ❌ — inject
$context: "@=service('prestashop.adapter.legacy.context').getContext()"
instead
- ❌ / — inject
@prestashop.adapter.legacy.configuration
instead
- ❌
Context::getContext()->getTranslator()
— inject instead
Key rules:
- Define services in sub-folders (imported by
config/admin/services.yml
)
- Use 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
or
):
yaml
# 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: format (e.g., , )
- Return codes: for success, for failure (NOT / constants)
- Use for rich console output (tables, progress bars, styled messages)
- Add options for batch operations to enable testing with small datasets
- Always inject services via constructor, never use static calls or
9) Cart Rules & Vouchers
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 for all active languages via
Language::getLanguages(true)
- Customer restriction: Use for customer-specific vouchers
- Date range: Both and required (format: )
- Reduction types: , , , or
- Essential fields: , , , , ,
- Restrictions: All default to (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:
- — columns (, , ), filters, row actions
- — Doctrine DBAL query with sorting, pagination, and filters
- — default sort/limit settings
- 5 service definitions in (factory, query, data, grid, position) + 1 Twig FilesystemLoader
- Twig FilesystemLoader path is
%kernel.project_dir%/modules/mymodule/views
— is the PS root (parent of ), so is a direct child. Never use %kernel.project_dir%/../modules/
- 4 routes in (index, search, toggle, update-position)
- 4 controller actions (, , , )
- Pre-built JS bundle (copied from , grid ID replaced via )
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
for full instructions.
Step 1 — lotr (run from the module root)
bash
vendor/websenso/prestashop-module-devtools/bin/lotr
Expected:
🎉 All commands completed successfully! Executed: 6/6
Step 2 — Install test (run from the PS root)
bash
php bin/console pr:mo install mymodule
Expected:
L'action Install sur le module … a réussi.
Failure modes / debugging
Common failure areas:
- — 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
directory.
Read them for deep understanding of PS9 core architecture, conventions, and patterns.
- — 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.
- — Architecture of the 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
from 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:
- at 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 provides vendor/websenso/prestashop-module-devtools/bin/lotr
)
- Any
vendor/*/*/steering/resolver.md
— scan all vendor subfolders for a file (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