Skip to content

Commit cbb0fce

Browse files
committed
docs: document view opt-out, the parser app variable, translator autowiring and PHPStan include merging
1 parent 5ee2b73 commit cbb0fce

8 files changed

Lines changed: 109 additions & 12 deletions

File tree

plugins/thelia/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "thelia",
33
"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.",
4-
"version": "0.5.0",
4+
"version": "0.6.0",
55
"author": {
66
"name": "Thelia"
77
},

plugins/thelia/skills/ddev/SKILL.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,6 @@ hooks:
111111
- exec-host: ddev mysql -uroot -proot -e "CREATE DATABASE IF NOT EXISTS db_test; GRANT ALL PRIVILEGES ON db_test.* TO 'db'@'%'; FLUSH PRIVILEGES;"
112112
- exec: php Thelia cache:clear
113113
- exec: php bin/console tailwind:build
114-
- exec: symfony run --daemon bin/console messenger:consume async -vv
115114
```
116115
117116
Key points:

plugins/thelia/skills/ddev/references/hooks-et-services.md

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,6 @@ hooks:
4848
- exec: bash -c "cd templates/backOffice/default-twig && npm install && npm run build"
4949
```
5050

51-
### Hook: Start Messenger
52-
53-
```yaml
54-
hooks:
55-
post-start:
56-
- exec: symfony run --daemon bin/console messenger:consume async -vv
57-
```
58-
5951
**Note:** `exec` commands run inside the web container. `exec-host` commands run on the host machine.
6052

6153
## Mailpit (email testing)

plugins/thelia/skills/thelia3-backoffice-twig/SKILL.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ The back-office uses two translation systems at once, and you need to know which
3333

3434
Both have to be populated to cover every string on a screen.
3535

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+
3646
Pitfalls:
3747

3848
- `|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.

plugins/thelia/skills/thelia3-tooling/SKILL.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
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."
44
---
55

66
# Thelia 3 tooling
@@ -24,6 +24,31 @@ PHPStan caches results, and the cache can go stale on generated Propel `Base` cl
2424
vendor/bin/phpstan clear-result-cache
2525
```
2626

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+
2752
## PHPUnit 11 fails on deprecated XML
2853

2954
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
4469

4570
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.
4671

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+
4774
## A missing GitHub token fails an install on an unrelated message
4875

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

plugins/thelia/skills/thelia3/SKILL.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,9 @@ final class MyModule extends BaseModule
265265
| `ConfigQuery::read()` returns stale value | PSR `thelia_config` cache not invalidated by direct SQL UPDATE | `ConfigQuery::write()` or `initCacheConfigs(true)`; `php Thelia cache:clear` |
266266
| `SecurityContext::isGranted([A, B], ...)` | combines resources in **AND** | for OR, iterate resource by resource |
267267
| `_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` |
268271

269272
## 6. Symfony-native evolution candidates
270273

plugins/thelia/skills/thelia3/references/front-office.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,32 @@ internal:
5353
5454
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.
5555

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+
5682
### Module template overrides
5783

5884
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/`):
91117
| `psesByProduct(productId)` | `PSEExtension` | JSON PSE data |
92118
| `filters_count(filters)` | `FilterExtension` | Active filter count |
93119

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+
94143
## 3. `resources()` in Twig
95144

96145
```twig
@@ -321,6 +370,19 @@ There is **no bundler, no `package.json`, no `webpack.config.js` and no `node_mo
321370

322371
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.
323372

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+
324386
## 12. Virtual product download
325387

326388
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
374436
| `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 |
375437
| Component named with `name:` or `template:` | prefix is empty and the template is derived from the class path - write the attribute bare |
376438
| 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 |

plugins/thelia/skills/thelia3/references/glossary.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191
| `config/views.yaml` | Theme file declaring internal views (root templates that are not pages) |
9292
| `InternalViewsDeclaration` | Reads `config/views.yaml`; `ViewRenderer` 404s a request naming an internal view |
9393
| `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) |
9595
| `#[AsLiveComponent]` | Interactive Ajax component |
9696
| `#[AsTwigComponent]` | Static component |
9797
| `#[LiveProp]` | LiveComponent property (writable, url, etc.) |

0 commit comments

Comments
 (0)