SuperAPI AI Directory

prestashop-module-development

Coding & development

View repository

Updated 10-15 22:41

About this skill

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

MANDATORY patterns:

  • install() method: ALWAYS if (!parent::install()) { return false; } FIRST, THEN delegate to installer
  • uninstall() method: ALWAYS installer FIRST, THEN parent::uninstall() 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: name, class_name, label, parent_class_name, visible
  • Parent group tab: Auto-check if exists, if not, prepend to tabs array with array_unshift($this->tabs, $parentTab)
  • No getTabs(): NEVER add getTabs() method to module class — all tab management in Installer only

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
  • getContent() 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 getWidgetVariables) 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 use ContainerFinder — 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 Configuration::get() directly in business logic
  • ALWAYS create /config_data/module.yml to define all configuration keys and default values (single source of truth)
  • ALWAYS use ConfigurationInstaller service for install/uninstall operations
  • ALWAYS use ConfigurationManager service to read configuration (from database or YAML fallback)
  • Structure:
    • config_data/module.yml — 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 HelperForm — use Symfony form components + FrameworkBundleAdminController
  • Do NOT access services via $this->get() in controllers — Controllers have a limited service locator. Use constructor dependency injection with FormHandler instead
  • Five classes: DataConfiguration, FormDataProvider, FormType, FormHandler (PrestaShop Core), Controller (with DI)
  • Wire everything in config/components/configuration/services.yml (imported by config/admin/services.yml) and config/routes.yml
  • FormHandler: Use PrestaShop\PrestaShop\Core\Form\Handler service that wraps form creation, validation, and saving
  • Controller registration: Must be registered as public: true 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 {% extends %}; place {{ form_start() }} BEFORE <div class="card"> and {{ form_end() }} AFTER </div> — form tags must wrap the entire card structure at the same nesting level to prevent broken HTML
  • Controller pattern:
    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: 'multiple' => true for multi-selection; 'required' => false for optional. DO NOT use 'expanded' option — it does not exist for this type
    • Template requirements: Add {% form_theme configurationForm '@PrestaShop/Admin/TwigTemplateForm/prestashop_ui_kit.html.twig' %} before {% extends %} directive; include JS bundle in {% block javascripts %}
    • 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 id attribute (typically form_<field_name>)
    • 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 ChoiceTree

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
  • 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 '' use string $field = ''; columns declared NULL use ?string $field = null
  • TranslatableType returns null for languages the user did not fill in — in the Manager, always coerce null to '' for NOT NULL string fields: $row->setName((string) ($value ?? '')). Never pass the raw form value directly to a setter on a NOT NULL column
  • 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 use Db::getInstance() raw SQL — SymfonyContainer::getInstance() returns null in the pr:mo Symfony console context (global $kernel is never set). All Doctrine ORM calls silently do nothing at install time. See references/module-class-and-installer.md → FixturesInstaller section.
  • Db::getValue() appends LIMIT 1 internally — never write LIMIT 1 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 config/services.yml — only config/admin/services.yml and config/front/services.yml are needed; a root-level config/services.yml is never required and should not exist
  • config/admin/services.yml is loaded by admin kernel only — import ../common.yml + admin components (never common.yml without the ../ prefix)
  • Repository services only in config/common.yml components (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
  • Only services accessed via $this->get('service.id') need public: true — declare it explicitly on each such service; all other services remain private (omit public:). Avoid _defaults: public: true as a blanket: it is against Symfony best practices and causes a fatal error in PrestaShop's Symfony version when combined with parent: services
  • Services that use parent: and need to be public must declare public: true explicitly on the service definition — public cannot be inherited from _defaults when parent is set (PrestaShop's Symfony version enforces this)
  • Never point config/services.yml at admin/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 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

  • CRITICAL: For front-office CSS/JS registration, ALWAYS use actionFrontControllerSetMedia hook, NEVER use header or displayHeader — use $this->context->controller->registerStylesheet() and ->registerJavascript() in that hook
  • Register hooks in Installer, not in install() directly
  • When implementing WidgetInterface, DO NOT define explicit hook methods like hookDisplayHome() that just call renderWidget() — PrestaShop automatically calls renderWidget() 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 getWidgetVariables() — 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: ThemeTemplateInjector (service, reusable) + ThemeTemplateInstaller (install orchestrator)
  • Never use Theme::getThemes() 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 true — 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 $parameters 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:

  • ❌ 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/components/ sub-folders (imported by config/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:action format (e.g., wsautocartrules:create, ws_keepdblight:cleandb)
  • Return codes: 0 for success, 1 for failure (NOT Command::SUCCESS/FAILURE constants)
  • Use SymfonyStyle for rich console output (tables, progress bars, styled messages)
  • Add --limit options 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 name for all active languages via Language::getLanguages(true)
  • Customer restriction: Use $cartRule->id_customer for customer-specific vouchers
  • Date range: Both date_from and date_to required (format: Y-m-d H:i:s)
  • Reduction types: reduction_percent, reduction_amount, free_shipping, or gift_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 actions
  • QueryBuilder — Doctrine DBAL query with sorting, pagination, and filters
  • Filters — 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 of app/), so modules/ 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 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)

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 --install 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:

  1. steering/resolver.md at the module root (custom installation)
  2. 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)
  3. Any vendor/*/*/steering/resolver.md — scan all vendor subfolders for a steering/resolver.md 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