You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: plugins/thelia/.claude-plugin/plugin.json
+1-1Lines changed: 1 addition & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -1,7 +1,7 @@
1
1
{
2
2
"name": "thelia",
3
3
"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.",
Copy file name to clipboardExpand all lines: plugins/thelia/skills/thelia3-backoffice-twig/SKILL.md
+10Lines changed: 10 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -33,6 +33,16 @@ The back-office uses two translation systems at once, and you need to know which
33
33
34
34
Both have to be populated to cover every string on a screen.
35
35
36
+
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.<locale>.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:
37
+
38
+
```php
39
+
public function __construct(
40
+
#[Autowire(service: 'translator')]
41
+
private readonly TranslatorInterface $translator,
42
+
) {
43
+
}
44
+
```
45
+
36
46
Pitfalls:
37
47
38
48
-`|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.
Copy file name to clipboardExpand all lines: plugins/thelia/skills/thelia3-tooling/SKILL.md
+28-1Lines changed: 28 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,6 +1,6 @@
1
1
---
2
2
name: thelia3-tooling
3
-
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."
3
+
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."
4
4
---
5
5
6
6
# Thelia 3 tooling
@@ -24,6 +24,31 @@ PHPStan caches results, and the cache can go stale on generated Propel `Base` cl
24
24
vendor/bin/phpstan clear-result-cache
25
25
```
26
26
27
+
## A PHPStan `includes:` merges `paths`, it does not replace them
28
+
29
+
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.
30
+
31
+
```neon
32
+
includes:
33
+
- phpstan.neon # already declares paths and a level
34
+
parameters:
35
+
paths:
36
+
- templates/backOffice/default-twig/src
37
+
```
38
+
39
+
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.
40
+
41
+
Mark the key with `!` to replace instead of append:
42
+
43
+
```neon
44
+
parameters:
45
+
level: 5
46
+
paths!:
47
+
- templates/backOffice/default-twig/src
48
+
```
49
+
50
+
`vendor/bin/phpstan dump-parameters -c <config>` 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.
51
+
27
52
## PHPUnit 11 fails on deprecated XML
28
53
29
54
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
44
69
45
70
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.
46
71
72
+
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.
73
+
47
74
## A missing GitHub token fails an install on an unrelated message
48
75
49
76
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:
Copy file name to clipboardExpand all lines: plugins/thelia/skills/thelia3/SKILL.md
+3Lines changed: 3 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -265,6 +265,9 @@ final class MyModule extends BaseModule
265
265
|`ConfigQuery::read()` returns stale value | PSR `thelia_config` cache not invalidated by direct SQL UPDATE |`ConfigQuery::write()` or `initCacheConfigs(true)`; `php Thelia cache:clear`|
266
266
|`SecurityContext::isGranted([A, B], ...)`| combines resources in **AND**| for OR, iterate resource by resource |
267
267
|`_or()` Propel | switches **the entire WHERE** to OR |`condition()` + `combine([...], 'OR')`|
268
+
| 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`|
269
+
| 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.<locale>.php`|`#[Autowire(service: 'translator')]` when you want the Symfony translator |
270
+
|`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`|
Copy file name to clipboardExpand all lines: plugins/thelia/skills/thelia3/references/front-office.md
+66Lines changed: 66 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -53,6 +53,32 @@ internal:
53
53
54
54
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.
55
55
56
+
### Routes that are not views: `ignore_thelia_view`
57
+
58
+
`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.
59
+
60
+
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.
61
+
62
+
Declare the flag in the route defaults for anything that is not a themed page: JSON and AJAX endpoints, webhooks, callbacks, file downloads.
63
+
64
+
```php
65
+
#[Route(
66
+
'/mymodule/webhook',
67
+
name: 'mymodule_webhook',
68
+
defaults: ['ignore_thelia_view' => true],
69
+
)]
70
+
```
71
+
72
+
One flag, three listeners, all keyed on the same request attribute:
73
+
74
+
| Listener | Without the flag | With it |
75
+
|---|---|---|
76
+
| `ViewListener` | renders the themed view | stands down |
77
+
| `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 |
78
+
| `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 |
79
+
80
+
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.
81
+
56
82
### Module template overrides
57
83
58
84
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/`):
91
117
| `psesByProduct(productId)` | `PSEExtension` | JSON PSE data |
92
118
| `filters_count(filters)` | `FilterExtension` | Active filter count |
93
119
120
+
### The `app` variable is not Symfony's `AppVariable`
121
+
122
+
`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`:
123
+
124
+
```php
125
+
'app' => (object) [
126
+
'environment' => ...,
127
+
'request' => ...,
128
+
'session' => ..., // null when the request carries none
129
+
'debug' => ...,
130
+
],
131
+
```
132
+
133
+
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.
134
+
135
+
Read flashes through the session instead, which is what the theme does:
136
+
137
+
```twig
138
+
{% for message in app.session ? app.session.flashBag.get('error') : [] %}
139
+
```
140
+
141
+
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.
142
+
94
143
## 3. `resources()` in Twig
95
144
96
145
```twig
@@ -321,6 +370,19 @@ There is **no bundler, no `package.json`, no `webpack.config.js` and no `node_mo
321
370
322
371
Public output is AssetMapper's, under `/assets/frontOffice/{theme}/`. The old `templates-assets/{theme}/dist` symlink and the `<assets>dist</assets>` entry in `template.xml` no longer apply to Flexy.
323
372
373
+
### Building for production
374
+
375
+
What `bin/install` runs is a development build. A production deployment needs two more steps:
376
+
377
+
```bash
378
+
php bin/console tailwind:build --minify
379
+
php bin/console asset-map:compile
380
+
```
381
+
382
+
`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.
383
+
384
+
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.
385
+
324
386
## 12. Virtual product download
325
387
326
388
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
374
436
| `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 |
375
437
| Component named with `name:` or `template:` | prefix is empty and the template is derived from the class path - write the attribute bare |
376
438
| A form rendering as bare Symfony markup | no theme is global any more; add `{% form_theme form with flexy_form_themes only %}` |
439
+
| A module JSON or webhook route answers a themed 404 page | it is still going through `ViewListener`; add `defaults: ['ignore_thelia_view' => true]` |
440
+
| `app.flashes()` or `app.user` renders nothing | the parser's `app` is a four-property stub, not `AppVariable` - read flashes from `app.session.flashBag` |
441
+
| 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')]` |
442
+
| Front office deployed with no CSS or JavaScript | `asset-map:compile` was not run; AssetMapper's dev server is off outside debug |
Copy file name to clipboardExpand all lines: plugins/thelia/skills/thelia3/references/glossary.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -91,7 +91,7 @@
91
91
|`config/views.yaml`| Theme file declaring internal views (root templates that are not pages) |
92
92
|`InternalViewsDeclaration`| Reads `config/views.yaml`; `ViewRenderer` 404s a request naming an internal view |
93
93
|`importmap.php`| AssetMapper entrypoints and vendor packages (replaces Webpack Encore) |
94
-
|`ignore_thelia_view`| Route default opting a route out of themed view rendering (`/_components` needs it) |
94
+
|`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) |
0 commit comments