diff --git a/plugins/thelia/.claude-plugin/plugin.json b/plugins/thelia/.claude-plugin/plugin.json index 7185007..4e79ffd 100644 --- a/plugins/thelia/.claude-plugin/plugin.json +++ b/plugins/thelia/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "thelia", "description": "Develop Thelia 3 with Claude Code: skills and agents for modules, Propel ORM, the API Platform bridge, the Flexy front-office, the default-twig back-office, and Thelia 2 to 3 migration.", - "version": "0.5.0", + "version": "0.6.0", "author": { "name": "Thelia" }, diff --git a/plugins/thelia/skills/ddev/SKILL.md b/plugins/thelia/skills/ddev/SKILL.md index d7d9f11..e5bad0d 100644 --- a/plugins/thelia/skills/ddev/SKILL.md +++ b/plugins/thelia/skills/ddev/SKILL.md @@ -111,7 +111,6 @@ hooks: - exec-host: ddev mysql -uroot -proot -e "CREATE DATABASE IF NOT EXISTS db_test; GRANT ALL PRIVILEGES ON db_test.* TO 'db'@'%'; FLUSH PRIVILEGES;" - exec: php Thelia cache:clear - exec: php bin/console tailwind:build - - exec: symfony run --daemon bin/console messenger:consume async -vv ``` Key points: diff --git a/plugins/thelia/skills/ddev/references/hooks-et-services.md b/plugins/thelia/skills/ddev/references/hooks-et-services.md index 9bd386d..9113c57 100644 --- a/plugins/thelia/skills/ddev/references/hooks-et-services.md +++ b/plugins/thelia/skills/ddev/references/hooks-et-services.md @@ -48,14 +48,6 @@ hooks: - exec: bash -c "cd templates/backOffice/default-twig && npm install && npm run build" ``` -### Hook: Start Messenger - -```yaml -hooks: - post-start: - - exec: symfony run --daemon bin/console messenger:consume async -vv -``` - **Note:** `exec` commands run inside the web container. `exec-host` commands run on the host machine. ## Mailpit (email testing) diff --git a/plugins/thelia/skills/thelia3-backoffice-twig/SKILL.md b/plugins/thelia/skills/thelia3-backoffice-twig/SKILL.md index 3d1892b..10c17e2 100644 --- a/plugins/thelia/skills/thelia3-backoffice-twig/SKILL.md +++ b/plugins/thelia/skills/thelia3-backoffice-twig/SKILL.md @@ -33,6 +33,16 @@ The back-office uses two translation systems at once, and you need to know which Both have to be populated to cover every string on a screen. +You get the second one by default, and not by choice: the core aliases `Symfony\Contracts\Translation\TranslatorInterface` to `Thelia\Core\Translation\Translator`, so a constructor that type-hints the interface is wired to Thelia's translator whatever the surrounding code looks like. That translator never loaded `translations/messages..php`, so a key defined for the templates comes back unchanged from PHP. When a service needs the same catalog the templates use, ask for the Symfony translator explicitly: + +```php +public function __construct( + #[Autowire(service: 'translator')] + private readonly TranslatorInterface $translator, +) { +} +``` + Pitfalls: - `|trans` resolves against the request locale, not the session or `default_locale`. A listener sets the request locale on `/admin` routes so the admin renders in the chosen language. diff --git a/plugins/thelia/skills/thelia3-tooling/SKILL.md b/plugins/thelia/skills/thelia3-tooling/SKILL.md index 7d3e1c8..cd51cdb 100644 --- a/plugins/thelia/skills/thelia3-tooling/SKILL.md +++ b/plugins/thelia/skills/thelia3-tooling/SKILL.md @@ -1,6 +1,6 @@ --- name: thelia3-tooling -description: "Operational gotchas when developing and testing Thelia 3: the Thelia console versus bin/console, a stale PHPStan result cache on Propel classes, PHPUnit 11 failing on deprecated XML, JWT keys for the API test suite, front-office assets built by bin/install versus the back-office theme built by hand, a missing GitHub token that fails an install with an unrelated error, and LiveComponents answering 404. Use when a Thelia command, an install, the test suite, PHPStan, JWT auth, or a theme's assets behave in a way the code does not explain." +description: "Operational gotchas when developing and testing Thelia 3: the Thelia console versus bin/console, a stale PHPStan result cache on Propel classes, a PHPStan config whose includes merge paths instead of replacing them, PHPUnit 11 failing on deprecated XML, JWT keys for the API test suite, front-office assets built by bin/install versus the back-office theme built by hand and the extra steps a production deployment needs, a missing GitHub token that fails an install with an unrelated error, and LiveComponents answering 404. Use when a Thelia command, an install, a deployment, the test suite, PHPStan, JWT auth, or a theme's assets behave in a way the code does not explain." --- # Thelia 3 tooling @@ -24,6 +24,31 @@ PHPStan caches results, and the cache can go stale on generated Propel `Base` cl vendor/bin/phpstan clear-result-cache ``` +## A PHPStan `includes:` merges `paths`, it does not replace them + +A second PHPStan config that includes the main one inherits every parameter, and array parameters are merged rather than overwritten. Include a root config that analyses `core`, add your own `paths`, and the run analyses both: the extra config is a superset of the first, not a narrower scope. + +```neon +includes: + - phpstan.neon # already declares paths and a level +parameters: + paths: + - templates/backOffice/default-twig/src +``` + +Scalars behave the other way round and simply override, so a config that does not restate `level` silently runs at the included one. The two combine into a gate you believe is scoped to one directory at one level while it re-analyses everything at a level someone else chose. + +Mark the key with `!` to replace instead of append: + +```neon +parameters: + level: 5 + paths!: + - templates/backOffice/default-twig/src +``` + +`vendor/bin/phpstan dump-parameters -c ` prints the merged result and settles the question without running an analysis. Relative paths in it resolve against the directory of the config file that declares them, not the working directory. + ## PHPUnit 11 fails on deprecated XML A deprecated attribute in `phpunit.xml` (for example `cacheResultFile`, or the old `listeners` element) makes PHPUnit 11 exit with code 1 even when every test passes. Inside a `composer test` chain this looks like a test failure but is a configuration warning. Migrate the config: use `cacheDirectory`, and move listeners to `extensions` and bootstrap entries. @@ -44,6 +69,8 @@ cd templates/backOffice/default-twig && npm install && npm run build An admin that renders with no styling is this missing build, not a broken configuration. Rebuild it after a `composer update` on the theme too, since the update replaces the package directory and takes `dist/` with it. +Deploying to production adds two steps that no install script runs for you: `tailwind:build --minify` for the stylesheet, and `asset-map:compile` to write the mapped assets into the public directory. AssetMapper's dev server, which serves them on the fly during development, follows the debug flag and is off in production, so a front office deployed without the compile loads with no CSS and no JavaScript. + ## A missing GitHub token fails an install on an unrelated message Composer needs a GitHub token to fetch `thelia/thelia-recipes`. Without one, Symfony Flex does not stop: it falls back to auto-generated recipes, so Thelia's `config/packages/*.yaml` files are never written. The install proceeds and dies much later on a message that names none of this: diff --git a/plugins/thelia/skills/thelia3/SKILL.md b/plugins/thelia/skills/thelia3/SKILL.md index 0ba6f6f..4636924 100644 --- a/plugins/thelia/skills/thelia3/SKILL.md +++ b/plugins/thelia/skills/thelia3/SKILL.md @@ -265,6 +265,9 @@ final class MyModule extends BaseModule | `ConfigQuery::read()` returns stale value | PSR `thelia_config` cache not invalidated by direct SQL UPDATE | `ConfigQuery::write()` or `initCacheConfigs(true)`; `php Thelia cache:clear` | | `SecurityContext::isGranted([A, B], ...)` | combines resources in **AND** | for OR, iterate resource by resource | | `_or()` Propel | switches **the entire WHERE** to OR | `condition()` + `combine([...], 'OR')` | +| A JSON or webhook route answers a themed 404 page | `ViewListener` renders a themed view for any controller that does not return a `Response`, and `ErrorListener` turns the resulting `NotFoundHttpException` into the theme's error page | `defaults: ['ignore_thelia_view' => true]` on the route, and return a real `Response` | +| Injected `TranslatorInterface` cannot see the theme catalog | the core aliases `TranslatorInterface` to `Thelia\Core\Translation\Translator`, which carries module and validator catalogs, not `translations/messages..php` | `#[Autowire(service: 'translator')]` when you want the Symfony translator | +| `app.flashes()` empty on a front page | templates rendered by `TwigParser` get a four-property `app` stub that shadows Symfony's `AppVariable` | read flashes from `app.session.flashBag` | ## 6. Symfony-native evolution candidates diff --git a/plugins/thelia/skills/thelia3/references/front-office.md b/plugins/thelia/skills/thelia3/references/front-office.md index 7798d06..5ccfadc 100644 --- a/plugins/thelia/skills/thelia3/references/front-office.md +++ b/plugins/thelia/skills/thelia3/references/front-office.md @@ -53,6 +53,32 @@ internal: Asking for one of these by name through the catch-all returns 404 instead of a half-rendered page. A controller rendering the same template is unaffected - only requests that *name* a view are filtered. The file is optional: absent, nothing is filtered. A malformed `internal:` key throws a `TemplateException` at boot rather than failing silently. +### Routes that are not views: `ignore_thelia_view` + +`Thelia\Core\EventListener\ViewListener` subscribes to `kernel.view`, so it runs only when a controller returns something that is not a `Response`, and only on the main request. Unless the request carries the `ignore_thelia_view` attribute it hands over to `Thelia\Core\View\ViewRenderer`, which reads the `_view` request attribute and renders that template from the active theme. + +A module route that declares no `_view` reaches the renderer with an empty view name: it logs `No view found` through `Tlog` and throws a `NotFoundHttpException`. `Thelia\Core\EventListener\ErrorListener` then catches that exception and, in production with the shop set to show its error page, replaces it with the theme's error template. So the endpoint answers a themed 404 page, and the only trace is one line in the Thelia log. A caller expecting JSON gets a shop page and no explanation. + +Declare the flag in the route defaults for anything that is not a themed page: JSON and AJAX endpoints, webhooks, callbacks, file downloads. + +```php +#[Route( + '/mymodule/webhook', + name: 'mymodule_webhook', + defaults: ['ignore_thelia_view' => true], +)] +``` + +One flag, three listeners, all keyed on the same request attribute: + +| Listener | Without the flag | With it | +|---|---|---| +| `ViewListener` | renders the themed view | stands down | +| `ControllerListener::adminFirewall` | refuses a `BaseAdminController` action to a visitor who is not logged into the admin | stands down, so the route has to guard itself | +| `ErrorListener` | logs the exception and, in production, swaps it for the theme's error page | stands down, so the exception surfaces as Symfony handles it | + +The counterpart is that opting out obliges you to return a `Response`: nothing renders a view for you any more, and `HttpKernel` throws `ControllerDoesNotReturnResponseException` on a controller that returns nothing. Nothing in the core ever sets the flag - it comes from route configuration only. + ### Module template overrides Place `{module}/templates/frontOffice/flexy/mytemplate.html.twig`. The kernel scans `{module}/templates/{templateSubdir}/` at boot and adds it via `addPath()` to the TwigParser `FilesystemLoader` **after** the active theme. @@ -91,6 +117,29 @@ Extensions from TwigEngine (`vendor/thelia/modules/TwigEngine/Extension/`): | `psesByProduct(productId)` | `PSEExtension` | JSON PSE data | | `filters_count(filters)` | `FilterExtension` | Active filter count | +### The `app` variable is not Symfony's `AppVariable` + +`TwigParser::render()` (in the TwigEngine module, `Template/TwigParser.php`) injects its own `app` into the render context, alongside `locale`, `lang_code`, `lang_id` and `current_url`: + +```php +'app' => (object) [ + 'environment' => ..., + 'request' => ..., + 'session' => ..., // null when the request carries none + 'debug' => ..., +], +``` + +A context variable shadows a Twig global, so every template the parser renders - the page, whatever it extends, whatever it includes - sees that stub instead of `Symfony\Bridge\Twig\AppVariable`. The stub is a plain object with four properties and no methods, and `strict_variables` is off outside the test environment, so `app.flashes(...)`, `app.user`, `app.token` and `app.locale` all resolve to null without a word. A flash block written the Symfony way renders empty and nothing says why. + +Read flashes through the session instead, which is what the theme does: + +```twig +{% for message in app.session ? app.session.flashBag.get('error') : [] %} +``` + +The back-office is not in the same position: its controllers render through the injected Twig `Environment` rather than through the parser, so their templates do get the real `AppVariable` and `app.flashes` works there. Which `app` you get depends on who renders the template. + ## 3. `resources()` in Twig ```twig @@ -321,6 +370,19 @@ There is **no bundler, no `package.json`, no `webpack.config.js` and no `node_mo Public output is AssetMapper's, under `/assets/frontOffice/{theme}/`. The old `templates-assets/{theme}/dist` symlink and the `dist` entry in `template.xml` no longer apply to Flexy. +### Building for production + +What `bin/install` runs is a development build. A production deployment needs two more steps: + +```bash +php bin/console tailwind:build --minify +php bin/console asset-map:compile +``` + +`tailwind:build` on its own writes unminified CSS. `asset-map:compile` writes the mapped assets and their manifest into the public directory; AssetMapper's dev server, which serves them on the fly otherwise, defaults to the debug flag and is therefore off in production. Skip the compile and the front office loads with no CSS and no JavaScript, on a page that is otherwise fine. + +Run both again after any deployment that touches templates. Tailwind v4 scans the Twig sources, so a utility class used for the first time in a template only reaches the stylesheet once a build has seen it. + ## 12. Virtual product download Flexy serves virtual product files from the customer account: @@ -374,3 +436,7 @@ To customize: override `sitemap.html.twig` in the child template, or inject `Sit | `provide()` / `inject()` undefined on PHP 8.3 | ux-twig-component resolves to 2.x; the theme's `ComponentContextExtension` supplies them - do not add your own | | Component named with `name:` or `template:` | prefix is empty and the template is derived from the class path - write the attribute bare | | A form rendering as bare Symfony markup | no theme is global any more; add `{% form_theme form with flexy_form_themes only %}` | +| A module JSON or webhook route answers a themed 404 page | it is still going through `ViewListener`; add `defaults: ['ignore_thelia_view' => true]` | +| `app.flashes()` or `app.user` renders nothing | the parser's `app` is a four-property stub, not `AppVariable` - read flashes from `app.session.flashBag` | +| A label translated in PHP shows its raw key | plain `TranslatorInterface` autowires to Thelia's `Translator`, which has no `messages` catalog - inject `#[Autowire(service: 'translator')]` | +| Front office deployed with no CSS or JavaScript | `asset-map:compile` was not run; AssetMapper's dev server is off outside debug | diff --git a/plugins/thelia/skills/thelia3/references/glossary.md b/plugins/thelia/skills/thelia3/references/glossary.md index 19050c1..77ce641 100644 --- a/plugins/thelia/skills/thelia3/references/glossary.md +++ b/plugins/thelia/skills/thelia3/references/glossary.md @@ -91,7 +91,7 @@ | `config/views.yaml` | Theme file declaring internal views (root templates that are not pages) | | `InternalViewsDeclaration` | Reads `config/views.yaml`; `ViewRenderer` 404s a request naming an internal view | | `importmap.php` | AssetMapper entrypoints and vendor packages (replaces Webpack Encore) | -| `ignore_thelia_view` | Route default opting a route out of themed view rendering (`/_components` needs it) | +| `ignore_thelia_view` | Route default opting a route out of themed view rendering, the admin firewall check and the themed error page (`/_components` needs it) | | `#[AsLiveComponent]` | Interactive Ajax component | | `#[AsTwigComponent]` | Static component | | `#[LiveProp]` | LiveComponent property (writable, url, etc.) |