CLI Commands

This content was generated with the assistance of AI and should be verified against the official documentation of each platform before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

This page was written against PrestaShop 9.2.0 (released 2026-09-30, Symfony 6.4.46, PHP 8.5.12 in the official Docker image, MySQL 8.4) and checked on 2026-10-27. The console exists in every edition of the open-source, self-hosted PrestaShop; it is the tool for everything that should not depend on a browser session: deployments, scheduled jobs, bulk operations, scripted installations and diagnostics. PrestaShop 8.x has a smaller set of prestashop: commands; several commands described here are new in 9.1 and 9.2 and the page says so where it matters.

The console is a Symfony Console application. PrestaShop adds a prestashop: namespace with 28 commands in 9.2.0 and keeps the standard commands of Symfony and of its bundles (cache:*, debug:*, lint:*, doctrine:*, messenger:*, secrets:*, about), which brings the total to 111 commands in the back-office kernel. A module can add its own. This page explains how to run the console safely, documents every prestashop: command with the options that were observed, shows the Symfony commands that matter to a shop developer, and builds a module with its own command. It follows the Admin API page (whose clients can be created here) and precedes the Performance and caching page (which uses cache:clear and the configuration keys below). The module that brings its own command is pcfhcli.

The running example is Pocket Coffee. Its deployment script installs the modules, applies the shop settings that belong to the coffee club (free shipping above 40 EUR), creates the API client of the ERP and rebuilds the search index; a nightly job prints an order digest with a command of the module pcfhcli. What was run and what was not is listed in What was run and what was not.

Running the console

The console is the file bin/console in the shop root, a PHP script. Run it from the shop root, with the PHP binary that the web server uses, and as the user that owns the shop files (usually www-data):

cd /var/www/html
sudo -u www-data php bin/console --version
# PrestaShop 9.2.0 with Symfony 6.4.46

# in the official Docker image
docker exec -u www-data -w /var/www/html ec-prestashop php bin/console list prestashop

Running the console as root is the most common mistake: it writes cache files (var/cache/<env>/..., var/logs) owned by root, after which the web server cannot write them and the shop fails with permission errors or silent cache misses. If it happened, chown -R www-data: var repairs it. The CLI also uses a different php.ini than the web server: opcache.enable_cli is Off in the Docker image, so every console invocation compiles PHP again (about a second of startup for a command), and memory_limit and max_execution_time may differ; long jobs should set their own limits (php -d memory_limit=1G bin/console ...).

Global options

Every command accepts the standard options plus three that PrestaShop defines:

Option Meaning

-e, --env=ENV

The environment: dev or prod. The default is dev, which is not what a production server should use for batch jobs: dev builds a debug container in var/cache/dev and logs far more. Add --env=prod to production scripts. The environment of the web requests is decided by _PS_MODE_DEV_, not by this option, see the performance page.

--no-debug

Switches the Symfony debug mode off for this call.

--app-id=ID

Selects the application kernel: admin (the default, back-office container) or admin-api (the Admin API container). debug:router --app-id=admin-api lists the routes of the API; cache:clear --app-id=admin-api clears only its cache. The admin-api container had 110 commands on the test shop, the admin container 111; module commands are in both.

-n, --no-interaction

Never ask a question. Required in scripts: employee:create-admin and employee:change-password prompt for missing values otherwise.

-q, -v, -vv, -vvv

Quiet and verbosity levels. -vvv prints the stack trace of an error, the first thing to try when a command exits with status 1 and one line of message.

--profile

Enables the Symfony profiler for the command (needs debug mode).

The Docker image prints PHP Warning: Constant _PS_MODE_DEV_ already defined at the start of every command when PS_DEV_MODE=1 (its defines_custom.inc.php defines the constant and so does the bootstrap). The warning goes to the error stream, it is harmless, and scripts that capture the output should read only standard output (2>/dev/null).

Exit codes

A command returns 0 on success and a non-zero code on failure, which is what CI and cron need, with one exception that was observed: prestashop:module <action> printed an error message for an invalid module (Cannot install module nonexistentmod. The module nonexistentmod is invalid and cannot be loaded.) and still exited with 0, so a deployment script should check the module state afterwards (prestashop:module:list), not only the exit code. A command that throws, such as a weak password for employee:create-admin, ends with Symfony’s error status. Codes seen in the runs: 1 for a failure reported by the command (unknown feature flag, a failing linter, translation:check-fixtures), 2 for an invalid input option of a custom command (Command::INVALID), 4 for an uncaught exception.

The command map

The 28 prestashop console commands of PrestaShop 9.2.0 in six groups: modules and themes, configuration and switches, Admin API, people and web server, upkeep of data, developer tools, and the Symfony commands beside them

The complete list printed by bin/console list prestashop:

Command Purpose

prestashop:api-client

Create or delete an Admin API client.

prestashop:api:generate-tracking-table

Generate a Markdown table of the Admin API endpoint coverage (alias prestashop:generate-api-tracking). Contributor tool.

prestashop:config

Get, set or remove a configuration value.

prestashop:debug

Show or set the debug mode.

prestashop:employee:change-password, prestashop:employee:create-admin

Change an employee’s password, or create a SuperAdmin employee.

prestashop:feature-flag

List, enable or disable feature flags.

prestashop:generate:apidoc

Generate the API documentation of the legacy web service (needs a source checkout).

prestashop:htaccess:generate

Generate the .htaccess file.

prestashop:linter:legacy-link, prestashop:linter:naming-convention, prestashop:linter:security-attribute

Check back-office routes and controllers.

prestashop:list:commands-and-queries

List the CQRS commands and queries and their API routes.

prestashop:mail:generate

Generate the mail templates of a theme for a locale.

prestashop:module

Install, uninstall, enable, disable, reset, upgrade, configure or delete a module.

prestashop:module:list

List the modules and their versions.

prestashop:schema:update-without-foreign

Compare the Doctrine schema with the database and optionally apply it, without foreign keys.

prestashop:search:index

Index products for the search.

prestashop:taxes:update-eu-tax-rule-groups

Update the EU tax rule groups.

prestashop:theme:enable, prestashop:theme:export

Enable a theme, or package a theme with its dependencies.

prestashop:thumbnails:regenerate

Regenerate image thumbnails.

prestashop:translation:check-fixtures, prestashop:translation:export-module, prestashop:translation:find-duplicates

Translation tooling.

prestashop:update:configuration-file-hooks-listing, prestashop:update:hooks-documentation, prestashop:update:sql-upgrade-file-hooks-listing

Tools of the core maintainers that keep the list of hooks in sync.

The documentation of each command marks the release that introduced it where that is recent. The pages for about, prestashop:feature-flag, prestashop:generate:apidoc, prestashop:search:index, prestashop:thumbnails:regenerate, prestashop:translation:export-module and prestashop:update:hooks-documentation are marked 9.1, and those for prestashop:employee:create-admin, prestashop:employee:change-password, prestashop:htaccess:generate and prestashop:module:list are marked 9.2. Commands without a mark are older or were documented without a version; on PrestaShop 8.x only a subset exists, so check bin/console list prestashop on the version you run. The documentation of the console component also lists prestashop:licenses:update, a tool for the core repository that is not present in a release install.

Two Symfony commands deserve to be listed with them because the documentation of the platform relies on them: about shows the version, environment, cache size, log size, OPcache and APCu state, and the PrestaShop section (version, debug mode, Smarty cache); and bin/console debug:* explains what the container, router and event dispatcher contain.

bin/console about | tail -8
#  --------------  -------
#   PrestaShop
#  --------------  -------
#   Version         9.2.0
#   Debug mode      false
#   Smarty Cache    true
#  --------------  -------

In that run about printed Debug mode false while the kernel above it was Environment dev, Debug true: the PrestaShop section reads the _PS_MODE_DEV_ define from the files, not the environment variable that the Docker image uses, see Debug mode.

Modules

prestashop:module <action> <module name> [<file path>] runs the same code as the Module Manager of the back office, so the module’s install(), upgrade scripts and hooks execute.

Action Effect

install

Installs the module: copies its overrides, runs install() and dispatches the hook actionBeforeInstallModule. If the module is already installed, install performs an upgrade instead, which makes the command safe to repeat in a deployment script (a repeated call printed Install action ... succeeded).

uninstall

Runs uninstall() and removes the module’s overrides. The module files stay on disk.

enable, disable

Marks the module active or inactive without uninstalling it. A disabled module’s hooks do not run and its Admin API resources are not loaded; its console command, however, stayed listed and runnable after cache:clear in the test, because the services of installed modules are loaded whatever their state.

reset

Uninstalls and installs again, which runs uninstall() and install() (the console calls it without the option that keeps the data). Data that the module deletes in uninstall() is lost.

upgrade

Runs the upgrade scripts upgrade/upgrade-x.y.z.php for versions newer than the installed one, when the version property of the main class was raised.

configure

Applies the declarative file self_config.yml of the module, or the YAML file given as third argument, see below.

delete

Removes the module folder from modules/ and nothing else: it does not run uninstall(). Uninstall first, otherwise the module’s tables, hooks and configuration stay in the database with no code behind them.

The option --skip-overrides installs or uninstalls the module without touching the files under override/, which is the way to install a module on a shop whose override of the same class is managed by hand.

bin/console prestashop:module install pcfhcli
#   Install action on module pcfhcli succeeded.
bin/console prestashop:module:list --simple | grep pcfh
bin/console prestashop:module disable pcfhcli
bin/console prestashop:module enable pcfhcli
bin/console prestashop:module upgrade pcfhcli
#   Update action on module pcfhcli succeeded.
bin/console prestashop:module reset pcfhcli
#   Reset action on module pcfhcli succeeded.
bin/console prestashop:module install nonexistentmod
#   Cannot install module nonexistentmod. The module nonexistentmod is invalid and cannot be loaded.   (exit status 0)

The modules of the install were listed with prestashop:module:list (9.2). Its flags are --active (installed and enabled), --disabled (installed but disabled), --not-installed (present on disk or registered by a hook but not installed), --all (also the uninstalled) and --simple (technical names only, one per line, for scripts):

bin/console prestashop:module:list --disabled
# +-----------------+---------+----------+
# | Name            | Version | Status   |
# +-----------------+---------+----------+
# | blockwishlist   | 4.0.0   | Disabled |
# | ps_brandlist    | 1.0.5   | Disabled |
# | ps_supplierlist | 1.0.7   | Disabled |
# +-----------------+---------+----------+
bin/console prestashop:module:list --all --simple | wc -l       # 91 modules on the test shop

Declarative configuration with self_config.yml

prestashop:module configure <name> reads self_config.yml from the module folder (the first file of that name found under it), or the YAML file given as last argument, and applies up to four steps in this order: configuration, files, sql and php. It is meant for turning a freshly installed module into a configured one in a script, with no back-office visit. The Pocket Coffee module pcfhcli ships this file:

configuration:
  update:
    PCFHCLI_DIGEST_FORMAT: json
    PCFHCLI_FREE_SHIPPING_FROM:
      value: 40
  delete:
    - PCFHCLI_OLD_KEY
Step Content

configuration

update: a map of configuration key to a scalar or to {value: ...}; delete: a list of keys.

files

A list of {source, dest} pairs copied with the Symfony filesystem. Relative paths are relative to the YAML file; URLs are accepted.

sql

A list of SQL files (or {file: ...} entries) run in one transaction; the tokens PREFIX_ and DB_NAME are replaced by the table prefix and the database name. Statements are split on ;.

php

A list of PHP files; each must define a class named like the file with a run($module, $params) method, which receives the module instance and the optional params.

bin/console prestashop:module configure pcfhcli
#   Configuration successfully applied.
bin/console prestashop:config get PCFHCLI_DIGEST_FORMAT
#  [OK] PCFHCLI_DIGEST_FORMAT=json
bin/console prestashop:module configure pcfhapi
#   Validation of configuration details failed:
#   - No config file to apply
bin/console prestashop:module configure pcfhcli /nonexistent.yml
#   - Specified config file is not found

Configuration values

prestashop:config <get|set|remove> <KEY> reads and writes the table ps_configuration, the same values that Configuration::get() and Configuration::updateValue() use in PHP. Options: --value (alias -val) for set, --lang (a language ID or an ISO code) for translatable values, and --shopId (-s) and --shopGroupId (-g) for the multistore scope. Without a shop option the value applies to every shop.

bin/console prestashop:config get PS_SHIPPING_FREE_PRICE
#  [OK] PS_SHIPPING_FREE_PRICE=0
bin/console prestashop:config set PS_SHIPPING_FREE_PRICE --value=40      # free shipping above 40 EUR for Pocket Coffee
#  [OK] PS_SHIPPING_FREE_PRICE=40
bin/console prestashop:config set PCFHCLI_NOTE --value=hola --lang=es    # a value in one language only
bin/console prestashop:config get PCFHCLI_NOTE --lang=es
#  [OK] PCFHCLI_NOTE=hola
bin/console prestashop:config remove PCFHCLI_NOTE
#  [OK] PCFHCLI_NOTE removed
bin/console prestashop:config get PS_SHOP_NAME -s 99
#  [ERROR] Shop group was not found for the shop with id 99.

Notes from the runs:

  • get of a key that does not exist is not an error: it prints KEY= with an empty value and exits with 0, so a typo in a key name is silent. Check the value you just wrote with a get, as above.

  • A global value is stored with id_shop and id_shop_group set to NULL: select name, value, id_shop, id_shop_group from ps_configuration where name = 'PS_SHIPPING_FREE_PRICE' returned one row with both columns NULL.

  • The set output echoes the value that is stored after the call; for a translatable key written with --lang=es, the echo showed the value without language, so verify with get --lang=es.

  • The command changes the database. The web process loads the configuration on each request, so a changed value is seen by the next request with no cache clearing, unless the SQL cache of the performance page is enabled with APCu or Memcached; the APCu cache of the web server is not shared with the command line, so a set from the console is then not visible until the cached query expires or the web cache is flushed (see the performance page). Values compiled into the container (services, routes) are different: they need cache:clear.

  • Configuration keys that matter to the performance page (PS_SMARTY_CACHE, PS_SMARTY_FORCE_COMPILE, PS_CSS_THEME_CACHE, PS_JS_THEME_CACHE, PS_HTACCESS_CACHE_CONTROL) and to the Admin API (PS_ENABLE_ADMIN_API, PS_ADMIN_API_FORCE_DEBUG_SECURED) can be read and set with it.

Feature flags

A feature flag turns an in-progress area of the core on or off. The back office lists them in Advanced Parameters > New & Experimental Features; prestashop:feature-flag list|enable|disable [<flag>] does the same from the console (9.1 and later).

bin/console prestashop:feature-flag list
# +----------------------------------+-----------------------+----------+
# | Feature flag                     | Type                  | State    |
# +----------------------------------+-----------------------+----------+
# | admin_api_multistore             | env,dotenv,[db]       | Disabled |
# | admin_api_experimental_endpoints | env,dotenv,[db]       | Disabled |
# | catalog_price_rule               | env,dotenv,[db]       | Disabled |
# | country                          | env,dotenv,[db]       | Enabled  |
# | ...                              |                       |          |
# | front_container_v2               | env,[dotenv],db       | Disabled |
# | new_pricing                      | env,query,dotenv,[db] | Disabled |
# +----------------------------------+-----------------------+----------+
bin/console prestashop:feature-flag enable catalog_price_rule
# Feature flag catalog_price_rule was enabled
bin/console prestashop:feature-flag disable catalog_price_rule
# Feature flag catalog_price_rule was disabled
bin/console prestashop:feature-flag enable nope
# Feature flag nope does not exist        (exit status 1)

The Type column lists the sources that can define the flag, in priority order, and the source currently in force is in brackets: env (an environment variable PS_FF_<FLAG> such as PS_FF_FRONT_CONTAINER_V2=true), query (a URL parameter, only for new_pricing, to try it in the browser), dotenv (the .env file of the shop, where the flags front_container_v2 is defined) and db (the ps_feature_flag table, which enable and disable write). The documentation says that enable and disable modify the appropriate source for the flag. Changing a flag whose bracket shows env or dotenv was not tested; look at the bracket first, because the source in force decides what the shop does.

The flags of 9.2.0, from the ps_feature_flag table (the stability column is what the back-office page shows; the description is the label of the table):

Flag State Stability Description in the shop

admin_api_multistore

off

beta

Enable or disable the Admin API when multistore is enabled.

admin_api_experimental_endpoints

off

beta

Experimental API endpoints are disabled by default in the prod environment; this allows to force them on.

catalog_price_rule, permission, customer_threads, customer_group, store

off

beta

The migrated catalogue price rules, permissions, customer threads, customer group and store pages.

discount, new_pricing, improved_b2b, improved_shipment

off

beta

The new discount system, the new pricing engine, the improved B2B mode (which also needs the B2B mode in the general settings) and the improved shipment system.

front_container_v2

off

beta

The new front container.

country, carrier, tax_rules_group, merchandise_return, tag, quick_access, email_body_translation, hook_module_v2

on

stable

The migrated countries, carriers, tax rule groups, merchandise return, tag, quick access, email body translation and "Hook a module" pages.

Two behaviours were verified. First, the state changed immediately and the CLI printed the new state; a flag that gates the Admin API surface needs the cache of the API kernel cleared before the routes change (cache:clear --app-id=admin-api). Second, flags that were switched on to test and left on stay on in the database: scripts should restore the state they found, as the example above does. Do not switch beta flags on in production without testing the modules the shop uses, because modules written for the legacy page can stop working when its page is migrated.

Admin API clients and reports

prestashop:api-client create|delete <client-id> is documented on the Admin API page, with its options (--name, --description, --scopes, --all-scopes, --timeout, --secret, --secret-only). In a script, capture the secret once and store it in the secret manager of the integration:

PS_ERP_SECRET=$(bin/console prestashop:api-client create pocket-coffee-erp \
  --name="Pocket Coffee ERP" --scopes=product_read,customer_group_read,customer_group_write \
  --timeout=900 --secret-only 2>/dev/null | tr -d ' \n')
# store ${PS_ERP_SECRET} in the secret manager now; the shop keeps only a bcrypt hash

bin/console prestashop:api-client delete pocket-coffee-erp
#  [OK] Successfully removed API Client pocket-coffee-erp
bin/console prestashop:api-client create x --scopes=bogus_scope
#  The scopes bogus_scope are not associated to any installed API.

prestashop:api:generate-tracking-table [-o FILE] [--skip-github] [--include-excluded] writes a Markdown report of the CQRS commands and queries that have an Admin API endpoint, with a percentage; --skip-github avoids the calls to the GitHub API that would look for open pull requests (and needs no token). On the test shop it reported 501 endpoints, 190 implemented (37.9%). prestashop:generate:apidoc belongs to the legacy web service documentation generator and needs the source checkout.

People and web server

prestashop:employee:create-admin [<email>] [--first-name] [--last-name] [--password] (9.2) creates a back-office SuperAdmin from the shell: active, in the default language, associated with every shop, no avatar. It is meant for first-access recovery and scripted installations. A password is asked twice and hidden when it is not passed with --password; passing it on the command line leaves it in the shell history and in the process list, so prefer the interactive prompt or read it from a file descriptor in a script.

bin/console prestashop:employee:create-admin pcfh-ops@example.test --first-name=Ada --last-name=Lovelace \
  --password="${PS_NEW_ADMIN_PASSWORD}" -n
#  [OK] SuperAdmin employee #3 (pcfh-ops@example.test) created with full back-office access.
#  [WARNING] This account has SuperAdmin privileges. Share the credentials carefully.

bin/console prestashop:employee:create-admin pcfh-ops@example.test ... -n
#  [ERROR] Failed to create employee: An account already exists for this email address.       (exit status 1)
bin/console prestashop:employee:create-admin not-an-email ... -n
#  [ERROR] Failed to create employee: Email address is invalid.
bin/console prestashop:employee:create-admin x@example.test --password=abc ... -n
#  Employee password length must be between 8 and 72                                           (exit status 4)

There is no command to delete an employee (the back-office Team page does that); the test employee was removed with SQL (delete from ps_employee_shop ...; delete from ps_employee ...) after the run. prestashop:employee:change-password <email> [--password] sets a new password and sends the standard "Your new password" e-mail to the employee, which means it needs working mail settings; it was not run on the test shop for that reason.

prestashop:htaccess:generate [--force] (9.2) regenerates the .htaccess file of the shop from the current configuration (URL rewriting, the Apache optimisation block when PS_HTACCESS_CACHE_CONTROL is on). Without --force it refuses to overwrite:

bin/console prestashop:htaccess:generate
# .htaccess already exists. Use --force to overwrite.

The checksum of the file was the same before and after that call. Use --force only after a backup of the file, because manual additions outside the markers that the generator preserves are lost.

Upkeep of catalogue data

prestashop:search:index (9.1) builds the product search index that the shop’s search uses. --full rebuilds the whole index, --shop-id (-s) and --shop-group-id (-g) limit it to a shop or a group, and --product-id (-p) re-indexes one product, which is the call to make after an import that bypassed the back office. Without options it indexes the products that are missing from the index.

bin/console prestashop:search:index -p 1
#  [OK] Search indexation complete.
bin/console prestashop:search:index --full
#  [OK] Search indexation complete.

prestashop:thumbnails:regenerate <image> [<image-type>] [--delete] (9.1) regenerates the derived images. image is one of all, categories, manufacturers, suppliers, products, stores; image-type is the ID of an image type (Design > Image Settings), 0 meaning all; --delete (-d) erases the previous files first. Run it after changing the image sizes of the theme or after adding a format. For a large catalogue it takes minutes and, as a CPU-heavy job, belongs to a maintenance window or a nightly cron:

bin/console prestashop:thumbnails:regenerate stores
#  [INFO] The thumbnails were successfully regenerated.
0 2 * * * cd /var/www/html && php bin/console prestashop:thumbnails:regenerate products --env=prod

prestashop:mail:generate <theme> <locale> [<coreOutputFolder> [<modulesOutputFolder>]] [--overwrite] renders the mail templates (HTML and text) of a theme for a locale into folders. Both folders must already exist: with a missing folder the command stops with Invalid core output folder. For the Classic theme and en-US it wrote 86 files (the core mails plus those of the modules, in a folder per module). prestashop:taxes:update-eu-tax-rule-groups refreshes the European VAT rule groups when the rates change (it modifies the database and was not run). prestashop:schema:update-without-foreign compares the Doctrine entities with the database:

bin/console prestashop:schema:update-without-foreign --dump-sql | head -4
# Updating database schema...
# Database schema not updated because force option is not set
# Showing required queries for update
# ALTER TABLE ps_image_type CHANGE image_fitment image_fitment VARCHAR(16) DEFAULT 'fit'

Without --force the command only prints the SQL. The fresh 9.2.0 install already showed a number of differences (column types, index names), so the output is a list to read, not a to-do list; applying it with --force on a production database is a decision for a backup and a maintenance window.

Themes and translations

prestashop:theme:enable <theme> activates an installed theme (the same as choosing it in Design > Theme and Logo) and prestashop:theme:export <theme> packages a theme with its dependencies as themes/<theme>.zip:

bin/console prestashop:theme:enable hummingbird
bin/console prestashop:theme:export classic
#  Your theme has been correctly exported: /var/www/html/themes/classic.zip      (1.9 MB)

The export copies the theme into a temporary folder under var/cache/prod first. On the first try it failed with Failed to copy ... to var/cache/prod/export-classic-..., with a different file each time, because another process was clearing var/cache at the same moment; the second try succeeded. Do not run a theme export in parallel with a cache:clear.

prestashop:translation:export-module <module> [<locale>] writes the XLF files of a module for one locale or all locales (the 9.1 changelog calls it prestashop:module:export-translations; the name in 9.2.0 is the first). The other two translation commands are checks for the core and for module developers: translation:check-fixtures (exit status 1 with findings on the test shop) and translation:find-duplicates. They are discussed with the module translations page.

Debug mode

prestashop:debug [on|off|1|0|true|false] shows the debug mode (_PS_MODE_DEV_) or switches it by rewriting the define in config/defines_custom.inc.php (or config/defines.inc.php). It is a convenience for classic hosts and it has a trap with the Docker image: there the custom file does not contain a literal true or false, it contains if ((bool) getenv('PS_DEV_MODE')) { define('_PS_MODE_DEV_', (bool) getenv('PS_DEV_MODE')); }. The command reads the value with a regular expression, so it reports Debug mode is: ON for a container started with PS_DEV_MODE=0, and prestashop:debug off replaces the environment-driven line with a fixed define('_PS_MODE_DEV_', false);, after which the environment variable no longer has any effect. On a Docker deployment, change the variable of the container instead, and use the command only for a classic installation. PS_DEV_MODE=false (a non-empty string) counts as true; use 0 or leave the variable unset.

bin/console prestashop:debug
#  [OK] Debug mode is: ON            (container with PS_DEV_MODE=1, correct)
bin/console prestashop:debug          # inside a container started with PS_DEV_MODE=0
#  [OK] Debug mode is: ON            (wrong: the regular expression matched the getenv() expression)

Linters and inspection

bin/console prestashop:linter:legacy-link
# There is no routes without _legacy_link settings
bin/console prestashop:linter:security-attribute find-missing
#  [WARNING] 3 routes are not configured with #[AdminSecurity] attribute:
#   * admin_mbo_module_cdc_error
#   * admin_mbo_recommended_modules
#   * admin_mbo_addons_module_upgrade
bin/console prestashop:linter:naming-convention
# prints the back-office routes whose names deviate from the convention (no path argument)

The three linters check back-office routes: that every route defines _legacy_link (needed for the old URLs and the menu), that route and controller names follow the convention, and that every controller route carries an #[AdminSecurity] attribute (list shows the configured ones, find-missing the others). They exit with status 1 when they find a problem, so they fit a CI job. They check the routes of the whole container, core and modules, so a failure can come from a module that is not yours; on the test shop legacy-link failed once because of a route of another module under test. The Symfony linters lint:twig <path>, lint:yaml <path> and lint:xliff <path> check a module’s templates, configuration and translations; lint:container failed on the test shop in dev mode with a logger configuration error (RotatingFileHandler ... $maxFiles ... string given) that is not a module problem.

prestashop:list:commands-and-queries [-d DOMAIN] [-f simple|regular] [--hasApiEndpoint=0|1] lists the CQRS classes registered on the command and query buses, which is the inventory a developer needs before writing a module that wraps core behaviour. The simple format prints class OK or class NOT OK (has an Admin API route or not); the regular format adds the type, the HTTP route and the description:

bin/console prestashop:list:commands-and-queries -d Customer --hasApiEndpoint=1 | head -5
# 177.
# Class: PrestaShop\PrestaShop\Core\Domain\Customer\Query\GetCustomerForViewing
# Type: Query
# API: GET /customers/{customerId}/details
# Gets detailed customer information for viewing in Back Office.

The prestashop:update:* commands and prestashop:generate:apidoc are maintenance tools for the core repository. On a release install, prestashop:update:hooks-documentation <dir> failed with File .../install-dev/data/xml/hook.xml has not been found because that file exists only in a source checkout.

Symfony commands that a shop developer uses

Command What it is for

cache:clear [--env=prod] [--app-id=admin-api]

Deletes the compiled containers, routes, translations and Twig templates of an environment and application, and warms the back-office and API caches. It took 7 to 9 seconds on the test shop. Run it after changing config/*.yml, a route, a service, an API resource or after installing a module that has any of them. Front-office containers are rebuilt on the first request.

cache:warmup

Warms an empty cache, to avoid a slow first request after a deployment.

cache:pool:list, cache:pool:clear

Lists and clears the Symfony cache pools (cache.app, cache.system, cache.validator, …​). These pools are separate from the Smarty cache and from the SQL cache of the performance page.

debug:router [--app-id=admin-api]

Lists routes. Pipe it to grep to find the routes of a module.

debug:container <id>

Describes a service: class, tags, autowiring, usages. For one handler of the Pocket Coffee module it showed the tag messenger.message_handler (method: handle, handles: ...GetRoastDate).

debug:messenger

Lists the messages that the bus can dispatch and the handlers of each, that is, every CQRS command and query of core and modules.

debug:event-dispatcher, debug:autowiring, debug:config, debug:translation, debug:twig, debug:validator, debug:form

Inspect what the container knows. debug:config framework messenger shows the transports.

debug:api-resource <class>

Lists the operations of an API Platform resource (interactive).

messenger:consume, messenger:stats, messenger:setup-transports, messenger:stop-workers

Run and observe asynchronous Messenger consumers, where a module defines a transport.

lint:twig, lint:yaml, lint:xliff, lint:container

Syntax and wiring checks.

doctrine:*, secrets:*, mailer:*

Doctrine and secrets tooling; doctrine:schema:* bypass PrestaShop’s own schema command, use prestashop:schema:update-without-foreign instead.

A module command

A module adds a command by shipping a class that extends Symfony\Component\Console\Command\Command, with the attribute #[AsCommand], and registering it as a service tagged console.command in config/services.yml. The Pocket Coffee module pcfhcli provides pcfhcli:order-digest, which prints how many orders were placed since a date and their total, as a table or as JSON for another program:

<?php

declare(strict_types=1);

namespace PrestaShop\Module\Pcfhcli\Command;

use DateTimeImmutable;
use Doctrine\DBAL\Connection;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(
    name: 'pcfhcli:order-digest',
    description: 'Prints how many orders were placed since a date and their total.'
)]
final class OrderDigestCommand extends Command
{
    public function __construct(private readonly Connection $connection, private readonly string $dbPrefix)
    {
        parent::__construct();
    }

    protected function configure(): void
    {
        $this
            ->addOption('since', 's', InputOption::VALUE_REQUIRED, 'First day to include (Y-m-d)', '1970-01-01')
            ->addOption('format', 'f', InputOption::VALUE_REQUIRED, 'table or json', 'table');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        $since = (string) $input->getOption('since');
        $date = DateTimeImmutable::createFromFormat('!Y-m-d', $since);
        if (false === $date || $date->format('Y-m-d') !== $since) {
            $io->error(sprintf('"%s" is not a valid Y-m-d date.', $since));

            return Command::INVALID;
        }

        $row = $this->connection->fetchAssociative(
            'SELECT COUNT(*) AS orders, COALESCE(SUM(total_paid_tax_incl), 0) AS total FROM ' . $this->dbPrefix . 'orders WHERE date_add >= :since',
            ['since' => $date->format('Y-m-d 00:00:00')]
        );

        if ('json' === $input->getOption('format')) {
            $output->writeln((string) json_encode(['since' => $since, 'orders' => (int) $row['orders'], 'total' => (float) $row['total']]));

            return Command::SUCCESS;
        }

        $io->table(['Since', 'Orders', 'Total (tax incl.)'], [[$since, $row['orders'], number_format((float) $row['total'], 2, '.', '')]]);

        return Command::SUCCESS;
    }
}

The service definition gives the command its two constructor arguments (the Doctrine connection by autowiring and the table prefix by binding) and tags it:

services:
  _defaults:
    autowire: true
    autoconfigure: true
    public: false
    bind:
      string $dbPrefix: '%database_prefix%'

  PrestaShop\Module\Pcfhcli\Command\OrderDigestCommand:
    tags:
      - { name: console.command }

The main class (pcfhcli.php) is the usual minimal module with isUsingNewTranslationSystem(); the module has no hooks and no install code. Install it, rebuild the cache and run the command:

bin/console prestashop:module install pcfhcli
bin/console list pcfhcli                      # nothing yet: the container was built before the module existed
bin/console cache:clear
bin/console list pcfhcli
# Available commands for the "pcfhcli" namespace:
#   pcfhcli:order-digest  Prints how many orders were placed since a date and their total.

bin/console pcfhcli:order-digest
#  ------------ -------- -------------------
#   Since        Orders   Total (tax incl.)
#  ------------ -------- -------------------
#   1970-01-01   5        308.00
#  ------------ -------- -------------------
bin/console pcfhcli:order-digest --since=2026-01-01 -f json
# {"since":"2026-01-01","orders":5,"total":308}
bin/console pcfhcli:order-digest --since=26-01-01 ; echo "exit=$?"
#  [ERROR] "26-01-01" is not a valid Y-m-d date.
# exit=2
bin/console pcfhcli:order-digest --env=prod -f json       # the same result from the production container

Things to know:

  • The console only sees the command after the container was rebuilt, so cache:clear after install is part of every deployment of a module that adds a command. A disabled module’s commands disappear from list after the next rebuild.

  • The command class is autoloaded through the module’s Composer autoloader. The module’s vendor/autoload.php must exist (composer dump-autoload -o before packaging).

  • Keep the command small and push the logic into a service that a controller and a command can both call; this is the same design as the CQRS handlers of the Admin API module.

  • A command that uses legacy classes (Configuration, Context, Product) in the CLI needs the legacy context initialised; the service prestashop.adapter.legacy_context_loader provides loadGenericContext(), which creates a fake employee and controller where the legacy code expects them.

  • The shop’s mail, logging and cache directories must be writable by the user that runs the command.

Scheduling a command

# /etc/cron.d/pocket-coffee  (user www-data)
*/15 * * * * www-data cd /var/www/html && php bin/console pcfhcli:order-digest --env=prod -f json >> var/logs/digest.jsonl 2>&1
0 3 * * *    www-data cd /var/www/html && php bin/console prestashop:search:index --env=prod

Always cd to the shop root, name the user, use --env=prod and redirect the output; add a lock (flock -n) for jobs that may overlap, such as the thumbnail regeneration.

A deployment script for Pocket Coffee

A script that makes a deployment repeatable. It uses only commands documented above; each of them was run on the test shop (the script as a whole was not).

#!/bin/sh
set -eu
cd /var/www/html
CONSOLE="php bin/console --env=prod"

# 1. code is already in place (git pull, composer install --no-dev -o); install or upgrade the modules
for m in pcfhapi pcfhcli; do
  $CONSOLE prestashop:module install "$m" || true     # exit status is 0 even on failure: check below
  $CONSOLE prestashop:module upgrade "$m"
done
$CONSOLE prestashop:module:list --active --simple | grep -qx pcfhapi
$CONSOLE prestashop:module:list --active --simple | grep -qx pcfhcli

# 2. declarative module settings and shop settings
$CONSOLE prestashop:module configure pcfhcli
$CONSOLE prestashop:config set PS_SHIPPING_FREE_PRICE --value=40

# 3. the container and the API container know the new classes
$CONSOLE cache:clear
$CONSOLE cache:clear --app-id=admin-api

# 4. the ERP client, created once
$CONSOLE prestashop:api-client create pocket-coffee-erp --scopes=product_read,customer_group_read,customer_group_write \
  --timeout=900 --secret-only >"${HOME}/.erp-secret" || echo "client exists"

# 5. search index
$CONSOLE prestashop:search:index

A secret written to a file such as ~/.erp-secret must be moved to the secret store and deleted at once; the script shows the mechanism, not a place to keep a secret.

Common problems

Symptom Cause and fix

Permission errors on the web site after running a command

The command ran as root and created files in var/cache owned by root. chown -R www-data: var and run as www-data.

A command prints nothing, with exit status 0, for every command, or /admin-api redirects with 302 Location: ../

A PHP file ending with exit (a module’s index.php) was loaded while the container was built, for instance by a resource: glob in config/services.yml or an index.php in src/ApiPlatform/Resources. Add exclude or remove the file, then delete var/cache/<env>/admin and var/cache/<env>/admin-api; cache:clear cannot run while the kernel dies.

A new command or service is not found

The container was compiled before the module was installed. Run cache:clear.

Failed to copy ... var/cache/prod/export-...

Another process cleared the cache during theme:export. Retry when nothing else runs.

Invalid core output folder or The directory ... does not exist or is not writable

The output folder of mail:generate or update:hooks-documentation must exist; create it first.

Constant _PS_MODE_DEV_ already defined on every command

A harmless warning of the Docker image with PS_DEV_MODE=1; it is on the error stream.

Debug mode is: ON in a production container

prestashop:debug parses defines_custom.inc.php; see Debug mode. Use the environment variable.

A command fails in lint:container with a logger error

An environment problem of the dev mode in 9.2.0 on the test shop; it is not caused by the module being linted.

A feature flag does not seem to switch

Look at the bracket in the Type column: the source in force may be an environment variable or .env rather than the database.

What was run and what was not

On a local PrestaShop 9.2.0 shop (official Docker image, MySQL 8.4, PHP 8.5.12, Apache 2.4.68) every command shown with output in this page was run, as the www-data user: the whole list and the --help of the 28 commands; the module actions with pcfhapi and pcfhcli including configure and the error cases; module:list with every flag; config with and without --lang and --shopId; feature-flag list, enable, disable and the unknown flag (the flag was restored); api-client create and delete with --secret-only and an invalid scope, api:generate-tracking-table; employee:create-admin with its error cases (the employee was removed afterwards); htaccess:generate without --force; search:index for one product and --full; thumbnails:regenerate stores; mail:generate; schema:update-without-foreign --dump-sql; theme:export; the three linters, list:commands-and-queries, about, debug:container, debug:messenger and cache:clear (with and without --app-id); the pcfhcli:order-digest command in dev and prod, and prestashop:debug in a throw-away copy of the shop. php -l was run on the module files.

Not run: employee:change-password (it sends mail), htaccess:generate --force, schema:update-without-foreign --force, taxes:update-eu-tax-rule-groups, translation:export-module for a module with translations on this page, prestashop:update:* and generate:apidoc (they need a source checkout), messenger:consume, prestashop:thumbnails:regenerate products for the full catalogue, and every command on PrestaShop 8.2. The scheduling and deployment examples were checked for syntax, not scheduled or executed as a unit.