iru-setup-php-prestashop
Scaffold a PrestaShop module for PrestaShop 8.x and 9.x in the repository root (the repository is the module directory, named after the module's technical name), or bring an existing module up to this
SKILL.md.claude/skills/iru-setup-php-prestashop/SKILL.md
GitHubSetup PrestaShop module
Generate a PrestaShop 8.x/9.x module in the repository root — the repository is the module directory, because
PrestaShop loads a module from modules/<module>/ and requires the directory, the main file, and $this->name to
match. The templates live in reference/ (too long to inline) and were generated into a throwaway module,
installed into real prestashop/prestashop:8 and :9 shops, and exercised end to end before being trusted (see
"Verification notes" at the end):
reference/module-templates.md— main class,config.xml,src/sample code, Symfony configuration page (form, controller, services, route, Twig), hook template,upgrade/README.md,index.phpguard, license headers, logo generator.reference/tooling-templates.md—composer.jsonand scripts,phpunit.xml.dist, unit/integration tests, PHPStan configs and stubs,.php-cs-fixer.dist.php, the Docker dev shop, the browser UI tests (package.json,playwright.config.ts,.ui-quality.yml, the screenshot/accessibility helper, theFooterMessagespec),.distignore,bin/package.sh,sonar-project.properties.
Read both files in full before writing anything. Copy templates verbatim, substituting only <placeholder> values.
The coding conventions they implement are the ones iru-php-code-one-task (reference/prestashop.md) enforces
later, and the gates iru-php-test, iru-php-coverage, iru-php-code-quality, iru-php-phpdoc run on the result
as-is.
Step 0 — Resolve inputs
Invoked stand-alone, or by iru-setup-php-repository, which passes resolved values through args as key: value
lines, one per line, e.g.:
name: Acme Greeting
slug: acmegreeting
vendor: acme
namespace: Acme\Greeting
description: Shows a configurable greeting above the shop footer.
license: AFL-3.0
developer-name: Jane Doe
developer-email: jane@example.com
organization-url: https://github.com/acme
php-version: 8.1
open-source: yes
visibility: public
ci: yes
distribution: store
sonar: cloud
sonar-organization: acme-github
sonar-project-key: acme_acmegreeting
sonar-host-url: https://sonarcloud.io
tab: front_office_features
accessibility: enforce
mode: new
on-existing: ask
Parse them now; every key found is resolved and not asked again in Step 2. Recognized keys: name (the module's
displayName), slug (the technical name), vendor, namespace, description, license (SPDX id, or
proprietary/UNLICENSED), developer-name, developer-email, organization-url, php-version (floor,
major.minor), open-source (yes/no), visibility (public/private/internal), ci (yes/no),
distribution (none/internal/store — only changes Step 8's packaging notes; bin/package.sh is always
written because it is also how a module is installed on any shop), sonar (cloud/self-hosted/none) plus
sonar-organization/sonar-project-key/sonar-host-url, tab, accessibility (enforce/report/off — the
automated accessibility level of the browser UI tests, written to .ui-quality.yml; default enforce), mode
(new/existing), and on-existing
(ask/keep/update/replace, default ask). Ignore any other key (e.g. ci-tests, pages belong to the
workflows skill — the Integration suite is always scaffolded; CI decides whether to run it). If args is absent or
not in this format, treat everything as unset.
mode: existing means an established repository: Step 1 announces an update but still asks per existing file
unless on-existing says otherwise — mode: existing alone never means "update silently".
Step 1 — Survey the repository and decide per existing file
Look at the repository root for an existing module: a *.php whose class extends Module/PaymentModule/
CarrierModule, config.xml, composer.json. When one exists, read $this->name, $this->version, $this->tab,
$this->author, ps_versions_compliancy, the registered hooks, whether isUsingNewTranslationSystem() returns
true, and composer.json's autoload.psr-4 — these become the defaults (or resolved values) for Step 2, and the
technical name must not change on an existing module (shops identify it by name).
The files this skill owns: <module>.php, config.xml, logo.png, composer.json, config/services.yml,
config/routes.yml, the sample src/ classes, the Twig/Smarty templates, upgrade/README.md, every index.php
guard, phpunit.xml.dist, tests/bootstrap.php, the sample unit/integration tests,
tests/Integration/docker-compose.yml, phpstan.dist.neon, tests/phpstan/phpstan.neon, tests/phpstan/stubs/*,
.php-cs-fixer.dist.php, .distignore, bin/package.sh, sonar-project.properties, .ui-quality.yml,
package.json, playwright.config.ts, tests/e2e/support/ui-screenshots.ts, tests/e2e/specs/FooterMessage.spec.ts.
Announce one of:
- None exist: "Creating the PrestaShop module from scratch — none of this skill's files exist yet." Go to Step 2.
- At least one exists: "Updating an existing module — found: <list>." Warn that each is handled
individually, then resolve one action per file:
on-existingiskeep/update/replace→ apply it to every existing file without asking (still print the warning).Otherwise ask with
AskUserQuestion, batching up to 4 files per call, naming any concrete contradiction with this run's choices in the question (e.g. "composer.jsonhas noprepend-autoloader: false", "nolint:reportscript"):- Update (Recommended) — merge the template in: add missing scripts (keep the shared names exactly:
test,test:coverage,test:integration,lint,lint:report,format,format:check,package), autoload entries, config options,index.phpguards, phpunit suites; correct what this skill owns; keep everything else (extra dependencies, hooks, services, routes, rules). Show the diff before writing. - Keep as is — untouched; listed with its mismatches in Step 8.
- Replace — regenerate from the template; customizations are lost (recoverable from git only).
- Stop (first batch only) — end without changing anything.
For the main class, real
src/code, templates, and tests of an existing module recommend Keep as is and say why: they are the module itself — Update can only add a missing piece the user names (e.g. theindex.phpguards,isUsingNewTranslationSystem()), and Replace would overwrite real code with the greeting sample. On an existing module, never add the sample feature (greeting classes, form, controller, route, hook) unless the user asks — add only the tooling and any guards/headers that are missing.- Update (Recommended) — merge the template in: add missing scripts (keep the shared names exactly:
Ask about
sonar-project.propertiesonly after Step 2 resolvedsonar; withsonar: none, Update/Replace mean removing it..ui-quality.yml: Update adds any missing key and setsaccessibilityto this run's value only when it came fromargsor Step 2 (an existing level and narroweddirections/themesare otherwise kept); Keep leaves it (Step 8 names a level that differs from this run's); Replace writes the contract defaults with this run's level.package.jsonUpdate merges thetest:e2escript and the two dev dependencies into an existing one. TheFooterMessagespec is part of the greeting sample: on an existing module write only the UI test tooling (.ui-quality.yml,package.json,playwright.config.ts, the helper) and recommend Keep as is for an existingplaywright.config.*ortests/e2e/.
Step 2 — Collect the required inputs
Skip every key Step 0 resolved; prefer values read in Step 1 and confirm them in one line instead of asking. Otherwise ask directly (plain conversation):
- name — the human-readable
displayName, e.g.Acme Greeting. - slug (technical name) — derived from the name and confirmed: lowercase letters and digits only, starting with a
letter (
acmegreeting). Reject hyphens. Discourage underscores: PrestaShop's docs note translation domains don't work with them (Modules.<ModuleClass>.Adminis built from the name). It becomes the directory name, the main file (<slug>.php),$this->name, theConfiguration-key prefix (uppercased), and the route prefix. If the repository directory isn't named<slug>, say that PrestaShop needs the module undermodules/<slug>/(the zip frombin/package.shalready nests it correctly) — don't rename the working copy. - vendor — Composer vendor (default: the git remote's owner, lowercased) →
composer.jsonname=<vendor>/<slug>. - namespace — default
<Vendor>\<Name>in StudlyCase (Acme\Greeting);PrestaShop\Module\<Name>is PrestaShop's own convention and also fine. - description, developer name, developer email, organization URL.
- tab (
AskUserQuestion, headerCategory) — the module-manager category:front_office_features(Recommended for a storefront feature),administration,payments_gateways,shipping_logistics, or Other (any value from PrestaShop's module tab list).
Then the license (AskUserQuestion) unless resolved: AFL-3.0 (Recommended) — "the license PrestaShop's own
modules use; Addons-compatible" / OSL-3.0 — "PrestaShop core's license; note it is not in the PrestaShop Addons
checklist's list of compatible licenses (Apache, AFL, MIT, BSD, ISC, EUPL)" / MIT / No license (proprietary —
common for paid Addons modules) / Other (ask display name, SPDX id, URL). The choice selects the header in
module-templates.md "License headers" and composer.json's license (proprietary for no license).
Then the PHP floor unless php-version was supplied: the module runs on the shop's PHP, so the floor is the
highest version every targeted PrestaShop major supports — look up the "System requirements" pages for 8.x and 9.x
on devdocs.prestashop-project.org (at the time of writing PrestaShop 8.x supports PHP 7.2–8.1 and 9.x requires
8.1 or newer, so the 8+9 floor is 8.1; the official prestashop/prestashop:8 image runs 8.1 and :9 runs 8.5).
Ask with AskUserQuestion (header PHP floor): the 8+9 floor (Recommended) vs. a higher floor that drops
PrestaShop 8 support. It sets <php-version> (require.php: >=<php-version>, config.platform.php: <php-version>.0) and <ps-min>: 8.0.0 when the floor is ≤ 8.1, otherwise 9.0.0 — say so when the user picks the
higher floor.
Then accessibility unless resolved (AskUserQuestion, header Accessibility, "Automated accessibility checks
in UI tests?"): Enforce (Recommended) — "axe-core violations fail the browser UI tests" / Report only —
"listed in the test output, never failing" / Off — "not run; the coding rules still apply". Written to
.ui-quality.yml.
Then open-source (Yes/No) and sonar, exactly like iru-setup-typescript-library Step 2: resolve visibility
(from args, else gh repo view --json visibility -q .visibility, else ask; internal = private; open source +
private is contradictory → ask which is right); recommend SonarCloud only for a public open-source repository,
otherwise None (SonarCloud is free only for public open-source projects) with self-hosted SonarQube second;
with ci: no, recommend None because nothing would send reports. For cloud/self-hosted collect
sonar-organization (suggest <owner>-github), sonar-project-key (suggest <owner>_<repo>),
sonar-host-url.
distribution is not asked here when absent: it only affects Step 8's notes (default none).
Step 3 — Infer repository information
- Owner/repo from
git remote get-url origin(bothgit@host:owner/repo.gitandhttps://host/owner/repo.gitforms) — used for the vendor default and the Sonar defaults. No remote → ask only if Sonar needs it. <year>=date +%Y.<ModuleClass>=<slug>with only its first letter uppercased;<MODULE>=<slug>uppercased;<NamespaceJson>=<Namespace>with each\doubled.- Tools:
command -v composer php docker. Without a hostcomposer/php, run every Composer/PHP command of Steps 6–7 through Docker from the repository root (docker run --rm -v "$PWD":/app -w /app composer:2 ...for Composer; an image with PCOV for coverage —php:<floor>-clipluspecl install pcov, or any PHP image already carrying PCOV/Xdebug). No Docker either → write the files, skip Step 7's runs, and report them as unverified.
Step 4 — Dependency versions
Nothing is pinned by hand. After composer.json exists with config.platform.php set (Step 6), Step 7 runs
composer require --dev phpunit/phpunit phpstan/phpstan friendsofphp/php-cs-fixer prestashop/php-dev-tools, so
Composer resolves the newest versions compatible with the PHP floor and writes caret constraints. Fallback when
Packagist is unreachable — the versions this skill resolved with a 8.1.0 platform in October 2026; write them into
require-dev and say so in Step 8:
| Package | October 2026 (platform 8.1) |
|---|---|
phpunit/phpunit |
^10.5 (10.5.66; PHPUnit 11 needs PHP 8.2, 12 needs 8.3) |
phpstan/phpstan |
^2.3 (2.3.1) |
friendsofphp/php-cs-fixer |
^3.95 (3.95.27) |
prestashop/php-dev-tools |
^5.0 |
npm (browser UI tests only — never shipped): npm view <package> version for @playwright/test (fallback 1.64.1,
written ^1.64.0), @axe-core/playwright (4.14.0, ^4.14.0) and @types/node (^24.0.0, Node 24 LTS), written as
^<major>.<minor>.0.
Don't add prestashop/autoindex or prestashop/header-stamp (see tooling-templates.md). Add no production
dependency: everything in require ships inside the module zip and loads next to the shop's own vendor.
Step 5 — Templates
All in reference/. Substitute placeholders only. Decisions that change a template:
- License:
<license-header>/<license-short-header>permodule-templates.md. <ps-min>:9.0.0for a floor above 8.1. The controller stays on the 8+9-compatible base class and annotation unless the user asks for the 9-only form (documented there, not shop-verified).sonar: none: nosonar-project.properties.- Existing module: tooling only (Step 1); the sample files are written only on a from-scratch run.
accessibility: the one word on.ui-quality.yml'saccessibility:line.- License in
package.json: the SPDX id, orUNLICENSEDfor no license.
Step 6 — Write the files
Apply Step 1's per-file decision (absent → create; Update → merge and show the diff; Keep → untouched, record mismatches; Replace → overwrite). On a from-scratch run, create:
<module>.php config.xml logo.png composer.json index.php
config/{services.yml,routes.yml,index.php}
src/{index.php,Settings/{MessageSettings,MessageViolation,SettingsKeys,index}.php,Presenter/{FooterMessagePresenter,index}.php,
Form/{MessageConfigurationType,MessageDataConfiguration,MessageFormDataProvider,index}.php,Controller/index.php,
Controller/Admin/{ConfigurationController,index}.php}
views/index.php views/templates/index.php views/templates/admin/{configure.html.twig,index.php}
views/templates/hook/{footer.tpl,index.php}
upgrade/{README.md,index.php} translations/index.php
phpunit.xml.dist phpstan.dist.neon .php-cs-fixer.dist.php .distignore bin/package.sh [sonar-project.properties]
.ui-quality.yml package.json playwright.config.ts tests/e2e/support/ui-screenshots.ts tests/e2e/specs/FooterMessage.spec.ts
tests/bootstrap.php tests/Unit/Settings/MessageSettingsTest.php tests/Unit/Presenter/FooterMessagePresenterTest.php
tests/Integration/{ModuleInstallTest.php,docker-compose.yml}
tests/phpstan/phpstan.neon tests/phpstan/stubs/{constants.php,prestashop.stub.php}
Generate logo.png with the PHP one-liner in module-templates.md (host PHP or php:*-cli in Docker) and
chmod +x bin/package.sh. Don't write LICENSE, .gitignore, README.md, CHANGELOG.md, or docs/ — other
skills own them.
Step 7 — Install and verify
Run from the repository root (through Docker per Step 3 when there is no host PHP), stopping to report on the first failure rather than editing templates to force a pass:
composer validate --no-check-publish, then the Step 4composer require --dev ...(fallback versions +composer updateif Packagist is unreachable).php -lon every*.phpoutsidevendor/.composer test— the Unit suite passes (10 tests on the sample).composer test:coveragewith PCOV or Xdebug loaded — writesbuild/logs/clover.xmlandbuild/logs/junit.xml; read the line rate (metricsof the Cloverprojectelement): 100% on the sample, never below 80%. No coverage driver → report "unverified: no PCOV/Xdebug", not 0%.composer lintandcomposer lint:report— PHPStan (unit config) reports no errors;build/logs/phpstan.jsonexists.composer format:check— PHP-CS-Fixer finds 0 fixable files.composer package(needsrsync,zip,composer) —unzip -l build/<module>-<version>.ziplists<module>/<module>.php,<module>/vendor/autoload.php, and notests/,package.json,node_modules/orplaywright.config.ts.- Browser UI test tooling (needs Node 20+):
npm install,npx playwright install chromium, thennpx playwright test --listlistsFooterMessage.spec.tsand a plainnpm run test:e2ereports it skipped (noPS_BASE_URL) — that proves the config, helper and spec load. No Node → "UI tests unverified (no Node)". - Shop check — offer it with
AskUserQuestion(Recommended when Docker is running; it takes a few minutes and pullsprestashop/prestashopandmariadbimages):composer shop:up(with a freePS_PORTif 8080 is taken;PS_VERSION=8for 8.x),composer shop:install,composer test:integration,composer lint:shop, the browser UI testsPS_BASE_URL=http://localhost:<PS_PORT> npm run test:e2e(check.ui-screenshots/component/FooterMessage/holdsdefault--<direction>--<theme>.pngfor every.ui-quality.ymldirection × theme; the right-to-left ones use the client-side fallback unless an RTL language was installed andPS_RTL_LANGset), thencomposer shop:down. After it, rungit diff --stat config.xml— PrestaShop rewritesconfig.xmlin a mounted module directory on install; restore the template version (git checkout config.xmlif it was committed, else rewrite it). Declined or Docker unavailable → "shop check not run" in Step 8.
Step 8 — Report
Start with the mode (created from scratch / updated an existing module). Then: technical name, display name,
namespace, <vendor>/<module>, version 0.1.0-dev, tab, license, PHP floor and ps_versions_compliancy (and where
the floor came from), and the resolved dev-dependency versions (live or fallback). Per file: created / updated /
replaced / kept as is / removed, with mismatches for kept files and what a Replace may have lost. Each Step 7
check: pass/fail/unverified with the reason (coverage %, PHPStan error counts, fixable-file count, zip path, shop
check result per PrestaShop major, browser UI tests: screenshot count, axe violations as rule id × count, or
"unverified (no shop)"). .ui-quality.yml (created / updated / replaced / kept as is) and the accessibility level it
holds.
State open-source, visibility (and its source), sonar, and distribution:
distribution: store— PrestaShop Addons has no upload API: the zip fromcomposer package(CI attaches it to the GitHub Release) is uploaded manually in the Addons seller account, where the validator runs; also run it through https://validator.prestashop.com before submitting. Remind the user of the Addons technical checklist points this scaffold does not settle: a compatible license (OSL-3.0 isn't on its list), English code and wordings, end-user documentation in adocs/folder of the zip (the repository'sdocs/is the Antora site and is excluded by.distignore— ship user docs another way, e.g. a PDF added at packaging time), no external downloads at runtime, tokens on any AJAX/cron entry point.distribution: internal— the zip attached to GitHub Releases is installed through Modules → Module Manager → Upload a module or by unzipping intomodules/and runningphp bin/console prestashop:module install <module>aswww-data.
Warn explicitly:
- Review every generated file; replace the greeting sample with the module's first real feature, keeping the
src/split (pure, unit-tested logic vs. PrestaShop/Symfony glue checked by PHPStan and the Integration suite). - Add a
LICENSEfile matching the headers (iru-check-licensecan create it and backfill headers); replace the placeholderlogo.png. -devversions and PrestaShop:ps_module.versionisVARCHAR(8), so a shop running a development build stores0.1.0-deand keeps offering an "Upgrade" — harmless in a dev shop, but never install a-devzip on a real shop; releases (x.y.z) are unaffected.- Every release that adds a hook, configuration key, or table ships an
upgrade/upgrade-<x.y.z>.php(upgrade/README.md);iru-php-bump-versionupdates$this->versionandconfig.xmlbut never writes upgrade scripts. - Run console commands in the dev shop as
www-data; running them as root breaks the back office with permission errors onvar/. - No
.gitignorewas written:vendor/,build/,.phpunit.cache/,.php-cs-fixer.cache,config_*.xml, and release zips must be ignored (andconfig.xmlkept tracked) —iru-setup-php-gitignoredoes it. - With Sonar on, a
SONAR_TOKENsecret and the Sonar project must exist; CI runscomposer test:coverageandcomposer lint:reportbefore the scanner. - Browser UI tests need a running shop with the module installed and
PS_BASE_URL; without it they skip and are reported unverified. For real right-to-left captures install an RTL language in the test shop and exportPS_RTL_LANG=<iso>.node_modules/,.ui-screenshots/,test-results/andplaywright-report/belong in.gitignore(iru-setup-php-gitignore);.distignorealready keeps all of it out of the zip.
Verification notes
Built in October 2026 by generating a module from these exact templates in $TMPDIR/iru-verify/php-setup/prestashop/
(platform PHP 8.1.0, resolved PHPUnit 10.5.66, PHPStan 2.3.1, PHP-CS-Fixer 3.95.27, php-dev-tools 5):
php -lclean; Unit suite 10/10 on PHP 8.4 (php:8.4-cli+ PCOV) and 8.5; coverage 100% lines of the measuredsrc/(Clover + JUnit written);composer lint/lint:report(unit config) no errors (also clean atmax);composer format:check0 of 29 files fixable;composer validateclean.- PrestaShop 9.2.0 (
prestashop/prestashop:9, PHP 8.5, MariaDB 11) and 8.2.8 (:8, PHP 8.1):php bin/console prestashop:module install <module>succeeded; hookdisplayFooterand both configuration defaults registered; uninstall → reinstall clean; Integration suite passed inside both shops; shop-level PHPStan (tests/phpstan/phpstan.neon,_PS_ROOT_DIR_) no errors on both; logged in to the back office as the image's demo employee: the module manager's Configure button reached the Symfony page throughgetContent()(8.x legacyAdminModules&configure=302 → route; 9.x direct), the form rendered with stored values, an invalid value showed the translated error, a valid one saved (flash "Successful update.",ps_configurationupdated) and appeared on the storefront footer. - The zip from
composer package(built incomposer:2+ rsync), unzipped into a clean 9.x shop and installed from it, passed the same back-office/storefront checks. - Fixed during verification and documented in the references:
blank_line_after_opening_tagbefore headers;_legacy_controller: AdminModulesin a module route creates anAdminModulestab and breaks every module's legacy Configure page on 8.x; prose mentioning the annotation name with@adds an empty security rule (HTTP 500 on 8.x);$this->trans()in a module controller fails on 8.x (inject the translator); console as root breaksvar/permissions;ps_module.versiontruncates-devversions; PrestaShop rewritesconfig.xmlin a mounted working copy. - Browser UI tests (issue 16, October 2026) in
$TMPDIR/iru-verify/php/iru-setup-php-prestashop/:npm installresolved@playwright/test1.64.1,@axe-core/playwright4.14.0,@types/node24.x;tsc --noEmit(strict) clean on the config, helper and spec;npm run test:e2ewithoutPS_BASE_URL→ 1 skipped; withPS_BASE_URLpointing at a static stand-in page carrying the hook's markup, the spec passed (4 capturescomponent/FooterMessage/default--{ltr,rtl}--{light,dark}.png, axe clean);rsync --exclude-from=.distignoredroppedpackage.json,node_modules/,playwright.config.ts,tests/,.ui-quality.ymland the Playwright output. Not verified: the spec against a real 8.x/9.x shop (the Docker VM had no disk space left this run), an installed RTL shop language (PS_RTL_LANG), and the classic theme's markup under axe. - Not verified: the 9-only controller variant (
PrestaShopAdminController+ attribute);composer test:integration/lint:shop/shop:*invoked through the composer scripts themselves (their exactdocker composecommands were run by hand); submission to validator.prestashop.com or Addons; GitHub Actions runs (the workflows skill's job).