Skip to content

Commit aa4c653

Browse files
committed
feat: add thelia3 skill
1 parent bd89ede commit aa4c653

9 files changed

Lines changed: 3371 additions & 0 deletions

File tree

plugins/thelia/skills/thelia3/SKILL.md

Lines changed: 296 additions & 0 deletions
Large diffs are not rendered by default.

plugins/thelia/skills/thelia3/references/api-integration.md

Lines changed: 641 additions & 0 deletions
Large diffs are not rendered by default.

plugins/thelia/skills/thelia3/references/code-patterns.md

Lines changed: 582 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
# Front-office Flexy and Symfony UX - Thelia 3
2+
3+
> Front 100% Twig (Flexy bundle). Stack: Webpack Encore 4 + Tailwind 3.4 + Stimulus 3.2 + LiveComponent 2.31 + TwigComponent. Turbo and Mercure are ABSENT.
4+
5+
## 1. FlexyBundle and template overrides
6+
7+
`FlexyBundle extends AbstractBundle` (`vendor/thelia/flexy/src/FlexyBundle.php:23`). Loads `FlexyBundle\` from `THELIA_TEMPLATE_DIR/frontOffice/{theme}/src/` and `FlexyBundle\UiComponents\` from `src/UiComponents/`. Active theme read via `ConfigQuery::read('active-front-template', 'default')` (default `flexy`), exposed as Symfony parameter `%thelia_front_template%` (`TheliaKernel.php:175-179`).
8+
9+
Flexy Twig namespaces (`vendor/thelia/flexy/config/packages/twig.yaml`):
10+
11+
| Namespace | Target | Content |
12+
|---|---|---|
13+
| `@components` | `templates/frontOffice/{theme}/components/` | Atoms / Molecules / Organisms / Layout / Page |
14+
| `@UiComponents` | `templates/frontOffice/{theme}/src/UiComponents/` | PHP-backed components (LiveComponent / TwigComponent) |
15+
| `@assets` | `templates/frontOffice/{theme}/assets/` | Images / icons / vendors |
16+
| `@formTwig` | `templates/frontOffice/{theme}/form/` | Form theme |
17+
| `@{ModuleCode}Module` | `{module}/Template/` or `{module}/templates/` | Module-specific Twig templates |
18+
19+
Module override: place `{module}/templates/frontOffice/flexy/mytemplate.html.twig`. The kernel scans `{module}/templates/{templateSubdir}/` at boot (`TheliaKernel.php:835-900`) and adds via `addPath()` to the TwigParser `FilesystemLoader` **after** the active theme (`TwigParser.php:146-149`).
20+
21+
Priority: active theme -> parents -> modules (activation order) -> `default`.
22+
23+
To force priority: `addTemplateDirectory(..., $unshift = true)` (`ParserTemplateTrait.php:78-85`) - not natively exposed, requires event listener.
24+
25+
Cache: `var/cache/{env}/module_template_dirs.php` - not auto-invalidated on activation/deactivation outside `module:post-activate-all`. Clear as needed.
26+
27+
## 2. Available Twig functions
28+
29+
`DataAccessExtension` (FlexyBundle):
30+
31+
| Function | Signature | Use |
32+
|---|---|---|
33+
| `resources(path, params, format?)` | `string, array=[], ?string='jsonld'` | Internal API Platform call |
34+
| `attr(type, name)` | `string, string` | Contextual attributes (`cart`, `customer`, `product`...) |
35+
| `loop(name, type, params)` | - | **@deprecated** |
36+
| `loopCount(type, params)` | - | **@deprecated** |
37+
38+
`FlexyBundleExtension`: `attributeAv(?ProductSaleElements)`, `getCurrentCustomer()`.
39+
40+
Extensions from TwigEngine (`vendor/thelia/modules/TwigEngine/Extension/`):
41+
42+
| Function | Extension | Use |
43+
|---|---|---|
44+
| `getForm(name, data)` | `FormExtension` | `FormView` of a Thelia form |
45+
| `hook(name, params)` | `HookExtension` | Execute a hook (mainly BO) |
46+
| `path(routeId, params)` | `URLExtension` | URL (Thelia + SF routes) |
47+
| `isAuthenticated()`, `isAuthenticatedFront()`, `isAuthenticatedAdmin()` | `SecurityExtension` | Auth guards |
48+
| `assertAuth(...)`, `assertCartNotEmpty()`, `assertValidDelivery()` | `SecurityExtension` | Guards (throw if KO) |
49+
| `svg(filename)` | `SvgExtension` | Inline SVG from `assets/icons/` |
50+
| `getAttributesAndValues(...)` | `AttributeExtension` | Product attributes |
51+
| `psesByProduct(productId)` | `PSEExtension` | JSON PSE data |
52+
| `filters_count(filters)` | `FilterExtension` | Active filter count |
53+
54+
## 3. `resources()` in Twig
55+
56+
```twig
57+
{% set product = resources('/api/front/products/' ~ productId) %}
58+
59+
{% set categories = resources('/api/front/categories', {
60+
'parent': categoryId,
61+
'order[position]': 'asc',
62+
'visible': true
63+
}) %}
64+
65+
{# JSON-LD format for pagination #}
66+
{% set page = resources('/api/front/products', {
67+
'productCategories.category.id': id,
68+
'itemsPerPage': 12,
69+
'page': 1,
70+
}, 'jsonld') %}
71+
{# page['hydra:totalItems'], page['hydra:member'] #}
72+
```
73+
74+
Synchronous, internal AP call. Do NOT call inside a Twig loop without caching.
75+
76+
## 4. LiveComponents
77+
78+
`symfony/ux-live-component` 2.31. Attribute `#[AsLiveComponent]` + PHP class.
79+
80+
```php
81+
use Symfony\UX\LiveComponent\Attribute\{AsLiveComponent, LiveAction, LiveArg, LiveListener, LiveProp};
82+
use Symfony\UX\LiveComponent\{ComponentToolsTrait, ComponentWithFormTrait, DefaultActionTrait};
83+
use Thelia\Domain\Cart\CartFacade;
84+
85+
#[AsLiveComponent(name: 'Flexy:Checkout:Cart', template: '@UiComponents/Checkout/Cart/Cart.html.twig')]
86+
class Cart
87+
{
88+
use DefaultActionTrait;
89+
use ComponentToolsTrait; // emit() + dispatchBrowserEvent()
90+
use ComponentWithFormTrait; // instantiateForm() + submitForm()
91+
92+
#[LiveProp(writable: true)]
93+
public array $items = [];
94+
95+
public function __construct(private CartFacade $cartFacade) {}
96+
97+
public function mount(): void { $this->fetchCart(); }
98+
99+
#[LiveAction]
100+
public function updateQuantity(#[LiveArg] int $itemId, #[LiveArg] int $quantity): void
101+
{
102+
$this->cartFacade->updateItemQuantity(new CartItemUpdateQuantityDTO(/* ... */));
103+
$this->emit(CheckoutEvents::UPDATE_ITEM_QUANTITY_EVENT);
104+
}
105+
106+
#[LiveListener('syncCart')]
107+
public function onSyncCart(): void { $this->fetchCart(); }
108+
}
109+
```
110+
111+
Patterns:
112+
- `mount()`: one-time init, Propel dependencies resolved.
113+
- Writable `LiveProp`: modified from JS. `url: true`: sync URL (persistent filters).
114+
- `array` LiveProp must contain scalars/simple DTOs - **Propel objects are not serializable**.
115+
- Inter-component events: `$this->emit('eventName')` + `#[LiveListener('eventName')]`.
116+
- Forms: `ComponentWithFormTrait::instantiateForm()` returns the Thelia BaseForm obtained via `formService->getFormByName()`.
117+
- Validation: `$this->submitForm()` runs standard Symfony validation.
118+
- Auto CSRF on `#[LiveAction]` - do not disable.
119+
- XSS: LiveProps serialized as JSON in `data-live-props` - NEVER put secrets there.
120+
121+
**Anti-pattern**: `extends BaseFrontController` to access `requestStack` - inject the service directly in the constructor instead.
122+
123+
## 5. TwigComponents
124+
125+
`symfony/ux-twig-component`. Stateless, no Ajax.
126+
127+
```php
128+
use Symfony\UX\TwigComponent\Attribute\{AsTwigComponent, ExposeInTemplate, PostMount, PreMount};
129+
130+
#[AsTwigComponent(name: 'Flexy:ProductCard', template: '@UiComponents/ProductCard/ProductCard.html.twig')]
131+
class ProductCard
132+
{
133+
public ?int $productId = null;
134+
public ?array $product = null;
135+
136+
public function __construct(private DataAccessService $das) {}
137+
138+
#[PreMount]
139+
public function preMount(array $data): array
140+
{
141+
if (isset($data['productId'])) {
142+
return $data;
143+
}
144+
return $data;
145+
}
146+
147+
public function mount(): void
148+
{
149+
$this->product = $this->das->resources('/api/front/products/'.$this->productId);
150+
}
151+
}
152+
```
153+
154+
`#[PostMount]`: modifies the object after prop injection. `#[ExposeInTemplate]`: exposes a private property to the template.
155+
156+
Twig invocation:
157+
```twig
158+
{{ component('Flexy:ProductCard', {productId: 42}) }}
159+
{# Or direct include for static @components templates #}
160+
{{ include('@components/Organisms/CategoryCard/CategoryCard.html.twig', category) }}
161+
```
162+
163+
`twig_component.yaml`: `FlexyBundle\Twig\:` with `name_prefix: Flexy`.
164+
165+
**Trap**: calling `DataAccessService` in `mount()` = synchronous internal AP call on every inclusion. No native cache. For lists, pass data from the parent.
166+
167+
## 6. Stimulus
168+
169+
`@symfony/stimulus-bridge` 3.2 + `@hotwired/stimulus` 3.2. Convention: `{name}_controller.{js|ts}` -> identifier `{name}`.
170+
171+
Twig activation: `stimulus_controller('product')` (SF UX helper).
172+
173+
Stimulus bridge to LiveComponent: `getComponent(this.element)` from `@symfony/ux-live-component` (always `await`!).
174+
175+
`data-action: 'live#action'` or `live#emit` activate LiveActions without custom Stimulus.
176+
177+
```twig
178+
<div {{ attributes.defaults(stimulus_controller('product')) }}
179+
data-product-current-pse-id-value="{{ currentPse.id }}">
180+
```
181+
182+
```js
183+
// assets/controllers/product_controller.js
184+
import { Controller } from '@hotwired/stimulus';
185+
import { getComponent } from '@symfony/ux-live-component';
186+
187+
export default class extends Controller {
188+
async connect() {
189+
this.component = await getComponent(this.element);
190+
}
191+
}
192+
```
193+
194+
**Turbo and Mercure: ABSENT** from Flexy. Architectural choice - interactivity = LiveComponents only.
195+
196+
### Stimulus / front JS traps
197+
198+
- **`Intl` locale**: Thelia exposes locale as `fr_FR` (underscore) but `Intl.NumberFormat`/`Intl.DateTimeFormat` require `fr-FR` (`RangeError: Invalid language tag` otherwise). Always `(document.documentElement.lang || 'fr-FR').replace('_', '-')` before instantiating.
199+
- **`data-*-value` JSON**: for a Stimulus `Object`/`Array` value, always `{{ data|json_encode|e('html_attr') }}`. Without `e('html_attr')`, a quote in the JSON (e.g. a product title) silently breaks the HTML attribute (Stimulus parses a partial/empty value).
200+
- **URL template + route with regex constraint**: `path('route', {x: 'PLACEHOLDER'})` throws `InvalidParameterException` at **Twig render time** if the route declares a `requirement` on `x`. Pass a **valid** value as anchor (e.g. `'image'` for `image|document|virtual`) then substitute on the JS side on a slash-delimited segment (`url.replace('/image/', '/'+value+'/')`).
201+
- **Module front + Stimulus**: the theme loads its own `Application` via `@symfony/stimulus-bridge`. A module that starts a second `Application.start()` (`@hotwired/stimulus`) conflicts (double-loading of the same controller). Prefer vanilla JS, or register the controller in the theme's existing app.
202+
203+
## 7. Domain facades
204+
205+
`core/lib/Thelia/Domain/{Cart,Customer,Checkout,Order}/`. Single entry point for cart/checkout/auth mutations.
206+
207+
| Facade | Key methods | Use |
208+
|---|---|---|
209+
| `CartFacade` | `addItem(CartItemAddDTO)`, `removeItem(CartItemDeleteDTO)`, `updateItemQuantity(CartItemUpdateQuantityDTO)` (throws `NotEnoughStockException`), `setDeliveryAddress/InvoiceAddress/DeliveryModule/PaymentModule(CheckoutDTO)`, `getCartFromSession(): ?Cart`, `getOrCreateFromSession(): Cart` | Cart mutations, preliminary checkout selections |
210+
| `CustomerFacade` | `login(CustomerLogin)`, `logout()`, `getCurrentCustomer(): ?Customer`, `isLoggedIn(): bool`, `register(CustomerRegisterDTO): Customer`, `update(...)`, `sendCode(Customer)` | Auth + front customer CRUD |
211+
| `CheckoutFacade` | `selectDeliveryAddress/InvoiceAddress/DeliveryModule/PaymentModule(CheckoutDTO)`, `validateForOrder(Cart)`, `pay(CheckoutDTO): ?Response`, `cancelOrder(int): Order`, `resetCheckout()` | Final checkout + payment |
212+
| `OrderFacade` | low-level, internal to `CheckoutFacade` | Do NOT inject in front modules (use `CheckoutFacade::pay()`) |
213+
214+
**Trap**: `CartFacade::getCartFromSession()` can return `null`. For write actions: use `getOrCreateFromSession()`.
215+
216+
When to use a Facade vs direct Propel:
217+
218+
| Situation | Approach |
219+
|---|---|
220+
| Business operations (cart, checkout, customer) | Facade |
221+
| Simple read queries | `resources()` Twig or `DataAccessService::resources()` PHP |
222+
| Import/export scripts, CLI | Direct Propel (`XxxQuery::create()`) |
223+
| LiveComponents | Facade |
224+
225+
## 8. Mailing and PDF
226+
227+
Emails: **Smarty legacy** in `templates/email/default/` (`.html` + `.txt`). `MailerFactory` uses `ParserResolver` - if `.html` (without `.twig`), `SmartyParser` takes over. Module override: `{module}/templates/email/default/`.
228+
229+
No Twig migration planned for emails. A module that wants Twig email: create its `.html.twig` files and ensure TwigParser resolves them.
230+
231+
PDF: `templates/pdf/default/` = back-office (admin invoices) in Smarty. No PDF engine on the Flexy front side - but the core has one, event-driven: dispatching `TheliaEvents::GENERATE_PDF` with a `PdfEvent($html)` triggers `Action\Pdf` which renders via `spipu/html2pdf` (already in core, no dompdf/wkhtmltopdf needed). A module can listen to this event at higher priority to swap the renderer. The HTML source still needs to be produced (currently Smarty BO).
232+
233+
## 9. Assets - Webpack Encore + Tailwind
234+
235+
Stack:
236+
- Bundler: Webpack Encore 4.0 (NOT Vite)
237+
- CSS: PostCSS + Tailwind CSS 3.4 + `postcss-nested` + `postcss-rem`
238+
- TS: `ts-loader` 9.5
239+
- React: Babel preset-react + `@symfony/ux-react` 2.31
240+
- Stimulus: `@symfony/stimulus-bridge` 3.2 + `lazy-controller-loader`
241+
242+
Commands:
243+
```bash
244+
ddev exec bash -c "cd templates/frontOffice/flexy && npm install && npm run build"
245+
# Variants: npm run watch (encore dev --watch), npm run dev (encore dev)
246+
```
247+
248+
Public path: `/templates-assets/frontOffice/{theme}/dist`, symlinked by `EncoreExtension` at kernel boot (guard `!is_dir($dest)`). In production, `THELIA_WEB_DIR/templates-assets/` must be writable.
249+
250+
Tailwind `tailwind.config.js`: custom CSS tokens (`var(--theme)`, `var(--theme-dark)`) -> theming without rebuild. Content scanned: `components/**/*.twig`, `src/UiComponents/**/*.twig`, `form/**/*.twig`, `*.twig`.
251+
252+
## 10. Custom module components
253+
254+
Registering components directly under `FlexyBundle\UiComponents\` (which points to `{theme}/src/UiComponents/`) is not possible for a third-party module.
255+
256+
Canonical option: create a Symfony Bundle for the module with `loadExtension()` that loads a separate namespace + a `config/packages/twig.yaml` in the bundle declaring a dedicated Twig namespace.
257+
258+
```yaml
259+
# {module}/config/packages/twig.yaml
260+
twig:
261+
paths:
262+
"%kernel.project_dir%/local/modules/MyModule/templates/UiComponents": MyModuleComponents
263+
```
264+
265+
```php
266+
#[AsTwigComponent(name: 'MyModule:MyCard', template: '@MyModuleComponents/MyCard.html.twig')]
267+
class MyCard { /* ... */ }
268+
```
269+
270+
Simple option (theme coupling): place in `templates/frontOffice/flexy/src/UiComponents/`.
271+
272+
## 11. Sitemap
273+
274+
Flexy exposes `GET /sitemap` and `GET /sitemap.xml` (alias) via `FlexyBundle\Controller\SitemapController`. Rendering goes through `FlexyBundle\Service\SitemapGenerator`, cache-backed on `thelia.cache` (TTL configurable via `ConfigQuery::read('sitemap_ttl', '7200')`). The `sitemap.html.twig` template lives at the root of the active theme and consumes `resources('/api/front/{categories,products,folders,contents}', {'visible': 1})`. Accepted parameters: `?lang=<code>` (404 if unknown lang), `?context=catalog|content` (404 if other value), `?flush=1` to force cache regeneration.
275+
276+
To customize: override `sitemap.html.twig` in the child template, or inject `SitemapGenerator` in a module controller to wrap the render (custom URL addition, multi-file sitemap).
277+
278+
## 12. Traps
279+
280+
| Trap | Fix |
281+
|---|---|
282+
| `resources()` / `attr()` in CLI | guard / fallback / avoid in CLI |
283+
| LiveProp with Propel objects | not serializable - use simple DTOs or arrays |
284+
| `getCartFromSession()` can be null | `getOrCreateFromSession()` for writes |
285+
| `getComponent()` Stimulus without `await` | always `await getComponent(this.element)` |
286+
| Stale `module_template_dirs.php` cache | `cache:clear` after activation |
287+
| Missing `templates-assets/{theme}/dist` symlink | first boot, guard `!is_dir($dest)` |
288+
| `extends BaseFrontController` in LiveComponent | inject `requestStack` directly |
289+
| `resources()` call in Twig `mount()` without cache | pass data from parent or cache |
290+
| `active-front-template` = non-existent directory | always a valid `flexy` value in DB |

0 commit comments

Comments
 (0)