Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/thelia/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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"
},
Expand Down
1 change: 0 additions & 1 deletion plugins/thelia/skills/ddev/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 0 additions & 8 deletions plugins/thelia/skills/ddev/references/hooks-et-services.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions plugins/thelia/skills/thelia3-backoffice-twig/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<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:

```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.
Expand Down
29 changes: 28 additions & 1 deletion plugins/thelia/skills/thelia3-tooling/SKILL.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 <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.

## 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.
Expand All @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions plugins/thelia/skills/thelia3/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<locale>.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

Expand Down
66 changes: 66 additions & 0 deletions plugins/thelia/skills/thelia3/references/front-office.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 `<assets>dist</assets>` 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:
Expand Down Expand Up @@ -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 |
2 changes: 1 addition & 1 deletion plugins/thelia/skills/thelia3/references/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.) |
Expand Down
Loading